diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index 29c6d3c242bb..29744342eb44 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -13392,6 +13392,11 @@ "description": "Reason is a brief description of why we are in the current hotplug volume phase", "type": "string" }, + "size": { + "description": "Represents the size of the volume", + "type": "integer", + "format": "int64" + }, "target": { "description": "Target is the target name used when adding the volume to the VM, eg: vda", "type": "string" diff --git a/pkg/cloud-init/cloud-init.go b/pkg/cloud-init/cloud-init.go index d93e33c758ee..b7268d8db9ae 100644 --- a/pkg/cloud-init/cloud-init.go +++ b/pkg/cloud-init/cloud-init.go @@ -25,6 +25,7 @@ import ( "fmt" "os" "os/exec" + "path" "path/filepath" "strconv" "strings" @@ -67,6 +68,7 @@ type CloudInitData struct { UserData string NetworkData string DevicesData *[]DeviceData + VolumeName string } type PublicSSHKey struct { @@ -117,6 +119,7 @@ func ReadCloudInitVolumeDataSource(vmi *v1.VirtualMachineInstance, secretSourceD cloudInitData, err = readCloudInitNoCloudSource(volume.CloudInitNoCloud) cloudInitData.NoCloudMetaData = readCloudInitNoCloudMetaData(vmi.Name, hostname, vmi.Namespace) + cloudInitData.VolumeName = volume.Name return cloudInitData, err } if volume.CloudInitConfigDrive != nil { @@ -128,6 +131,7 @@ func ReadCloudInitVolumeDataSource(vmi *v1.VirtualMachineInstance, secretSourceD cloudInitData, err = readCloudInitConfigDriveSource(volume.CloudInitConfigDrive) cloudInitData.ConfigDriveMetaData = readCloudInitConfigDriveMetaData(string(vmi.UID), vmi.Name, hostname, vmi.Namespace, keys) + cloudInitData.VolumeName = volume.Name return cloudInitData, err } } @@ -465,6 +469,59 @@ func removeLocalData(domain string, namespace string) error { return err } +func GenerateEmptyIso(vmiName string, namespace string, data *CloudInitData, size int64) error { + precond.MustNotBeEmpty(vmiName) + precond.MustNotBeNil(data) + + var err error + var isoStaging, iso string + + switch data.DataSource { + case DataSourceNoCloud, DataSourceConfigDrive: + iso = GetIsoFilePath(data.DataSource, vmiName, namespace) + default: + return fmt.Errorf("invalid cloud-init data source: '%v'", data.DataSource) + } + isoStaging = fmt.Sprintf("%s.staging", iso) + + err = diskutils.RemoveFilesIfExist(isoStaging) + if err != nil { + return err + } + + err = util.MkdirAllWithNosec(path.Dir(isoStaging)) + if err != nil { + log.Log.V(2).Reason(err).Errorf("unable to create cloud-init base path %s", path.Dir(isoStaging)) + return err + } + + f, err := os.Create(isoStaging) + if err != nil { + return fmt.Errorf("failed to create empty iso: '%s'", isoStaging) + } + + err = util.WriteBytes(f, 0, size) + if err != nil { + return err + } + util.CloseIOAndCheckErr(f, &err) + if err != nil { + return err + } + + if err := diskutils.DefaultOwnershipManager.SetFileOwnership(isoStaging); err != nil { + return err + } + err = os.Rename(isoStaging, iso) + if err != nil { + log.Log.Reason(err).Errorf("Cloud-init failed to rename file %s to %s", isoStaging, iso) + return err + } + + log.Log.V(2).Infof("generated empty iso file %s", iso) + return nil +} + func GenerateLocalData(vmiName string, namespace string, data *CloudInitData) error { precond.MustNotBeEmpty(vmiName) precond.MustNotBeNil(data) diff --git a/pkg/cloud-init/cloud-init_test.go b/pkg/cloud-init/cloud-init_test.go index acebb24e1040..6eb8dbb1e2e7 100644 --- a/pkg/cloud-init/cloud-init_test.go +++ b/pkg/cloud-init/cloud-init_test.go @@ -502,6 +502,7 @@ var _ = Describe("CloudInit", func() { }) }) }) + Describe("GenerateLocalData", func() { It("should cleanly run twice", func() { namespace := "fake-namespace" @@ -517,7 +518,6 @@ var _ = Describe("CloudInit", func() { err = GenerateLocalData(domain, namespace, cloudInitData) Expect(err).NotTo(HaveOccurred()) }) - }) Describe("PrepareLocalPath", func() { diff --git a/pkg/config/BUILD.bazel b/pkg/config/BUILD.bazel index 10ffb5833fe6..c3ad8d230830 100644 --- a/pkg/config/BUILD.bazel +++ b/pkg/config/BUILD.bazel @@ -14,6 +14,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/ephemeral-disk-utils:go_default_library", + "//pkg/util:go_default_library", "//staging/src/kubevirt.io/client-go/api/v1:go_default_library", ], ) diff --git a/pkg/config/config-map.go b/pkg/config/config-map.go index d8f3246a8f0f..42a42af10a59 100644 --- a/pkg/config/config-map.go +++ b/pkg/config/config-map.go @@ -37,7 +37,7 @@ func GetConfigMapDiskPath(volumeName string) string { } // CreateConfigMapDisks creates ConfigMap iso disks which are attached to vmis -func CreateConfigMapDisks(vmi *v1.VirtualMachineInstance) error { +func CreateConfigMapDisks(vmi *v1.VirtualMachineInstance, emptyIso bool) error { for _, volume := range vmi.Spec.Volumes { if volume.ConfigMap != nil { var filesPath []string @@ -47,7 +47,11 @@ func CreateConfigMapDisks(vmi *v1.VirtualMachineInstance) error { } disk := GetConfigMapDiskPath(volume.Name) - if err := createIsoConfigImage(disk, volume.ConfigMap.VolumeLabel, filesPath); err != nil { + vmiIsoSize, err := findIsoSize(vmi, &volume, emptyIso) + if err != nil { + return err + } + if err := createIsoConfigImage(disk, volume.ConfigMap.VolumeLabel, filesPath, vmiIsoSize); err != nil { return err } diff --git a/pkg/config/config-map_test.go b/pkg/config/config-map_test.go index ca248c2f3c63..7365565a5c67 100644 --- a/pkg/config/config-map_test.go +++ b/pkg/config/config-map_test.go @@ -64,7 +64,7 @@ var _ = Describe("ConfigMap", func() { }, }) - err := CreateConfigMapDisks(vmi) + err := CreateConfigMapDisks(vmi, false) Expect(err).NotTo(HaveOccurred()) _, err = os.Stat(filepath.Join(ConfigMapDisksDir, "configmap-volume.iso")) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/config/config.go b/pkg/config/config.go index d0c7e06cfeb0..fb9a2d4dc4f6 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -20,16 +20,22 @@ package config import ( + "fmt" "os" "os/exec" "path/filepath" + + "kubevirt.io/kubevirt/pkg/util" + + v1 "kubevirt.io/client-go/api/v1" ) type ( // Type represents allowed config types like ConfigMap or Secret Type string - isoCreationFunc func(output string, volID string, files []string) error + isoCreationFunc func(output string, volID string, files []string) error + emptyIsoCreationFunc func(output string, size int64) error ) const ( @@ -78,7 +84,8 @@ var ( // ServiceAccountDiskName represents the name of the ServiceAccount iso image ServiceAccountDiskName = "service-account.iso" - createISOImage = defaultCreateIsoImage + createISOImage = defaultCreateIsoImage + createEmptyISOImage = defaultCreateEmptyIsoImage ) // The unit test suite uses this function @@ -86,6 +93,11 @@ func setIsoCreationFunction(isoFunc isoCreationFunc) { createISOImage = isoFunc } +// The unit test suite uses this function +func setEmptyIsoCreationFunction(emptyIsoFunc emptyIsoCreationFunc) { + createEmptyISOImage = emptyIsoFunc +} + func getFilesLayout(dirPath string) ([]string, error) { var filesPath []string files, err := os.ReadDir(dirPath) @@ -129,10 +141,40 @@ func defaultCreateIsoImage(output string, volID string, files []string) error { return nil } -func createIsoConfigImage(output string, volID string, files []string) error { - err := createISOImage(output, volID, files) +func defaultCreateEmptyIsoImage(output string, size int64) error { + f, err := os.Create(output) + if err != nil { + return fmt.Errorf("failed to create empty iso: '%s'", output) + } + err = util.WriteBytes(f, 0, size) + if err != nil { + return err + } + util.CloseIOAndCheckErr(f, &err) + return err +} + +func createIsoConfigImage(output string, volID string, files []string, size int64) error { + var err error + if size == 0 { + err = createISOImage(output, volID, files) + } else { + err = createEmptyISOImage(output, size) + } if err != nil { return err } return nil } + +func findIsoSize(vmi *v1.VirtualMachineInstance, volume *v1.Volume, emptyIso bool) (int64, error) { + if emptyIso { + for _, vs := range vmi.Status.VolumeStatus { + if vs.Name == volume.Name { + return vs.Size, nil + } + } + return 0, fmt.Errorf("failed to find the status of volume %s", volume.Name) + } + return 0, nil +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 64b335c68268..67f8f52e26fb 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -73,7 +73,7 @@ var _ = Describe("Creating config images", func() { It("Should create an iso image", func() { imgPath := filepath.Join(tempISODir, "volume1.iso") - err := createIsoConfigImage(imgPath, "", expectedLayout) + err := createIsoConfigImage(imgPath, "", expectedLayout, 0) Expect(err).NotTo(HaveOccurred()) _, err = os.Stat(imgPath) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/config/downwardapi.go b/pkg/config/downwardapi.go index c86338c0fd1c..4f0dca2df00b 100644 --- a/pkg/config/downwardapi.go +++ b/pkg/config/downwardapi.go @@ -37,7 +37,7 @@ func GetDownwardAPIDiskPath(volumeName string) string { } // CreateDownwardAPIDisks creates DownwardAPI iso disks which are attached to vmis -func CreateDownwardAPIDisks(vmi *v1.VirtualMachineInstance) error { +func CreateDownwardAPIDisks(vmi *v1.VirtualMachineInstance, emptyIso bool) error { for _, volume := range vmi.Spec.Volumes { if volume.DownwardAPI != nil { @@ -48,7 +48,11 @@ func CreateDownwardAPIDisks(vmi *v1.VirtualMachineInstance) error { } disk := GetDownwardAPIDiskPath(volume.Name) - if err := createIsoConfigImage(disk, volume.DownwardAPI.VolumeLabel, filesPath); err != nil { + vmiIsoSize, err := findIsoSize(vmi, &volume, emptyIso) + if err != nil { + return err + } + if err := createIsoConfigImage(disk, volume.DownwardAPI.VolumeLabel, filesPath, vmiIsoSize); err != nil { return err } diff --git a/pkg/config/downwardapi_test.go b/pkg/config/downwardapi_test.go index d9cb11345976..0934d572585a 100644 --- a/pkg/config/downwardapi_test.go +++ b/pkg/config/downwardapi_test.go @@ -70,7 +70,7 @@ var _ = Describe("DownwardAPI", func() { }, }) - err := CreateDownwardAPIDisks(vmi) + err := CreateDownwardAPIDisks(vmi, false) Expect(err).NotTo(HaveOccurred()) _, err = os.Stat(filepath.Join(DownwardAPIDisksDir, "downwardapi-volume.iso")) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/config/secret.go b/pkg/config/secret.go index f1ccce8e39f5..77c1b866fc66 100644 --- a/pkg/config/secret.go +++ b/pkg/config/secret.go @@ -37,7 +37,7 @@ func GetSecretDiskPath(volumeName string) string { } // CreateSecretDisks creates Secret iso disks which are attached to vmis -func CreateSecretDisks(vmi *v1.VirtualMachineInstance) error { +func CreateSecretDisks(vmi *v1.VirtualMachineInstance, emptyIso bool) error { for _, volume := range vmi.Spec.Volumes { if volume.Secret != nil { @@ -48,7 +48,11 @@ func CreateSecretDisks(vmi *v1.VirtualMachineInstance) error { } disk := GetSecretDiskPath(volume.Name) - if err := createIsoConfigImage(disk, volume.Secret.VolumeLabel, filesPath); err != nil { + vmiIsoSize, err := findIsoSize(vmi, &volume, emptyIso) + if err != nil { + return err + } + if err := createIsoConfigImage(disk, volume.Secret.VolumeLabel, filesPath, vmiIsoSize); err != nil { return err } diff --git a/pkg/config/secret_test.go b/pkg/config/secret_test.go index 14eac7f73a15..5a8462d65596 100644 --- a/pkg/config/secret_test.go +++ b/pkg/config/secret_test.go @@ -61,7 +61,7 @@ var _ = Describe("Secret", func() { }, }) - err := CreateSecretDisks(vmi) + err := CreateSecretDisks(vmi, false) Expect(err).NotTo(HaveOccurred()) _, err = os.Stat(filepath.Join(SecretDisksDir, "secret-volume.iso")) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/config/service-account.go b/pkg/config/service-account.go index 84bf549f9dce..f33fc05c447a 100644 --- a/pkg/config/service-account.go +++ b/pkg/config/service-account.go @@ -32,7 +32,7 @@ func GetServiceAccountDiskPath() string { } // CreateServiceAccountDisk creates the ServiceAccount iso disk which is attached to vmis -func CreateServiceAccountDisk(vmi *v1.VirtualMachineInstance) error { +func CreateServiceAccountDisk(vmi *v1.VirtualMachineInstance, emptyIso bool) error { for _, volume := range vmi.Spec.Volumes { if volume.ServiceAccount != nil { var filesPath []string @@ -42,7 +42,11 @@ func CreateServiceAccountDisk(vmi *v1.VirtualMachineInstance) error { } disk := GetServiceAccountDiskPath() - if err := createIsoConfigImage(disk, "", filesPath); err != nil { + vmiIsoSize, err := findIsoSize(vmi, &volume, emptyIso) + if err != nil { + return err + } + if err := createIsoConfigImage(disk, "", filesPath, vmiIsoSize); err != nil { return err } diff --git a/pkg/config/service-account_test.go b/pkg/config/service-account_test.go index 73e58e8fde08..d1f5fbb777a0 100644 --- a/pkg/config/service-account_test.go +++ b/pkg/config/service-account_test.go @@ -61,7 +61,7 @@ var _ = Describe("ServiceAccount", func() { }, }) - err := CreateServiceAccountDisk(vmi) + err := CreateServiceAccountDisk(vmi, false) Expect(err).NotTo(HaveOccurred()) _, err = os.Stat(filepath.Join(ServiceAccountDiskDir, ServiceAccountDiskName)) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/config/sysprep.go b/pkg/config/sysprep.go index 98fbd2c0737d..ab4ac863228f 100644 --- a/pkg/config/sysprep.go +++ b/pkg/config/sysprep.go @@ -64,12 +64,16 @@ func validateAutounattendPresence(dirPath string) error { } // CreateSysprepDisks creates Sysprep iso disks which are attached to vmis from either ConfigMap or Secret as a source -func CreateSysprepDisks(vmi *v1.VirtualMachineInstance) error { +func CreateSysprepDisks(vmi *v1.VirtualMachineInstance, emptyIso bool) error { for _, volume := range vmi.Spec.Volumes { if !shouldCreateSysprepDisk(volume.Sysprep) { continue } - if err := createSysprepDisk(volume.Name); err != nil { + vmiIsoSize, err := findIsoSize(vmi, &volume, emptyIso) + if err != nil { + return err + } + if err := createSysprepDisk(volume.Name, vmiIsoSize); err != nil { return err } } @@ -80,7 +84,7 @@ func shouldCreateSysprepDisk(volumeSysprep *v1.SysprepSource) bool { return volumeSysprep != nil && sysprepVolumeHasContents(volumeSysprep) } -func createSysprepDisk(volumeName string) error { +func createSysprepDisk(volumeName string, size int64) error { sysprepSourcePath := GetSysprepSourcePath(volumeName) if err := validateAutounattendPresence(sysprepSourcePath); err != nil { return err @@ -90,12 +94,12 @@ func createSysprepDisk(volumeName string) error { return err } - return createIsoImageAndSetFileOwnership(volumeName, filesPath) + return createIsoImageAndSetFileOwnership(volumeName, filesPath, size) } -func createIsoImageAndSetFileOwnership(volumeName string, filesPath []string) error { +func createIsoImageAndSetFileOwnership(volumeName string, filesPath []string, size int64) error { disk := GetSysprepDiskPath(volumeName) - if err := createIsoConfigImage(disk, sysprepVolumeLabel, filesPath); err != nil { + if err := createIsoConfigImage(disk, sysprepVolumeLabel, filesPath, size); err != nil { return err } if err := ephemeraldiskutils.DefaultOwnershipManager.SetFileOwnership(disk); err != nil { diff --git a/pkg/config/sysprep_test.go b/pkg/config/sysprep_test.go index 5093e3c0577e..b608480aa0d1 100644 --- a/pkg/config/sysprep_test.go +++ b/pkg/config/sysprep_test.go @@ -87,7 +87,7 @@ var _ = Describe("SysprepConfigMap", func() { }) It("Should fail on creating config map iso disk", func() { - err := CreateSysprepDisks(vmiConfigMap) + err := CreateSysprepDisks(vmiConfigMap, false) Expect(err).To(HaveOccurred()) }) }) @@ -99,14 +99,14 @@ var _ = Describe("SysprepConfigMap", func() { }) It("Should create a new config map iso disk", func() { - err := CreateSysprepDisks(vmiConfigMap) + err := CreateSysprepDisks(vmiConfigMap, false) Expect(err).NotTo(HaveOccurred()) _, err = os.Stat(filepath.Join(SysprepDisksDir, "sysprep-volume.iso")) Expect(err).NotTo(HaveOccurred()) }) It("Should create a new secret iso disk", func() { - err := CreateSysprepDisks(vmiSecret) + err := CreateSysprepDisks(vmiSecret, false) Expect(err).NotTo(HaveOccurred()) _, err = os.Stat(filepath.Join(SysprepDisksDir, "sysprep-volume.iso")) Expect(err).NotTo(HaveOccurred()) diff --git a/pkg/util/os_helper.go b/pkg/util/os_helper.go index 98f7cc5bb84e..4a959daa4054 100644 --- a/pkg/util/os_helper.go +++ b/pkg/util/os_helper.go @@ -20,6 +20,7 @@ package util import ( + "fmt" "io" "io/ioutil" "os" @@ -60,3 +61,32 @@ func WriteFileWithNosec(pathName string, data []byte) error { // #nosec G306, Expect WriteFile permissions to be 0600 or less return ioutil.WriteFile(pathName, data, 0644) } + +func WriteBytes(f *os.File, c byte, n int64) error { + var err error + var i, total int64 + buf := make([]byte, 1<<12) + + for i = 0; i < 1<<12; i++ { + buf[i] = c + } + + for i = 0; i < n>>12; i++ { + x, err := f.Write(buf) + total += int64(x) + if err != nil { + return err + } + } + + x, err := f.Write(buf[:n&(1<<12-1)]) + total += int64(x) + if err != nil { + return err + } + if total != n { + return fmt.Errorf("wrote %d bytes instead of %d", total, n) + } + + return nil +} diff --git a/pkg/virt-handler/BUILD.bazel b/pkg/virt-handler/BUILD.bazel index ff8dfb58cd90..7df475f420bd 100644 --- a/pkg/virt-handler/BUILD.bazel +++ b/pkg/virt-handler/BUILD.bazel @@ -11,6 +11,7 @@ go_library( importpath = "kubevirt.io/kubevirt/pkg/virt-handler", visibility = ["//visibility:public"], deps = [ + "//pkg/config:go_default_library", "//pkg/container-disk:go_default_library", "//pkg/controller:go_default_library", "//pkg/ephemeral-disk-utils:go_default_library", diff --git a/pkg/virt-handler/isolation/validation.go b/pkg/virt-handler/isolation/validation.go index a5d507ddd601..89a5f4aa11ff 100644 --- a/pkg/virt-handler/isolation/validation.go +++ b/pkg/virt-handler/isolation/validation.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os/exec" + "strconv" v1 "kubevirt.io/client-go/api/v1" virt_chroot "kubevirt.io/kubevirt/pkg/virt-handler/virt-chroot" @@ -39,3 +40,17 @@ func GetImageInfo(imagePath string, context IsolationResult, config *v1.DiskVeri } return info, err } + +func GetFileSize(imagePath string, context IsolationResult, config *v1.DiskVerification) (int, error) { + memoryLimit := fmt.Sprintf("%d", config.MemoryLimit.Value()) + + // #nosec g204 no risk to use MountNamespace() argument as it returns a fixed string of "/proc//ns/mnt" + out, err := virt_chroot.ExecChroot( + "--user", "qemu", "--memory", memoryLimit, "--cpu", "10", "--mount", context.MountNamespace(), "exec", "--", + "/usr/bin/stat", "--printf=%s", imagePath, + ).Output() + if err == nil { + return strconv.Atoi(string(out)) + } + return -1, err +} diff --git a/pkg/virt-handler/vm.go b/pkg/virt-handler/vm.go index cc12f4d5f88b..3470ac8e3f34 100644 --- a/pkg/virt-handler/vm.go +++ b/pkg/virt-handler/vm.go @@ -34,6 +34,8 @@ import ( "strings" "time" + "kubevirt.io/kubevirt/pkg/config" + "github.com/opencontainers/runc/libcontainer/cgroups" nodelabellerapi "kubevirt.io/kubevirt/pkg/virt-handler/node-labeller/api" @@ -1156,6 +1158,73 @@ func (d *VirtualMachineController) updateFSFreezeStatus(vmi *v1.VirtualMachineIn } +func IsoGuestVolumePath(vmi *v1.VirtualMachineInstance, volume *v1.Volume) (string, bool) { + var volPath string + + basepath := "/var/run" + if volume.CloudInitNoCloud != nil { + volPath = path.Join(basepath, "kubevirt-ephemeral-disks", "cloud-init-data", vmi.Namespace, vmi.Name, "noCloud.iso") + } else if volume.CloudInitConfigDrive != nil { + volPath = path.Join(basepath, "kubevirt-ephemeral-disks", "cloud-init-data", vmi.Namespace, vmi.Name, "configdrive.iso") + } else if volume.ConfigMap != nil { + volPath = path.Join(basepath, "kubevirt-private", path.Base(config.ConfigMapDisksDir), volume.Name+".iso") + } else if volume.DownwardAPI != nil { + volPath = path.Join(basepath, "kubevirt-private", path.Base(config.DownwardAPIDisksDir), volume.Name+".iso") + } else if volume.Secret != nil { + volPath = path.Join(basepath, "kubevirt-private", path.Base(config.SecretDisksDir), volume.Name+".iso") + } else if volume.ServiceAccount != nil { + volPath = path.Join(basepath, "kubevirt-private", path.Base(config.ServiceAccountDiskDir), config.ServiceAccountDiskName) + } else if volume.Sysprep != nil { + volPath = path.Join(basepath, "kubevirt-private", path.Base(config.SysprepDisksDir), volume.Name+".iso") + } else { + return "", false + } + + return volPath, true +} + +func (d *VirtualMachineController) updateIsoSizeStatus(vmi *v1.VirtualMachineInstance) { + var podUID string + if vmi.Status.Phase != v1.Running { + return + } + + for k, v := range vmi.Status.ActivePods { + if v == vmi.Status.NodeName { + podUID = string(k) + break + } + } + if podUID == "" { + log.DefaultLogger().V(2).Warningf("failed to find pod UID for VMI %s", vmi.Name) + return + } + + for _, volume := range vmi.Spec.Volumes { + volPath, found := IsoGuestVolumePath(vmi, &volume) + if !found { + continue + } + res, err := d.podIsolationDetector.Detect(vmi) + if err != nil { + log.DefaultLogger().V(2).Warningf("failed to detect VMI %s", vmi.Name) + continue + } + size, err := isolation.GetFileSize(volPath, res, d.clusterConfig.GetDiskVerification()) + if err != nil { + log.DefaultLogger().V(2).Warningf("failed to determine file size for volume %s", volPath) + continue + } + + for i, _ := range vmi.Status.VolumeStatus { + if vmi.Status.VolumeStatus[i].Name == volume.Name { + vmi.Status.VolumeStatus[i].Size = int64(size) + continue + } + } + } +} + func (d *VirtualMachineController) updateVMIStatus(origVMI *v1.VirtualMachineInstance, domain *api.Domain, syncError error) (err error) { condManager := controller.NewVirtualMachineInstanceConditionManager() @@ -1174,6 +1243,7 @@ func (d *VirtualMachineController) updateVMIStatus(origVMI *v1.VirtualMachineIns oldStatus := *vmi.Status.DeepCopy() // Update VMI status fields based on what is reported on the domain + d.updateIsoSizeStatus(vmi) d.setMigrationProgressStatus(vmi, domain) d.updateGuestInfoFromDomain(vmi, domain) d.updateVolumeStatusesFromDomain(vmi, domain) diff --git a/pkg/virt-launcher/virtwrap/live-migration-target.go b/pkg/virt-launcher/virtwrap/live-migration-target.go index a4f974a089e7..221b83ededec 100644 --- a/pkg/virt-launcher/virtwrap/live-migration-target.go +++ b/pkg/virt-launcher/virtwrap/live-migration-target.go @@ -69,10 +69,6 @@ func (l *LibvirtDomainManager) prepareMigrationTarget( ) error { logger := log.Log.Object(vmi) - if shouldBlockMigrationTargetPreparation(vmi) { - return fmt.Errorf("Blocking preparation of migration target in order to satisfy a functional test condition") - } - c, err := l.generateConverterContext(vmi, allowEmulation, options, true) if err != nil { return fmt.Errorf("Failed to generate libvirt domain from VMI spec: %v", err) @@ -83,12 +79,12 @@ func (l *LibvirtDomainManager) prepareMigrationTarget( return fmt.Errorf("conversion failed: %v", err) } - dom, err := l.preStartHook(vmi, domain) + dom, err := l.preStartHook(vmi, domain, true) if err != nil { return fmt.Errorf("pre-start pod-setup failed: %v", err) } - err = l.generateCloudInitISO(vmi, nil) + err = l.generateCloudInitEmptyISO(vmi, nil) if err != nil { return err } @@ -101,6 +97,10 @@ func (l *LibvirtDomainManager) prepareMigrationTarget( return fmt.Errorf("executing custom preStart hooks failed: %v", err) } + if shouldBlockMigrationTargetPreparation(vmi) { + return fmt.Errorf("Blocking preparation of migration target in order to satisfy a functional test condition") + } + if canSourceMigrateOverUnixURI(vmi) { // Prepare the directory for migration sockets migrationSocketsPath := filepath.Join(l.virtShareDir, "migrationproxy") diff --git a/pkg/virt-launcher/virtwrap/manager.go b/pkg/virt-launcher/virtwrap/manager.go index 47804155437c..bf084da281d5 100644 --- a/pkg/virt-launcher/virtwrap/manager.go +++ b/pkg/virt-launcher/virtwrap/manager.go @@ -388,7 +388,7 @@ func (l *LibvirtDomainManager) MigrateVMI(vmi *v1.VirtualMachineInstance, option return l.startMigration(vmi, options) } -func (l *LibvirtDomainManager) generateCloudInitISO(vmi *v1.VirtualMachineInstance, domPtr *cli.VirDomain) error { +func (l *LibvirtDomainManager) generateSomeCloudInitISO(vmi *v1.VirtualMachineInstance, domPtr *cli.VirDomain, size int64) error { var devicesMetadata []cloudinit.DeviceData // this is the point where we need to build the devices metadata if it was requested. // This metadata maps the user provided tag to the hypervisor assigned device address. @@ -409,7 +409,12 @@ func (l *LibvirtDomainManager) generateCloudInitISO(vmi *v1.VirtualMachineInstan if devicesMetadata != nil { cloudInitDataStore.DevicesData = &devicesMetadata } - err := cloudinit.GenerateLocalData(vmi.Name, vmi.Namespace, cloudInitDataStore) + var err error + if size != 0 { + err = cloudinit.GenerateEmptyIso(vmi.Name, vmi.Namespace, cloudInitDataStore, size) + } else { + err = cloudinit.GenerateLocalData(vmi.Name, vmi.Namespace, cloudInitDataStore) + } if err != nil { return fmt.Errorf("generating local cloud-init data failed: %v", err) } @@ -417,6 +422,22 @@ func (l *LibvirtDomainManager) generateCloudInitISO(vmi *v1.VirtualMachineInstan return nil } +func (l *LibvirtDomainManager) generateCloudInitISO(vmi *v1.VirtualMachineInstance, domPtr *cli.VirDomain) error { + return l.generateSomeCloudInitISO(vmi, domPtr, 0) +} + +func (l *LibvirtDomainManager) generateCloudInitEmptyISO(vmi *v1.VirtualMachineInstance, domPtr *cli.VirDomain) error { + if l.cloudInitDataStore == nil { + return nil + } + for _, vs := range vmi.Status.VolumeStatus { + if vs.Name == l.cloudInitDataStore.VolumeName { + return l.generateSomeCloudInitISO(vmi, domPtr, vs.Size) + } + } + return fmt.Errorf("failed to find the status of volume %s", l.cloudInitDataStore.VolumeName) +} + // All local environment setup that needs to occur before VirtualMachineInstance starts // can be done in this function. This includes things like... // @@ -427,7 +448,7 @@ func (l *LibvirtDomainManager) generateCloudInitISO(vmi *v1.VirtualMachineInstan // // The Domain.Spec can be alterned in this function and any changes // made to the domain will get set in libvirt after this function exits. -func (l *LibvirtDomainManager) preStartHook(vmi *v1.VirtualMachineInstance, domain *api.Domain) (*api.Domain, error) { +func (l *LibvirtDomainManager) preStartHook(vmi *v1.VirtualMachineInstance, domain *api.Domain, generateEmptyIsos bool) (*api.Domain, error) { logger := log.Log.Object(vmi) logger.Info("Executing PreStartHook on VMI pod environment") @@ -499,25 +520,25 @@ func (l *LibvirtDomainManager) preStartHook(vmi *v1.VirtualMachineInstance, doma return domain, fmt.Errorf("creating empty disks failed: %v", err) } // create ConfigMap disks if they exists - if err := config.CreateConfigMapDisks(vmi); err != nil { + if err := config.CreateConfigMapDisks(vmi, generateEmptyIsos); err != nil { return domain, fmt.Errorf("creating config map disks failed: %v", err) } // create Secret disks if they exists - if err := config.CreateSecretDisks(vmi); err != nil { + if err := config.CreateSecretDisks(vmi, generateEmptyIsos); err != nil { return domain, fmt.Errorf("creating secret disks failed: %v", err) } // create Sysprep disks if they exists - if err := config.CreateSysprepDisks(vmi); err != nil { + if err := config.CreateSysprepDisks(vmi, generateEmptyIsos); err != nil { return domain, fmt.Errorf("creating sysprep disks failed: %v", err) } // create DownwardAPI disks if they exists - if err := config.CreateDownwardAPIDisks(vmi); err != nil { + if err := config.CreateDownwardAPIDisks(vmi, generateEmptyIsos); err != nil { return domain, fmt.Errorf("creating DownwardAPI disks failed: %v", err) } // create ServiceAccount disk if exists - if err := config.CreateServiceAccountDisk(vmi); err != nil { + if err := config.CreateServiceAccountDisk(vmi, generateEmptyIsos); err != nil { return domain, fmt.Errorf("creating service account disk failed: %v", err) } // create downwardMetric disk if exists @@ -778,7 +799,7 @@ func (l *LibvirtDomainManager) SyncVMI(vmi *v1.VirtualMachineInstance, allowEmul if err != nil { // We need the domain but it does not exist, so create it if domainerrors.IsNotFound(err) { - domain, err = l.preStartHook(vmi, domain) + domain, err = l.preStartHook(vmi, domain, false) if err != nil { logger.Reason(err).Error("pre start setup for VirtualMachineInstance failed.") return nil, err diff --git a/pkg/virt-launcher/virtwrap/manager_test.go b/pkg/virt-launcher/virtwrap/manager_test.go index e9b38d95fd67..0d5b5505d179 100644 --- a/pkg/virt-launcher/virtwrap/manager_test.go +++ b/pkg/virt-launcher/virtwrap/manager_test.go @@ -1843,6 +1843,80 @@ var _ = Describe("Manager", func() { Expect(virtualMachineInstanceGuestAgentInfo).ToNot(BeNil()) }) + It("executes generateCloudInitEmptyISO and succeeds", func() { + agentStore := agentpoller.NewAsyncAgentStore() + agentStore.Store(agentpoller.GET_FILESYSTEM, []api.Filesystem{ + { + Name: "test", + Mountpoint: "/mnt/whatever", + Type: "fs", + UsedBytes: 0, + TotalBytes: 0, + }, + }) + + manager, _ := NewLibvirtDomainManager(mockConn, testVirtShareDir, &agentStore, "/usr/share/OVMF", ephemeralDiskCreatorMock) + + // we need the non-typecast object to make the function we want to test available + libvirtmanager := manager.(*LibvirtDomainManager) + + vmi := newVMI(testNamespace, testVmName) + vmi.Status.VolumeStatus = make([]v1.VolumeStatus, 1) + vmi.Status.VolumeStatus[0] = v1.VolumeStatus{ + Name: "test1", + Size: 42, + } + + userData := "fake\nuser\ndata\n" + networkData := "FakeNetwork" + addCloudInitDisk(vmi, userData, networkData) + libvirtmanager.cloudInitDataStore = &cloudinit.CloudInitData{ + DataSource: cloudinit.DataSourceNoCloud, + VolumeName: "test1", + } + + err := libvirtmanager.generateCloudInitEmptyISO(vmi, nil) + Expect(err).ToNot(HaveOccurred()) + + isoPath := cloudinit.GetIsoFilePath(libvirtmanager.cloudInitDataStore.DataSource, vmi.Name, vmi.Namespace) + stats, err := os.Stat(isoPath) + Expect(err).ToNot(HaveOccurred()) + Expect(stats.Size()).To(Equal(int64(42))) + }) + + It("executes generateCloudInitEmptyISO and fails", func() { + agentStore := agentpoller.NewAsyncAgentStore() + agentStore.Store(agentpoller.GET_FILESYSTEM, []api.Filesystem{ + { + Name: "test", + Mountpoint: "/mnt/whatever", + Type: "fs", + UsedBytes: 0, + TotalBytes: 0, + }, + }) + + manager, _ := NewLibvirtDomainManager(mockConn, testVirtShareDir, &agentStore, "/usr/share/OVMF", ephemeralDiskCreatorMock) + + // we need the non-typecast object to make the function we want to test available + libvirtmanager := manager.(*LibvirtDomainManager) + + vmi := newVMI(testNamespace, testVmName) + vmi.Status.VolumeStatus = make([]v1.VolumeStatus, 1) + + userData := "fake\nuser\ndata\n" + networkData := "FakeNetwork" + addCloudInitDisk(vmi, userData, networkData) + libvirtmanager.cloudInitDataStore = &cloudinit.CloudInitData{ + DataSource: cloudinit.DataSourceNoCloud, + VolumeName: "test1", + } + + err := libvirtmanager.generateCloudInitEmptyISO(vmi, nil) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("failed to find the status of volume test1")) + }) + // TODO: test error reporting on non successful VirtualMachineInstance syncs and kill attempts AfterEach(func() { diff --git a/pkg/virt-operator/resource/generate/components/validations_generated.go b/pkg/virt-operator/resource/generate/components/validations_generated.go index ec6315b46bea..d76121a6a8d2 100644 --- a/pkg/virt-operator/resource/generate/components/validations_generated.go +++ b/pkg/virt-operator/resource/generate/components/validations_generated.go @@ -8251,6 +8251,10 @@ var CRDsValidation map[string]string = map[string]string{ description: Reason is a brief description of why we are in the current hotplug volume phase type: string + size: + description: Represents the size of the volume + format: int64 + type: integer target: description: 'Target is the target name used when adding the volume to the VM, eg: vda' diff --git a/staging/src/kubevirt.io/client-go/api/v1/openapi_generated.go b/staging/src/kubevirt.io/client-go/api/v1/openapi_generated.go index 4be918771e78..3bbb5183fae9 100644 --- a/staging/src/kubevirt.io/client-go/api/v1/openapi_generated.go +++ b/staging/src/kubevirt.io/client-go/api/v1/openapi_generated.go @@ -25900,6 +25900,13 @@ func schema_kubevirtio_client_go_api_v1_VolumeStatus(ref common.ReferenceCallbac Ref: ref("kubevirt.io/client-go/api/v1.HotplugVolumeStatus"), }, }, + "size": { + SchemaProps: spec.SchemaProps{ + Description: "Represents the size of the volume", + Type: []string{"integer"}, + Format: "int64", + }, + }, }, Required: []string{"name", "target"}, }, diff --git a/staging/src/kubevirt.io/client-go/api/v1/types.go b/staging/src/kubevirt.io/client-go/api/v1/types.go index 5d1a1451ffed..9fc49b18685d 100644 --- a/staging/src/kubevirt.io/client-go/api/v1/types.go +++ b/staging/src/kubevirt.io/client-go/api/v1/types.go @@ -287,6 +287,8 @@ type VolumeStatus struct { PersistentVolumeClaimInfo *PersistentVolumeClaimInfo `json:"persistentVolumeClaimInfo,omitempty"` // If the volume is hotplug, this will contain the hotplug status. HotplugVolume *HotplugVolumeStatus `json:"hotplugVolume,omitempty"` + // Represents the size of the volume + Size int64 `json:"size,omitempty"` } // HotplugVolumeStatus represents the hotplug status of the volume diff --git a/staging/src/kubevirt.io/client-go/api/v1/types_swagger_generated.go b/staging/src/kubevirt.io/client-go/api/v1/types_swagger_generated.go index fc9c2a68cc17..c164798b1abc 100644 --- a/staging/src/kubevirt.io/client-go/api/v1/types_swagger_generated.go +++ b/staging/src/kubevirt.io/client-go/api/v1/types_swagger_generated.go @@ -98,6 +98,7 @@ func (VolumeStatus) SwaggerDoc() map[string]string { "message": "Message is a detailed message about the current hotplug volume phase", "persistentVolumeClaimInfo": "PersistentVolumeClaimInfo is information about the PVC that handler requires during start flow", "hotplugVolume": "If the volume is hotplug, this will contain the hotplug status.", + "size": "Represents the size of the volume", } } diff --git a/staging/src/kubevirt.io/client-go/apis/snapshot/v1alpha1/openapi_generated.go b/staging/src/kubevirt.io/client-go/apis/snapshot/v1alpha1/openapi_generated.go index 866f0fa795ea..aa6d120b9ebf 100644 --- a/staging/src/kubevirt.io/client-go/apis/snapshot/v1alpha1/openapi_generated.go +++ b/staging/src/kubevirt.io/client-go/apis/snapshot/v1alpha1/openapi_generated.go @@ -20958,6 +20958,13 @@ func schema_kubevirtio_client_go_api_v1_VolumeStatus(ref common.ReferenceCallbac Ref: ref("kubevirt.io/client-go/api/v1.HotplugVolumeStatus"), }, }, + "size": { + SchemaProps: spec.SchemaProps{ + Description: "Represents the size of the volume", + Type: []string{"integer"}, + Format: "int64", + }, + }, }, Required: []string{"name", "target"}, }, diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index ee51d31c16b4..8d31d5e0b376 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -185,6 +185,7 @@ go_test( "//pkg/virt-controller/leaderelectionconfig:go_default_library", "//pkg/virt-controller/services:go_default_library", "//pkg/virt-controller/watch:go_default_library", + "//pkg/virt-handler:go_default_library", "//pkg/virt-handler/device-manager:go_default_library", "//pkg/virt-handler/node-labeller/util:go_default_library", "//pkg/virt-launcher/virtwrap/api:go_default_library", diff --git a/tests/migration_test.go b/tests/migration_test.go index a82c3e161f0c..b8683b2a2e0b 100644 --- a/tests/migration_test.go +++ b/tests/migration_test.go @@ -28,6 +28,8 @@ import ( "strings" "sync" + virthandler "kubevirt.io/kubevirt/pkg/virt-handler" + "kubevirt.io/kubevirt/tests/util" "kubevirt.io/kubevirt/tools/vms-generator/utils" @@ -1741,6 +1743,94 @@ var _ = Describe("[Serial][rfe_id:393][crit:high][vendor:cnv-qe@redhat.com][leve By("Deleting the VMI") Expect(virtClient.VirtualMachineInstance(vmi.Namespace).Delete(vmi.Name, &metav1.DeleteOptions{})).To(Succeed()) + By("Waiting for VMI to disappear") + tests.WaitForVirtualMachineToDisappearWithTimeout(vmi, 240) + }) + It("Migration should generate empty isos of the right size on the target", func() { + By("Creating a VMI with cloud-init and config maps") + vmi := tests.NewRandomVMIWithEphemeralDisk(cd.ContainerDiskFor(cd.ContainerDiskCirros)) + configMapName := "configmap-" + rand.String(5) + secretName := "secret-" + rand.String(5) + downwardAPIName := "downwardapi-" + rand.String(5) + config_data := map[string]string{ + "config1": "value1", + "config2": "value2", + } + secret_data := map[string]string{ + "user": "admin", + "password": "community", + } + tests.CreateConfigMap(configMapName, config_data) + tests.CreateSecret(secretName, secret_data) + tests.AddUserData(vmi, "cloud-init", "#!/bin/bash\necho 'hello'\n") + tests.AddConfigMapDisk(vmi, configMapName, configMapName) + tests.AddSecretDisk(vmi, secretName, secretName) + tests.AddServiceAccountDisk(vmi, "default") + // In case there are no existing labels add labels to add some data to the downwardAPI disk + if vmi.ObjectMeta.Labels == nil { + vmi.ObjectMeta.Labels = map[string]string{"downwardTestLabelKey": "downwardTestLabelVal"} + } + tests.AddLabelDownwardAPIVolume(vmi, downwardAPIName) + + // this annotation causes virt launcher to immediately fail a migration + vmi.Annotations = map[string]string{v1.FuncTestBlockLauncherPrepareMigrationTargetAnnotation: ""} + + By("Starting the VirtualMachineInstance") + vmi = runVMIAndExpectLaunch(vmi, 240) + + // execute a migration + By("Starting the Migration") + migration := tests.NewRandomMigration(vmi.Name, vmi.Namespace) + migration, err = virtClient.VirtualMachineInstanceMigration(migration.Namespace).Create(migration) + Expect(err).ToNot(HaveOccurred()) + + By("Waiting for Migration to reach Preparing Target Phase") + Eventually(func() v1.VirtualMachineInstanceMigrationPhase { + migration, err = virtClient.VirtualMachineInstanceMigration(migration.Namespace).Get(migration.Name, &metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + + phase := migration.Status.Phase + Expect(phase).NotTo(Equal(v1.MigrationSucceeded)) + return phase + }, 120, 1*time.Second).Should(Equal(v1.MigrationPreparingTarget)) + + vmi, err = virtClient.VirtualMachineInstance(vmi.Namespace).Get(vmi.Name, &metav1.GetOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(vmi.Status.MigrationState).ToNot(BeNil()) + Expect(vmi.Status.MigrationState.TargetPod).ToNot(Equal("")) + + By("Sanity checking the volume status size and the actual virt-launcher file") + for _, volume := range vmi.Spec.Volumes { + for _, volType := range []string{"cloud-init", "configmap-", "default-", "downwardapi-", "secret-"} { + if strings.HasPrefix(volume.Name, volType) { + for _, volStatus := range vmi.Status.VolumeStatus { + if volStatus.Name == volume.Name { + Expect(volStatus.Size).To(BeNumerically(">", 0), "Size of volume %s is 0", volume.Name) + volPath, found := virthandler.IsoGuestVolumePath(vmi, &volume) + if !found { + continue + } + // Wait for the iso to be created + Eventually(func() string { + output, err := tests.RunCommandOnVmiTargetPod(vmi, []string{"/bin/bash", "-c", "[[ -f " + volPath + " ]] && echo found || true"}) + Expect(err).ToNot(HaveOccurred()) + return output + }, 30*time.Second, time.Second).Should(ContainSubstring("found"), volPath+" never appeared") + output, err := tests.RunCommandOnVmiTargetPod(vmi, []string{"/bin/bash", "-c", "/usr/bin/stat --printf=%s " + volPath}) + Expect(err).ToNot(HaveOccurred()) + Expect(strconv.Atoi(output)).To(Equal(int(volStatus.Size)), "ISO file for volume %s is not empty", volume.Name) + output, err = tests.RunCommandOnVmiTargetPod(vmi, []string{"/bin/bash", "-c", fmt.Sprintf(`/usr/bin/cmp -n %d %s /dev/zero || true`, volStatus.Size, volPath)}) + Expect(err).ToNot(HaveOccurred()) + Expect(output).ToNot(ContainSubstring("differ"), "ISO file for volume %s is not empty", volume.Name) + } + } + } + } + } + + By("Deleting the VMI") + Expect(virtClient.VirtualMachineInstance(vmi.Namespace).Delete(vmi.Name, &metav1.DeleteOptions{})).To(Succeed()) + By("Waiting for VMI to disappear") tests.WaitForVirtualMachineToDisappearWithTimeout(vmi, 240) }) diff --git a/tests/operator_test.go b/tests/operator_test.go index 0e52213dccec..f816f897c1eb 100644 --- a/tests/operator_test.go +++ b/tests/operator_test.go @@ -51,6 +51,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/rand" aggregatorclient "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" "k8s.io/utils/pointer" @@ -637,7 +638,35 @@ var _ = Describe("[Serial][sig-operator]Operator", func() { vmis := []*v1.VirtualMachineInstance{} for i := 0; i < num; i++ { - vmis = append(vmis, tests.NewRandomVMIWithEphemeralDisk(cd.ContainerDiskFor(cd.ContainerDiskCirros))) + vmi := tests.NewRandomVMIWithEphemeralDisk(cd.ContainerDiskFor(cd.ContainerDiskCirros)) + configMapName := "configmap-" + rand.String(5) + secretName := "secret-" + rand.String(5) + downwardAPIName := "downwardapi-" + rand.String(5) + + config_data := map[string]string{ + "config1": "value1", + "config2": "value2", + } + + secret_data := map[string]string{ + "user": "admin", + "password": "community", + } + + tests.CreateConfigMap(configMapName, config_data) + tests.CreateSecret(secretName, secret_data) + + tests.AddUserData(vmi, "cloud-init", "#!/bin/bash\necho 'hello'\n") + tests.AddConfigMapDisk(vmi, configMapName, configMapName) + tests.AddSecretDisk(vmi, secretName, secretName) + tests.AddServiceAccountDisk(vmi, "default") + // In case there are no existing labels add labels to add some data to the downwardAPI disk + if vmi.ObjectMeta.Labels == nil { + vmi.ObjectMeta.Labels = map[string]string{"downwardTestLabelKey": "downwardTestLabelVal"} + } + tests.AddLabelDownwardAPIVolume(vmi, downwardAPIName) + + vmis = append(vmis, vmi) } return vmis diff --git a/tests/utils.go b/tests/utils.go index acbb9ca7d819..94520f3cec38 100644 --- a/tests/utils.go +++ b/tests/utils.go @@ -3985,6 +3985,36 @@ func RunCommandOnVmiPod(vmi *v1.VirtualMachineInstance, command []string) string return output } +// RunCommandOnVmiTargetPod runs specified command on the target virt-launcher pod of a migration +func RunCommandOnVmiTargetPod(vmi *v1.VirtualMachineInstance, command []string) (string, error) { + virtClient, err := kubecli.GetKubevirtClient() + util2.PanicOnError(err) + + pods, err := virtClient.CoreV1().Pods(util2.NamespaceTestDefault).List(context.Background(), metav1.ListOptions{}) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + ExpectWithOffset(1, pods.Items).NotTo(BeEmpty()) + var vmiPod *k8sv1.Pod + for _, pod := range pods.Items { + if pod.Name == vmi.Status.MigrationState.TargetPod { + vmiPod = &pod + break + } + } + if vmiPod == nil { + return "", fmt.Errorf("failed to find migration target pod") + } + + output, err := ExecuteCommandOnPod( + virtClient, + vmiPod, + "compute", + command, + ) + ExpectWithOffset(1, err).ToNot(HaveOccurred()) + + return output, nil +} + // GetNodeLibvirtCapabilities returns node libvirt capabilities func GetNodeLibvirtCapabilities(vmi *v1.VirtualMachineInstance) string { return RunCommandOnVmiPod(vmi, []string{"virsh", "-r", "capabilities"})