Ctrl K
ComunidadGuías…

api/handler/service_vm_live_update_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"context"
	"errors"
	"strings"
	"testing"

	"github.com/golang/mock/gomock"
	"github.com/goodrain/rainbond/db"
	dbmodel "github.com/goodrain/rainbond/db/model"
	workermodel "github.com/goodrain/rainbond/worker/discover/model"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	kubefake "k8s.io/client-go/kubernetes/fake"
	v1 "kubevirt.io/api/core/v1"
	kubecli "kubevirt.io/client-go/kubecli"
)

func TestServiceVerticalVMLiveUpdatePatchesRunningVM(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    6000,
			ContainerMemory: 12288,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMList := kubecli.NewMockVirtualMachineInterface(ctrl)
	mockVMPatch := kubecli.NewMockVirtualMachineInterface(ctrl)
	mockVMIList := kubecli.NewMockVirtualMachineInstanceInterface(ctrl)
	mockKV := kubecli.NewMockKubeVirtInterface(ctrl)

	mockClient.EXPECT().VirtualMachine("").Return(mockVMList)
	mockVMList.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-vm"}).Return(&v1.VirtualMachineList{
		Items: []v1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"},
				Spec: v1.VirtualMachineSpec{
					Template: &v1.VirtualMachineInstanceTemplateSpec{
						Spec: v1.VirtualMachineInstanceSpec{
							Domain: v1.DomainSpec{
								CPU: &v1.CPU{
									Sockets:    6,
									Cores:      1,
									Threads:    1,
									MaxSockets: 8,
								},
								Memory: &v1.Memory{
									Guest:    quantityPtr("12Gi"),
									MaxGuest: quantityPtr("16Gi"),
								},
							},
						},
					},
				},
				Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
			},
		},
	}, nil)

	mockClient.EXPECT().VirtualMachineInstance("").Return(mockVMIList)
	mockVMIList.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-vm"}).Return(&v1.VirtualMachineInstanceList{
		Items: []v1.VirtualMachineInstance{
			{
				ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"},
				Status: v1.VirtualMachineInstanceStatus{
					Phase: v1.Running,
					Conditions: []v1.VirtualMachineInstanceCondition{
						{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
					},
				},
			},
		},
	}, nil)

	mockClient.EXPECT().KubeVirt("").Return(mockKV)
	mockKV.EXPECT().List(gomock.Any(), gomock.Any()).Return(&v1.KubeVirtList{
		Items: []v1.KubeVirt{
			{
				Spec: v1.KubeVirtSpec{
					WorkloadUpdateStrategy: v1.KubeVirtWorkloadUpdateStrategy{
						WorkloadUpdateMethods: []v1.WorkloadUpdateMethod{v1.WorkloadUpdateMethodLiveMigrate},
					},
					Configuration: v1.KubeVirtConfiguration{
						VMRolloutStrategy: func() *v1.VMRolloutStrategy {
							strategy := v1.VMRolloutStrategyLiveUpdate
							return &strategy
						}(),
					},
				},
			},
		},
	}, nil)

	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMPatch)
	mockVMPatch.EXPECT().Patch(gomock.Any(), "demo-vm", types.JSONPatchType, gomock.Any(), gomock.Any()).DoAndReturn(
		func(_ context.Context, _ string, _ types.PatchType, data []byte, _ metav1.PatchOptions, _ ...string) (*v1.VirtualMachine, error) {
			payload := string(data)
			if !strings.Contains(payload, "/spec/template/spec/domain/cpu/sockets") {
				t.Fatalf("expected cpu sockets patch, got %s", payload)
			}
			return &v1.VirtualMachine{}, nil
		},
	)

	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: mockClient,
	}
	newCPU := 7000
	memory := 12288
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerCPU:    &newCPU,
		ContainerMemory: &memory,
	})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerCPU != 7000 {
		t.Fatalf("expected updated cpu persisted, got %#v", serviceDao.updatedService)
	}
}

func TestServiceVerticalVMStoppedFallsBackToSpecSync(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    4000,
			ContainerMemory: 8192,
			ContainerGPU:    0,
		},
	}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   &resourceSyncEventDao{},
	})
	defer db.SetTestManager(nil)

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMList := kubecli.NewMockVirtualMachineInterface(ctrl)

	mockClient.EXPECT().VirtualMachine("").Return(mockVMList)
	mockVMList.EXPECT().List(gomock.Any(), metav1.ListOptions{LabelSelector: "service_id=service-vm"}).Return(&v1.VirtualMachineList{
		Items: []v1.VirtualMachine{
			{
				ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"},
				Status:     v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusStopped},
			},
		},
	}, nil)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: mockClient,
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	newCPU := 5000
	newMemory := 10240
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerCPU:    &newCPU,
		ContainerMemory: &newMemory,
	})
	if err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected vm spec sync for stopped VM, got %q", syncedServiceID)
	}
}

// capability_id: rainbond.vm-live-update.unsupported-auto-restart
func TestServiceVerticalVMNonMigratableFallsBackToSpecSyncAndRestart(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    4000,
			ContainerMemory: 8192,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: mockClient,
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"},
			Status:     v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{
					Type:    v1.VirtualMachineInstanceIsMigratable,
					Status:  "False",
					Message: "PVC manual82 is not shared",
				},
			},
		}}, nil
	}

	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	newMemory := 10240
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerMemory: &newMemory,
	})
	if err != nil {
		t.Fatalf("expected non-migratable running VM to restart instead of failing live update, got %v", err)
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID)
	}
	if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerMemory != 10240 {
		t.Fatalf("expected updated memory to persist, got %#v", serviceDao.updatedService)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status, got %#v", eventDao.statuses)
	}
}

func TestServiceVerticalVMFixedPodIPFallsBackToSpecSyncAndRestart(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    4000,
			ContainerMemory: 8192,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: mockClient,
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) {
		if componentID != "service-vm" {
			t.Fatalf("unexpected component id %q", componentID)
		}
		return map[string]string{
			"vm_fixed_ip_enabled": "true",
			"vm_fixed_ip":         "10.42.247.130",
		}, nil
	}
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"},
			Status:     v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}

	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	newMemory := 10240
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerMemory: &newMemory,
	})
	if err != nil {
		t.Fatalf("expected fixed pod ip VM to restart instead of live update, got %v", err)
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID)
	}
	if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerMemory != 10240 {
		t.Fatalf("expected updated memory to persist, got %#v", serviceDao.updatedService)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status, got %#v", eventDao.statuses)
	}
}

// capability_id: rainbond.vm-live-update.unsupported-auto-restart
func TestServiceVerticalVMPatchMigrationErrorFallsBackToSpecSyncAndRestart(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    4000,
			ContainerMemory: 8192,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: mockClient,
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"},
			Spec: v1.VirtualMachineSpec{
				Template: &v1.VirtualMachineInstanceTemplateSpec{
					Spec: v1.VirtualMachineInstanceSpec{
						Domain: v1.DomainSpec{
							Memory: &v1.Memory{
								Guest:    quantityPtr("8Gi"),
								MaxGuest: quantityPtr("16Gi"),
							},
						},
					},
				},
			},
			Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface).Times(2)
	mockVMInterface.EXPECT().Patch(gomock.Any(), "demo-vm", types.JSONPatchType, gomock.Any(), gomock.Any()).
		Return(nil, errors.New("cannot migrate VMI: PVC manual82 is not shared, live migration requires that all PVCs must be shared (using ReadWriteMany access mode)"))
	mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	newMemory := 10240
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerMemory: &newMemory,
	})
	if err != nil {
		t.Fatalf("expected live migration pvc error to restart instead of failing vertical update, got %v", err)
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID)
	}
	if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerMemory != 10240 {
		t.Fatalf("expected updated memory to persist, got %#v", serviceDao.updatedService)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status, got %#v", eventDao.statuses)
	}
}

// capability_id: rainbond.vm-live-update.cpu-memory-combined-rejected
func TestServiceVerticalVMLiveUpdateRejectsCombinedCPUAndMemoryChange(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    8000,
			ContainerMemory: 16384,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: kubecli.NewMockKubevirtClient(ctrl),
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"},
			Spec: v1.VirtualMachineSpec{
				Template: &v1.VirtualMachineInstanceTemplateSpec{
					Spec: v1.VirtualMachineInstanceSpec{
						Domain: v1.DomainSpec{
							CPU: &v1.CPU{
								Sockets:    8,
								Cores:      1,
								Threads:    1,
								MaxSockets: 16,
							},
							Memory: &v1.Memory{
								Guest:    quantityPtr("16Gi"),
								MaxGuest: quantityPtr("64Gi"),
							},
						},
					},
				},
			},
			Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	newCPU := 10000
	newMemory := 24576
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerCPU:    &newCPU,
		ContainerMemory: &newMemory,
	})
	if err == nil {
		t.Fatal("expected combined cpu and memory live update to be rejected")
	}
	if err.Error() != "运行中虚拟机 CPU 和内存热更新请分两次操作,不支持一次同时修改。" {
		t.Fatalf("expected combined resource rejection message, got %q", err.Error())
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID)
	}
	if serviceDao.service == nil || serviceDao.service.ContainerCPU != 8000 || serviceDao.service.ContainerMemory != 16384 {
		t.Fatalf("expected resources rollback to original values, got %#v", serviceDao.service)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure {
		t.Fatalf("expected failure event status, got %#v", eventDao.statuses)
	}
}

// capability_id: rainbond.vm-live-update.running-cpu-shrink-rejected
func TestServiceVerticalVMLiveUpdateRejectsRunningVMCPUShrink(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    6000,
			ContainerMemory: 12288,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: kubecli.NewMockKubevirtClient(ctrl),
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	newCPU := 4000
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:    "service-vm",
		ContainerCPU: &newCPU,
	})
	if err == nil {
		t.Fatal("expected running vm cpu shrink to be rejected")
	}
	if err.Error() != "虚拟机 CPU 热更新仅支持扩容,不支持缩容,请停机后再修改规格。" {
		t.Fatalf("expected localized rejection message, got %q", err.Error())
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID)
	}
	if serviceDao.service == nil || serviceDao.service.ContainerCPU != 6000 {
		t.Fatalf("expected cpu rollback to original value, got %#v", serviceDao.service)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure {
		t.Fatalf("expected failure event status, got %#v", eventDao.statuses)
	}
}

func TestServiceVerticalWindowsVMLiveUpdateFallsBackToSpecSyncAndRestart(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    4000,
			ContainerMemory: 8192,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: mockClient,
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"},
			Spec: v1.VirtualMachineSpec{
				Template: &v1.VirtualMachineInstanceTemplateSpec{
					Spec: v1.VirtualMachineInstanceSpec{
						Domain: v1.DomainSpec{
							CPU: &v1.CPU{
								Sockets: 1,
								Cores:   4,
								Threads: 1,
							},
							Memory: &v1.Memory{
								Guest:    quantityPtr("8Gi"),
								MaxGuest: quantityPtr("16Gi"),
							},
						},
					},
				},
			},
			Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	mockClient.EXPECT().VirtualMachine("demo-ns").Return(mockVMInterface)
	mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	newCPU := 6000
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:    "service-vm",
		ContainerCPU: &newCPU,
	})
	if err != nil {
		t.Fatalf("expected windows vm cpu change to fall back to sync and restart, got %v", err)
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID)
	}
	if serviceDao.updatedService == nil || serviceDao.updatedService.ContainerCPU != 6000 {
		t.Fatalf("expected updated cpu persisted, got %#v", serviceDao.updatedService)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status, got %#v", eventDao.statuses)
	}
}

// capability_id: rainbond.vm-live-update.running-memory-shrink-rejected
func TestServiceVerticalVMLiveUpdateRejectsRunningVMMemoryShrink(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    6000,
			ContainerMemory: 12288,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: kubecli.NewMockKubevirtClient(ctrl),
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	newMemory := 8192
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerMemory: &newMemory,
	})
	if err == nil {
		t.Fatal("expected running vm memory shrink to be rejected")
	}
	if err.Error() != "虚拟机内存热更新仅支持扩容,不支持缩容,请停机后再修改规格。" {
		t.Fatalf("expected localized rejection message, got %q", err.Error())
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID)
	}
	if serviceDao.service == nil || serviceDao.service.ContainerMemory != 12288 {
		t.Fatalf("expected memory rollback to original value, got %#v", serviceDao.service)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure {
		t.Fatalf("expected failure event status, got %#v", eventDao.statuses)
	}
}

// capability_id: rainbond.vm-live-update.memory-target-below-max-guest
func TestServiceVerticalVMLiveUpdateRejectsRunningVMMemoryAtMaxGuest(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    4000,
			ContainerMemory: 8192,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: kubecli.NewMockKubevirtClient(ctrl),
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "demo-ns"},
			Spec: v1.VirtualMachineSpec{
				Template: &v1.VirtualMachineInstanceTemplateSpec{
					Spec: v1.VirtualMachineInstanceSpec{
						Domain: v1.DomainSpec{
							Memory: &v1.Memory{
								Guest:    quantityPtr("8Gi"),
								MaxGuest: quantityPtr("16Gi"),
							},
						},
					},
				},
			},
			Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	newMemory := 16384
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerMemory: &newMemory,
	})
	if err == nil {
		t.Fatal("expected running vm memory target at maxGuest to be rejected")
	}
	if err.Error() != "vm memory live update target must be lower than maxGuest" {
		t.Fatalf("expected maxGuest headroom rejection message, got %q", err.Error())
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID)
	}
	if serviceDao.service == nil || serviceDao.service.ContainerMemory != 8192 {
		t.Fatalf("expected memory rollback to original value, got %#v", serviceDao.service)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure {
		t.Fatalf("expected failure event status, got %#v", eventDao.statuses)
	}
}

func TestGetVMLiveUpdateCapabilityIgnoresRemovedNetworkFields(t *testing.T) {
	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    6000,
			ContainerMemory: 12288,
		},
	}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   &resourceSyncEventDao{},
	})
	defer db.SetTestManager(nil)

	action := &ServiceAction{
		syncVirtualMachineSpecHook: func(serviceID string) error { return nil },
	}
	action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{"vm_network_mode": "fixed"}, nil
	}
	action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{}, nil
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	capability := action.GetVMLiveUpdateCapability("service-vm")
	if !capability.CPUHotUpdateSupported || !capability.MemoryHotUpdateSupported {
		t.Fatalf("expected removed vm network fields to no longer block hot update, got %#v", capability)
	}
	if capability.HotUpdateReason != "" {
		t.Fatalf("did not expect removed vm network fields to set hot update reason, got %#v", capability)
	}
}

func TestGetVMLiveUpdateCapabilityRejectsFixedPodIPVM(t *testing.T) {
	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    6000,
			ContainerMemory: 12288,
		},
	}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   &resourceSyncEventDao{},
	})
	defer db.SetTestManager(nil)

	action := &ServiceAction{}
	action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{
			"vm_fixed_ip_enabled": "true",
			"vm_fixed_ip":         "10.42.247.130",
		}, nil
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }

	capability := action.GetVMLiveUpdateCapability("service-vm")
	if capability.CPUHotUpdateSupported || capability.MemoryHotUpdateSupported {
		t.Fatalf("expected fixed IP VM hot update to be unsupported, got %#v", capability)
	}
	if !strings.Contains(capability.HotUpdateReason, "固定 IP") || !strings.Contains(capability.HotUpdateReason, "重启") {
		t.Fatalf("expected fixed IP restart reason, got %#v", capability)
	}
}

func TestGetVMLiveUpdateCapabilityRejectsGPUVM(t *testing.T) {
	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    6000,
			ContainerMemory: 12288,
		},
	}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   &resourceSyncEventDao{},
	})
	defer db.SetTestManager(nil)

	action := &ServiceAction{}
	action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{}, nil
	}
	action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{"vm_gpu_enabled": "true"}, nil
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning}}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	capability := action.GetVMLiveUpdateCapability("service-vm")
	if capability.CPUHotUpdateSupported || capability.MemoryHotUpdateSupported {
		t.Fatalf("expected gpu vm to be unsupported, got %#v", capability)
	}
	if capability.HotUpdateReason == "" {
		t.Fatalf("expected gpu reason, got %#v", capability)
	}
}

func TestGetVMLiveUpdateCapabilityRejectsNonSocketCPUHotUpdate(t *testing.T) {
	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    4000,
			ContainerMemory: 8192,
		},
	}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   &resourceSyncEventDao{},
	})
	defer db.SetTestManager(nil)

	action := &ServiceAction{}
	action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{}, nil
	}
	action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{}, nil
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			Spec: v1.VirtualMachineSpec{
				Template: &v1.VirtualMachineInstanceTemplateSpec{
					Spec: v1.VirtualMachineInstanceSpec{
						Domain: v1.DomainSpec{
							CPU: &v1.CPU{
								Sockets: 1,
								Cores:   4,
								Threads: 1,
							},
						},
					},
				},
			},
			Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	capability := action.GetVMLiveUpdateCapability("service-vm")
	if capability.CPUHotUpdateSupported {
		t.Fatalf("expected non-socket vm cpu hot update to be disabled, got %#v", capability)
	}
	if !capability.MemoryHotUpdateSupported {
		t.Fatalf("expected non-socket vm memory hot update to remain enabled, got %#v", capability)
	}
	if capability.HotUpdateReason == "" {
		t.Fatalf("expected non-socket cpu hot update rejection reason, got %#v", capability)
	}
}

// capability_id: rainbond.vm-live-update.capability-requires-installer-media-removal
func TestGetVMLiveUpdateCapabilityRejectsWhenInstallerMediaStillAttached(t *testing.T) {
	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    6000,
			ContainerMemory: 12288,
		},
	}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   &resourceSyncEventDao{},
	})
	defer db.SetTestManager(nil)

	action := &ServiceAction{}
	action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{}, nil
	}
	action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{}, nil
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
			Spec: v1.VirtualMachineSpec{
				Template: &v1.VirtualMachineInstanceTemplateSpec{
					Spec: v1.VirtualMachineInstanceSpec{
						Domain: v1.DomainSpec{
							Devices: v1.Devices{
								Disks: []v1.Disk{
									{
										Name: "vmimage",
										DiskDevice: v1.DiskDevice{
											CDRom: &v1.CDRomTarget{Bus: v1.DiskBusSATA},
										},
									},
								},
							},
						},
					},
				},
			},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	capability := action.GetVMLiveUpdateCapability("service-vm")
	if capability.CPUHotUpdateSupported || capability.MemoryHotUpdateSupported {
		t.Fatalf("expected installer media to block live update capability, got %#v", capability)
	}
	if !strings.Contains(capability.HotUpdateReason, "初始化安装光盘") {
		t.Fatalf("expected installer media reason, got %#v", capability)
	}
}

// capability_id: rainbond.vm-live-update.installer-media-removal-required
func TestServiceVerticalVMLiveUpdateRejectsWhenInstallerMediaStillAttached(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    4000,
			ContainerMemory: 8192,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	syncedServiceID := ""
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubevirtClient: kubecli.NewMockKubevirtClient(ctrl),
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			Status: v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
			Spec: v1.VirtualMachineSpec{
				Template: &v1.VirtualMachineInstanceTemplateSpec{
					Spec: v1.VirtualMachineInstanceSpec{
						Domain: v1.DomainSpec{
							Devices: v1.Devices{
								Disks: []v1.Disk{
									{
										Name: "vmimage",
										DiskDevice: v1.DiskDevice{
											CDRom: &v1.CDRomTarget{Bus: v1.DiskBusSATA},
										},
									},
								},
							},
						},
					},
				},
			},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	newMemory := 10240
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerMemory: &newMemory,
	})
	if err == nil {
		t.Fatal("expected live update to be rejected when installer media is still attached")
	}
	if !strings.Contains(err.Error(), "初始化安装光盘") {
		t.Fatalf("expected installer media rejection, got %q", err.Error())
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected rollback vm spec sync, got %q", syncedServiceID)
	}
	if serviceDao.service == nil || serviceDao.service.ContainerMemory != 8192 {
		t.Fatalf("expected memory rollback to original value, got %#v", serviceDao.service)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusFailure {
		t.Fatalf("expected failure event status, got %#v", eventDao.statuses)
	}
}

// capability_id: rainbond.vm-live-update.migration-target-missing-auto-restart
func TestServiceVerticalVMLiveUpdateRestartsWhenNoMigrationTargetNode(t *testing.T) {
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    4000,
			ContainerMemory: 8192,
			ContainerGPU:    0,
		},
	}
	eventDao := &resourceSyncEventDao{}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   eventDao,
	})
	defer db.SetTestManager(nil)

	kubeClient := kubefake.NewSimpleClientset(
		&corev1.Pod{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "virt-launcher-service-vm",
				Namespace: "default",
				Labels: map[string]string{
					"service_id":  "service-vm",
					"kubevirt.io": "virt-launcher",
				},
			},
			Spec: corev1.PodSpec{
				NodeName: "node-a",
				NodeSelector: map[string]string{
					"kubevirt.io/schedulable":           "true",
					"cpu-vendor.node.kubevirt.io/Intel": "true",
				},
			},
			Status: corev1.PodStatus{Phase: corev1.PodRunning},
		},
		&corev1.Node{
			ObjectMeta: metav1.ObjectMeta{
				Name: "node-a",
				Labels: map[string]string{
					"kubevirt.io/schedulable":           "true",
					"cpu-vendor.node.kubevirt.io/Intel": "true",
				},
			},
		},
		&corev1.Node{
			ObjectMeta: metav1.ObjectMeta{
				Name: "node-b",
				Labels: map[string]string{
					"kubevirt.io/schedulable": "true",
				},
			},
		},
	)

	syncedServiceID := ""
	mockClient := kubecli.NewMockKubevirtClient(ctrl)
	action := &ServiceAction{
		MQClient:       &noopMQClient{},
		kubeClient:     kubeClient,
		kubevirtClient: mockClient,
		syncVirtualMachineSpecHook: func(serviceID string) error {
			syncedServiceID = serviceID
			return nil
		},
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "default"},
			Status:     v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	mockVMInterface := kubecli.NewMockVirtualMachineInterface(ctrl)
	mockClient.EXPECT().VirtualMachine("default").Return(mockVMInterface)
	mockVMInterface.EXPECT().Restart(gomock.Any(), "demo-vm", gomock.Any()).Return(nil)

	newMemory := 10240
	err := action.ServiceVertical(context.Background(), &workermodel.VerticalScalingTaskBody{
		ServiceID:       "service-vm",
		ContainerMemory: &newMemory,
	})
	if err != nil {
		t.Fatalf("expected missing migration target to restart instead of failing live update, got %v", err)
	}
	if syncedServiceID != "service-vm" {
		t.Fatalf("expected vm spec sync before restart, got %q", syncedServiceID)
	}
	if serviceDao.service == nil || serviceDao.service.ContainerMemory != 10240 {
		t.Fatalf("expected updated memory to persist, got %#v", serviceDao.service)
	}
	if len(eventDao.statuses) == 0 || eventDao.statuses[len(eventDao.statuses)-1] != dbmodel.EventStatusSuccess {
		t.Fatalf("expected success event status, got %#v", eventDao.statuses)
	}
}

// capability_id: rainbond.vm-live-update.capability-requires-migration-target
func TestGetVMLiveUpdateCapabilityRejectsWhenNoMigrationTargetNode(t *testing.T) {
	serviceDao := &resourceSyncTenantServiceDao{
		service: &dbmodel.TenantServices{
			ServiceID:       "service-vm",
			ServiceAlias:    "service-vm",
			ExtendMethod:    "vm",
			ContainerCPU:    6000,
			ContainerMemory: 12288,
		},
	}
	db.SetTestManager(resourceSyncTestManager{
		serviceDao: serviceDao,
		eventDao:   &resourceSyncEventDao{},
	})
	defer db.SetTestManager(nil)

	kubeClient := kubefake.NewSimpleClientset(
		&corev1.Pod{
			ObjectMeta: metav1.ObjectMeta{
				Name:      "virt-launcher-service-vm",
				Namespace: "default",
				Labels: map[string]string{
					"service_id":  "service-vm",
					"kubevirt.io": "virt-launcher",
				},
			},
			Spec: corev1.PodSpec{
				NodeName: "node-a",
				NodeSelector: map[string]string{
					"kubevirt.io/schedulable":           "true",
					"cpu-vendor.node.kubevirt.io/Intel": "true",
				},
			},
			Status: corev1.PodStatus{Phase: corev1.PodRunning},
		},
		&corev1.Node{
			ObjectMeta: metav1.ObjectMeta{
				Name: "node-a",
				Labels: map[string]string{
					"kubevirt.io/schedulable":           "true",
					"cpu-vendor.node.kubevirt.io/Intel": "true",
				},
			},
		},
		&corev1.Node{
			ObjectMeta: metav1.ObjectMeta{
				Name: "node-b",
				Labels: map[string]string{
					"kubevirt.io/schedulable": "true",
				},
			},
		},
	)

	action := &ServiceAction{
		kubeClient: kubeClient,
	}
	action.loadVMRuntimeSpecExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{}, nil
	}
	action.loadVMRuntimeDeviceExtensionSetHook = func(componentID string) (map[string]string, error) {
		return map[string]string{}, nil
	}
	action.isVMLiveUpdateClusterConfiguredHook = func(ctx context.Context) bool { return true }
	action.getVirtualMachineByServiceIDHook = func(serviceID string) (*v1.VirtualMachine, error) {
		return &v1.VirtualMachine{
			ObjectMeta: metav1.ObjectMeta{Name: "demo-vm", Namespace: "default"},
			Status:     v1.VirtualMachineStatus{PrintableStatus: v1.VirtualMachineStatusRunning},
		}, nil
	}
	action.getVirtualMachineInstanceByServiceIDHook = func(serviceID string) (*v1.VirtualMachineInstance, error) {
		return &v1.VirtualMachineInstance{Status: v1.VirtualMachineInstanceStatus{
			Phase: v1.Running,
			Conditions: []v1.VirtualMachineInstanceCondition{
				{Type: v1.VirtualMachineInstanceIsMigratable, Status: "True"},
			},
		}}, nil
	}

	capability := action.GetVMLiveUpdateCapability("service-vm")
	if capability.CPUHotUpdateSupported || capability.MemoryHotUpdateSupported {
		t.Fatalf("expected live update capability to be blocked, got %#v", capability)
	}
	if !strings.Contains(capability.HotUpdateReason, "没有可用的迁移目标节点") {
		t.Fatalf("expected migration target reason, got %#v", capability)
	}
}

func quantityPtr(raw string) *resource.Quantity {
	q := resource.MustParse(raw)
	return &q
}