diff --git a/images/virtualization-artifact/pkg/controller/service/restorer/keys.go b/images/virtualization-artifact/pkg/controller/service/restorer/keys.go index c3d9670c45..baa8543a82 100644 --- a/images/virtualization-artifact/pkg/controller/service/restorer/keys.go +++ b/images/virtualization-artifact/pkg/controller/service/restorer/keys.go @@ -20,5 +20,6 @@ const ( virtualMachineKey = "vm" virtualMachineBlockDeviceAttachmentKey = "vmbdas" virtualMachineIPAddressKey = "vmip" + virtualMachineMACAddressesKey = "vmmacs" provisionerKey = "provisioner" ) diff --git a/images/virtualization-artifact/pkg/controller/service/restorer/restorer.go b/images/virtualization-artifact/pkg/controller/service/restorer/restorer.go index f71b7ceadc..b8436db8e6 100644 --- a/images/virtualization-artifact/pkg/controller/service/restorer/restorer.go +++ b/images/virtualization-artifact/pkg/controller/service/restorer/restorer.go @@ -70,6 +70,11 @@ func (r SecretRestorer) Store(ctx context.Context, vm *virtv2.VirtualMachine, vm return nil, err } + err = r.setVirtualMachineMACAddresses(ctx, &secret, vm) + if err != nil { + return nil, err + } + err = r.setProvisioning(ctx, &secret, vm) if err != nil { return nil, err @@ -100,6 +105,26 @@ func (r SecretRestorer) RestoreVirtualMachineIPAddress(_ context.Context, secret return get[*virtv2.VirtualMachineIPAddress](secret, virtualMachineIPAddressKey) } +func (r SecretRestorer) RestoreVirtualMachineMACAddresses(_ context.Context, secret *corev1.Secret) ([]*virtv2.VirtualMachineMACAddress, error) { + return get[[]*virtv2.VirtualMachineMACAddress](secret, virtualMachineMACAddressesKey) +} + +func (r SecretRestorer) RestoreMACAddressOrder(_ context.Context, secret *corev1.Secret) ([]string, error) { + vm, err := get[*virtv2.VirtualMachine](secret, virtualMachineKey) + if err != nil { + return nil, err + } + + var macAddressOrder []string + for _, ns := range vm.Status.Networks { + if ns.Type == virtv2.NetworksTypeMain { + continue + } + macAddressOrder = append(macAddressOrder, ns.MAC) + } + return macAddressOrder, nil +} + func (r SecretRestorer) RestoreVirtualMachineBlockDeviceAttachments(_ context.Context, secret *corev1.Secret) ([]*virtv2.VirtualMachineBlockDeviceAttachment, error) { return get[[]*virtv2.VirtualMachineBlockDeviceAttachment](secret, virtualMachineBlockDeviceAttachmentKey) } @@ -225,6 +250,38 @@ func (r SecretRestorer) setVirtualMachineIPAddress(ctx context.Context, secret * return nil } +func (r SecretRestorer) setVirtualMachineMACAddresses(ctx context.Context, secret *corev1.Secret, vm *virtv2.VirtualMachine) error { + var vmmacs []virtv2.VirtualMachineMACAddress + for _, ns := range vm.Status.Networks { + if ns.Type == virtv2.NetworksTypeMain { + continue + } + + vmmac, err := object.FetchObject(ctx, types.NamespacedName{ + Namespace: vm.Namespace, + Name: ns.VirtualMachineMACAddressName, + }, r.client, &virtv2.VirtualMachineMACAddress{}) + if err != nil { + return err + } + + if vmmac == nil { + return fmt.Errorf("the virtual machine mac address %q not found", ns.VirtualMachineMACAddressName) + } + + vmmac.Spec.Address = vmmac.Status.Address + vmmacs = append(vmmacs, *vmmac) + } + + JSON, err := json.Marshal(vmmacs) + if err != nil { + return err + } + + secret.Data[virtualMachineMACAddressesKey] = []byte(base64.StdEncoding.EncodeToString(JSON)) + return nil +} + func (r SecretRestorer) setProvisioning(ctx context.Context, secret *corev1.Secret, vm *virtv2.VirtualMachine) error { var secretName string diff --git a/images/virtualization-artifact/pkg/controller/vmrestore/internal/interfaces.go b/images/virtualization-artifact/pkg/controller/vmrestore/internal/interfaces.go index dfb6363f7a..56dfdbf195 100644 --- a/images/virtualization-artifact/pkg/controller/vmrestore/internal/interfaces.go +++ b/images/virtualization-artifact/pkg/controller/vmrestore/internal/interfaces.go @@ -31,4 +31,6 @@ type Restorer interface { RestoreProvisioner(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) RestoreVirtualMachineIPAddress(ctx context.Context, secret *corev1.Secret) (*virtv2.VirtualMachineIPAddress, error) RestoreVirtualMachineBlockDeviceAttachments(ctx context.Context, secret *corev1.Secret) ([]*virtv2.VirtualMachineBlockDeviceAttachment, error) + RestoreVirtualMachineMACAddresses(ctx context.Context, secret *corev1.Secret) ([]*virtv2.VirtualMachineMACAddress, error) + RestoreMACAddressOrder(ctx context.Context, secret *corev1.Secret) ([]string, error) } diff --git a/images/virtualization-artifact/pkg/controller/vmrestore/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vmrestore/internal/life_cycle.go index 6b8b732018..37c66ef637 100644 --- a/images/virtualization-artifact/pkg/controller/vmrestore/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vmrestore/internal/life_cycle.go @@ -160,6 +160,35 @@ func (h LifeCycleHandler) Handle(ctx context.Context, vmRestore *virtv2.VirtualM overrideValidators = append(overrideValidators, restorer.NewVirtualMachineIPAddressOverrideValidator(vmip, h.client, string(vmRestore.UID))) } + vmmacs, err := h.restorer.RestoreVirtualMachineMACAddresses(ctx, restorerSecret) + if err != nil { + setPhaseConditionToFailed(cb, &vmRestore.Status.Phase, err) + return reconcile.Result{}, err + } + + macAddressOrder, err := h.restorer.RestoreMACAddressOrder(ctx, restorerSecret) + if err != nil { + setPhaseConditionToFailed(cb, &vmRestore.Status.Phase, err) + return reconcile.Result{}, err + } + + if len(vmmacs) > 0 { + macAddressNamesByAddress := make(map[string]string) + for _, vmmac := range vmmacs { + overrideValidators = append(overrideValidators, restorer.NewVirtualMachineMACAddressOverrideValidator(vmmac, h.client, string(vmRestore.UID))) + macAddressNamesByAddress[vmmac.Status.Address] = vmmac.Name + } + + for i := range vm.Spec.Networks { + ns := &vm.Spec.Networks[i] + if ns.Type == virtv2.NetworksTypeMain { + continue + } + + ns.VirtualMachineMACAddressName = macAddressNamesByAddress[macAddressOrder[i-1]] + } + } + overrideValidators = append(overrideValidators, restorer.NewVirtualMachineOverrideValidator(vm, h.client, string(vmRestore.UID))) overridedVMName, err = h.getOverrridedVMName(overrideValidators) diff --git a/images/virtualization-artifact/pkg/controller/vmrestore/internal/mock.go b/images/virtualization-artifact/pkg/controller/vmrestore/internal/mock.go index 33e3fa69b7..67bdbbf2bd 100644 --- a/images/virtualization-artifact/pkg/controller/vmrestore/internal/mock.go +++ b/images/virtualization-artifact/pkg/controller/vmrestore/internal/mock.go @@ -20,6 +20,9 @@ var _ Restorer = &RestorerMock{} // // // make and configure a mocked Restorer // mockedRestorer := &RestorerMock{ +// RestoreMACAddressOrderFunc: func(ctx context.Context, secret *corev1.Secret) ([]string, error) { +// panic("mock out the RestoreMACAddressOrder method") +// }, // RestoreProvisionerFunc: func(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) { // panic("mock out the RestoreProvisioner method") // }, @@ -32,6 +35,9 @@ var _ Restorer = &RestorerMock{} // RestoreVirtualMachineIPAddressFunc: func(ctx context.Context, secret *corev1.Secret) (*virtv2.VirtualMachineIPAddress, error) { // panic("mock out the RestoreVirtualMachineIPAddress method") // }, +// RestoreVirtualMachineMACAddressesFunc: func(ctx context.Context, secret *corev1.Secret) ([]*virtv2.VirtualMachineMACAddress, error) { +// panic("mock out the RestoreVirtualMachineMACAddresses method") +// }, // } // // // use mockedRestorer in code that requires Restorer @@ -39,6 +45,9 @@ var _ Restorer = &RestorerMock{} // // } type RestorerMock struct { + // RestoreMACAddressOrderFunc mocks the RestoreMACAddressOrder method. + RestoreMACAddressOrderFunc func(ctx context.Context, secret *corev1.Secret) ([]string, error) + // RestoreProvisionerFunc mocks the RestoreProvisioner method. RestoreProvisionerFunc func(ctx context.Context, secret *corev1.Secret) (*corev1.Secret, error) @@ -51,8 +60,18 @@ type RestorerMock struct { // RestoreVirtualMachineIPAddressFunc mocks the RestoreVirtualMachineIPAddress method. RestoreVirtualMachineIPAddressFunc func(ctx context.Context, secret *corev1.Secret) (*virtv2.VirtualMachineIPAddress, error) + // RestoreVirtualMachineMACAddressesFunc mocks the RestoreVirtualMachineMACAddresses method. + RestoreVirtualMachineMACAddressesFunc func(ctx context.Context, secret *corev1.Secret) ([]*virtv2.VirtualMachineMACAddress, error) + // calls tracks calls to the methods. calls struct { + // RestoreMACAddressOrder holds details about calls to the RestoreMACAddressOrder method. + RestoreMACAddressOrder []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Secret is the secret argument value. + Secret *corev1.Secret + } // RestoreProvisioner holds details about calls to the RestoreProvisioner method. RestoreProvisioner []struct { // Ctx is the ctx argument value. @@ -81,11 +100,56 @@ type RestorerMock struct { // Secret is the secret argument value. Secret *corev1.Secret } + // RestoreVirtualMachineMACAddresses holds details about calls to the RestoreVirtualMachineMACAddresses method. + RestoreVirtualMachineMACAddresses []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Secret is the secret argument value. + Secret *corev1.Secret + } } + lockRestoreMACAddressOrder sync.RWMutex lockRestoreProvisioner sync.RWMutex lockRestoreVirtualMachine sync.RWMutex lockRestoreVirtualMachineBlockDeviceAttachments sync.RWMutex lockRestoreVirtualMachineIPAddress sync.RWMutex + lockRestoreVirtualMachineMACAddresses sync.RWMutex +} + +// RestoreMACAddressOrder calls RestoreMACAddressOrderFunc. +func (mock *RestorerMock) RestoreMACAddressOrder(ctx context.Context, secret *corev1.Secret) ([]string, error) { + if mock.RestoreMACAddressOrderFunc == nil { + panic("RestorerMock.RestoreMACAddressOrderFunc: method is nil but Restorer.RestoreMACAddressOrder was just called") + } + callInfo := struct { + Ctx context.Context + Secret *corev1.Secret + }{ + Ctx: ctx, + Secret: secret, + } + mock.lockRestoreMACAddressOrder.Lock() + mock.calls.RestoreMACAddressOrder = append(mock.calls.RestoreMACAddressOrder, callInfo) + mock.lockRestoreMACAddressOrder.Unlock() + return mock.RestoreMACAddressOrderFunc(ctx, secret) +} + +// RestoreMACAddressOrderCalls gets all the calls that were made to RestoreMACAddressOrder. +// Check the length with: +// +// len(mockedRestorer.RestoreMACAddressOrderCalls()) +func (mock *RestorerMock) RestoreMACAddressOrderCalls() []struct { + Ctx context.Context + Secret *corev1.Secret +} { + var calls []struct { + Ctx context.Context + Secret *corev1.Secret + } + mock.lockRestoreMACAddressOrder.RLock() + calls = mock.calls.RestoreMACAddressOrder + mock.lockRestoreMACAddressOrder.RUnlock() + return calls } // RestoreProvisioner calls RestoreProvisionerFunc. @@ -231,3 +295,39 @@ func (mock *RestorerMock) RestoreVirtualMachineIPAddressCalls() []struct { mock.lockRestoreVirtualMachineIPAddress.RUnlock() return calls } + +// RestoreVirtualMachineMACAddresses calls RestoreVirtualMachineMACAddressesFunc. +func (mock *RestorerMock) RestoreVirtualMachineMACAddresses(ctx context.Context, secret *corev1.Secret) ([]*virtv2.VirtualMachineMACAddress, error) { + if mock.RestoreVirtualMachineMACAddressesFunc == nil { + panic("RestorerMock.RestoreVirtualMachineMACAddressesFunc: method is nil but Restorer.RestoreVirtualMachineMACAddresses was just called") + } + callInfo := struct { + Ctx context.Context + Secret *corev1.Secret + }{ + Ctx: ctx, + Secret: secret, + } + mock.lockRestoreVirtualMachineMACAddresses.Lock() + mock.calls.RestoreVirtualMachineMACAddresses = append(mock.calls.RestoreVirtualMachineMACAddresses, callInfo) + mock.lockRestoreVirtualMachineMACAddresses.Unlock() + return mock.RestoreVirtualMachineMACAddressesFunc(ctx, secret) +} + +// RestoreVirtualMachineMACAddressesCalls gets all the calls that were made to RestoreVirtualMachineMACAddresses. +// Check the length with: +// +// len(mockedRestorer.RestoreVirtualMachineMACAddressesCalls()) +func (mock *RestorerMock) RestoreVirtualMachineMACAddressesCalls() []struct { + Ctx context.Context + Secret *corev1.Secret +} { + var calls []struct { + Ctx context.Context + Secret *corev1.Secret + } + mock.lockRestoreVirtualMachineMACAddresses.RLock() + calls = mock.calls.RestoreVirtualMachineMACAddresses + mock.lockRestoreVirtualMachineMACAddresses.RUnlock() + return calls +} diff --git a/images/virtualization-artifact/pkg/controller/vmrestore/internal/restorer/vmmac_restorer.go b/images/virtualization-artifact/pkg/controller/vmrestore/internal/restorer/vmmac_restorer.go new file mode 100644 index 0000000000..9e1c56fb28 --- /dev/null +++ b/images/virtualization-artifact/pkg/controller/vmrestore/internal/restorer/vmmac_restorer.go @@ -0,0 +1,171 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package restorer + +import ( + "context" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/deckhouse/virtualization-controller/pkg/common/annotations" + "github.com/deckhouse/virtualization-controller/pkg/common/object" + "github.com/deckhouse/virtualization-controller/pkg/controller/indexer" + virtv2 "github.com/deckhouse/virtualization/api/core/v1alpha2" +) + +type VirtualMachineMACAddressOverrideValidator struct { + vmmac *virtv2.VirtualMachineMACAddress + client client.Client + vmRestoreUID string +} + +func NewVirtualMachineMACAddressOverrideValidator(vmmacTmpl *virtv2.VirtualMachineMACAddress, client client.Client, vmRestoreUID string) *VirtualMachineMACAddressOverrideValidator { + if vmmacTmpl.Annotations != nil { + vmmacTmpl.Annotations[annotations.AnnVMRestore] = vmRestoreUID + } else { + vmmacTmpl.Annotations = make(map[string]string) + vmmacTmpl.Annotations[annotations.AnnVMRestore] = vmRestoreUID + } + return &VirtualMachineMACAddressOverrideValidator{ + vmmac: &virtv2.VirtualMachineMACAddress{ + TypeMeta: metav1.TypeMeta{ + Kind: vmmacTmpl.Kind, + APIVersion: vmmacTmpl.APIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: vmmacTmpl.Name, + Namespace: vmmacTmpl.Namespace, + Annotations: vmmacTmpl.Annotations, + Labels: vmmacTmpl.Labels, + }, + Spec: vmmacTmpl.Spec, + Status: vmmacTmpl.Status, + }, + client: client, + vmRestoreUID: vmRestoreUID, + } +} + +func (v *VirtualMachineMACAddressOverrideValidator) Override(rules []virtv2.NameReplacement) { + v.vmmac.Name = overrideName(v.vmmac.Kind, v.vmmac.Name, rules) +} + +func (v *VirtualMachineMACAddressOverrideValidator) Validate(ctx context.Context) error { + vmmacKey := types.NamespacedName{Namespace: v.vmmac.Namespace, Name: v.vmmac.Name} + existed, err := object.FetchObject(ctx, vmmacKey, v.client, &virtv2.VirtualMachineMACAddress{}) + if err != nil { + return err + } + + if existed == nil { + if v.vmmac.Spec.Address == "" { + return nil + } + + var vmmacs virtv2.VirtualMachineMACAddressList + err = v.client.List(ctx, &vmmacs, &client.ListOptions{ + Namespace: v.vmmac.Namespace, + FieldSelector: fields.OneTermEqualSelector(indexer.IndexFieldVMMACByAddress, v.vmmac.Spec.Address), + }) + if err != nil { + return err + } + + if len(vmmacs.Items) > 0 { + return fmt.Errorf( + "the set address %q is %w by the different virtual machine mac address %q and cannot be used for the restored virtual machine", + v.vmmac.Spec.Address, ErrAlreadyInUse, vmmacs.Items[0].Name, + ) + } + + return nil + } + + if existed.Status.Phase == virtv2.VirtualMachineMACAddressPhaseAttached || existed.Status.VirtualMachine != "" { + return fmt.Errorf("the virtual machine mac address %q is %w and cannot be used for the restored virtual machine", vmmacKey.Name, ErrAlreadyInUse) + } + + return nil +} + +func (v *VirtualMachineMACAddressOverrideValidator) ValidateWithForce(ctx context.Context) error { + vmmacKey := types.NamespacedName{Namespace: v.vmmac.Namespace, Name: v.vmmac.Name} + existed, err := object.FetchObject(ctx, vmmacKey, v.client, &virtv2.VirtualMachineMACAddress{}) + if err != nil { + return err + } + + vmName := v.vmmac.Status.VirtualMachine + + if existed == nil { + if v.vmmac.Spec.Address == "" { + return nil + } + + var vmmacs virtv2.VirtualMachineMACAddressList + err = v.client.List(ctx, &vmmacs, &client.ListOptions{ + Namespace: v.vmmac.Namespace, + FieldSelector: fields.OneTermEqualSelector(indexer.IndexFieldVMMACByAddress, v.vmmac.Spec.Address), + }) + if err != nil { + return err + } + + if len(vmmacs.Items) > 0 { + return fmt.Errorf( + "the set address %q is %w by the different virtual machine mac address %q and cannot be used for the restored virtual machine", + v.vmmac.Spec.Address, ErrAlreadyInUse, vmmacs.Items[0].Name, + ) + } + + return nil + } + + if existed.Status.Phase == virtv2.VirtualMachineMACAddressPhaseAttached && existed.Status.VirtualMachine == vmName { + return ErrAlreadyExists + } + + if existed.Status.Phase == virtv2.VirtualMachineMACAddressPhaseAttached || existed.Status.VirtualMachine != "" { + return fmt.Errorf("the virtual machine mac address %q is %w and cannot be used for the restored virtual machine", vmmacKey.Name, ErrAlreadyInUse) + } + + return nil +} + +func (v *VirtualMachineMACAddressOverrideValidator) ProcessWithForce(_ context.Context) error { + return nil +} + +func (v *VirtualMachineMACAddressOverrideValidator) Object() client.Object { + return &virtv2.VirtualMachineMACAddress{ + TypeMeta: metav1.TypeMeta{ + Kind: v.vmmac.Kind, + APIVersion: v.vmmac.APIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: v.vmmac.Name, + Namespace: v.vmmac.Namespace, + Annotations: v.vmmac.Annotations, + Labels: v.vmmac.Labels, + }, + Spec: v.vmmac.Spec, + } +} diff --git a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle.go b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle.go index 143940cbe5..e4c9056b75 100644 --- a/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle.go +++ b/images/virtualization-artifact/pkg/controller/vmsnapshot/internal/life_cycle.go @@ -676,6 +676,30 @@ func (h LifeCycleHandler) fillStatusResources(ctx context.Context, vmSnapshot *v }) } + if len(vm.Spec.Networks) > 1 { + for _, ns := range vm.Status.Networks { + if ns.Type == virtv2.NetworksTypeMain { + continue + } + + vmmac, err := object.FetchObject(ctx, types.NamespacedName{ + Namespace: vm.Namespace, + Name: ns.VirtualMachineMACAddressName, + }, h.client, &virtv2.VirtualMachineMACAddress{}) + if err != nil { + return err + } + if vmmac == nil { + return fmt.Errorf("the virtual machine mac address %q not found", ns.VirtualMachineMACAddressName) + } + vmSnapshot.Status.Resources = append(vmSnapshot.Status.Resources, virtv2.ResourceRef{ + Kind: vmmac.Kind, + ApiVersion: vmmac.APIVersion, + Name: vmmac.Name, + }) + } + } + provisioner, err := h.getProvisionerFromVM(ctx, vm) if err != nil { return err