diff --git a/internal/vmware_nbdkit/v2v.go b/internal/vmware_nbdkit/v2v.go new file mode 100644 index 0000000..17340b7 --- /dev/null +++ b/internal/vmware_nbdkit/v2v.go @@ -0,0 +1,182 @@ +package vmware_nbdkit + +import ( + "context" + "encoding/xml" + "fmt" + "os" + "os/exec" + + log "github.com/sirupsen/logrus" + "github.com/vmware/govmomi/vim25/mo" +) + +type libvirtDomain struct { + XMLName xml.Name `xml:"domain"` + Type string `xml:"type,attr"` + Name string `xml:"name"` + Memory libvirtMemory `xml:"memory"` + VCPU int32 `xml:"vcpu"` + OS libvirtOS `xml:"os"` + Features libvirtFeatures `xml:"features"` + Devices libvirtDevices `xml:"devices"` +} + +type libvirtMemory struct { + Unit string `xml:"unit,attr,omitempty"` + Value int64 `xml:",chardata"` +} + +type libvirtOS struct { + Type libvirtOSType `xml:"type"` + Boot libvirtBoot `xml:"boot"` +} + +type libvirtOSType struct { + Value string `xml:",chardata"` +} + +type libvirtBoot struct { + Dev string `xml:"dev,attr"` +} + +type libvirtFeatures struct { + ACPI struct{} `xml:"acpi"` + APIC struct{} `xml:"apic"` + PAE struct{} `xml:"pae"` +} + +type libvirtDevices struct { + Disks []libvirtDisk `xml:"disk"` +} + +type libvirtDisk struct { + Type string `xml:"type,attr"` + Device string `xml:"device,attr"` + Driver libvirtDiskDriver `xml:"driver"` + Source libvirtDiskSource `xml:"source"` + Target libvirtDiskTarget `xml:"target"` +} + +type libvirtDiskDriver struct { + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` +} + +type libvirtDiskSource struct { + Dev string `xml:"dev,attr"` +} + +type libvirtDiskTarget struct { + Dev string `xml:"dev,attr"` + Bus string `xml:"bus,attr"` +} + +func linuxDiskName(index int) string { + name := "" + for { + name = string(rune('a'+index%26)) + name + index = index/26 - 1 + if index < 0 { + break + } + } + + return "sd" + name +} + +func (s *NbdkitServers) buildLibvirtXML(ctx context.Context, targets []*migrationTarget) ([]byte, error) { + if len(targets) == 0 { + return nil, fmt.Errorf("no disks available for virt-v2v") + } + + var vm mo.VirtualMachine + err := s.VirtualMachine.Properties(ctx, s.VirtualMachine.Reference(), []string{"config"}, &vm) + if err != nil { + return nil, err + } + + memoryKiB := int64(vm.Config.Hardware.MemoryMB) * 1024 + if memoryKiB == 0 { + memoryKiB = 1048576 + } + + vcpu := vm.Config.Hardware.NumCPU + if vcpu == 0 { + vcpu = 1 + } + + domain := libvirtDomain{ + Type: "kvm", + Name: vm.Config.Name, + Memory: libvirtMemory{ + Unit: "KiB", + Value: memoryKiB, + }, + VCPU: vcpu, + OS: libvirtOS{ + Type: libvirtOSType{Value: "hvm"}, + Boot: libvirtBoot{Dev: "hd"}, + }, + } + + for index, target := range targets { + domain.Devices.Disks = append(domain.Devices.Disks, libvirtDisk{ + Type: "block", + Device: "disk", + Driver: libvirtDiskDriver{ + Name: "qemu", + Type: "raw", + }, + Source: libvirtDiskSource{ + Dev: target.Path, + }, + Target: libvirtDiskTarget{ + Dev: linuxDiskName(index), + Bus: "scsi", + }, + }) + } + + body, err := xml.MarshalIndent(domain, "", " ") + if err != nil { + return nil, err + } + + return append([]byte(xml.Header), body...), nil +} + +func (s *NbdkitServers) RunVirtV2VInPlace(ctx context.Context, targets []*migrationTarget) error { + log.WithField("disks", len(targets)).Info("Running virt-v2v-in-place") + + xmlData, err := s.buildLibvirtXML(ctx, targets) + if err != nil { + return err + } + + xmlFile, err := os.CreateTemp("", "migratekit-v2v-*.xml") + if err != nil { + return err + } + defer os.Remove(xmlFile.Name()) + + _, err = xmlFile.Write(xmlData) + if closeErr := xmlFile.Close(); err == nil { + err = closeErr + } + if err != nil { + return err + } + + args := []string{"-i", "libvirtxml", xmlFile.Name()} + if s.VddkConfig.Debug { + args = append([]string{"-v", "-x"}, args...) + } + + cmd := exec.Command("virt-v2v-in-place", args...) + cmd.Env = append(os.Environ(), "LIBGUESTFS_BACKEND=direct") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} diff --git a/internal/vmware_nbdkit/vmware_nbdkit.go b/internal/vmware_nbdkit/vmware_nbdkit.go index 78b5134..3c6ee4f 100644 --- a/internal/vmware_nbdkit/vmware_nbdkit.go +++ b/internal/vmware_nbdkit/vmware_nbdkit.go @@ -2,9 +2,9 @@ package vmware_nbdkit import ( "context" + "errors" "net/url" "os" - "os/exec" "os/signal" "syscall" @@ -43,6 +43,12 @@ type NbdkitServer struct { Nbdkit *nbdkit.NbdkitServer } +type migrationTarget struct { + Server *NbdkitServer + Target target.Target + Path string +} + func NewNbdkitServers(vddk *VddkConfig, vm *object.VirtualMachine) *NbdkitServers { return &NbdkitServers{ VddkConfig: vddk, @@ -184,23 +190,114 @@ func (s *NbdkitServers) MigrationCycle(ctx context.Context, runV2V bool) error { } }() - for index, server := range s.Servers { + if runV2V { + return s.migrationCycleWithV2V(ctx) + } + + for _, server := range s.Servers { t, err := target.NewOpenStack(ctx, s.VirtualMachine, server.Disk) if err != nil { return err } - if index != 0 { - runV2V = false + err = server.SyncToTarget(ctx, t) + if err != nil { + return err + } + } + + return nil +} + +func disconnectMigrationTargets(ctx context.Context, targets []*migrationTarget) error { + var errs []error + + for _, mt := range targets { + if mt.Target == nil { + continue + } + + if err := mt.Target.Disconnect(ctx); err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} + +func (s *NbdkitServers) migrationCycleWithV2V(ctx context.Context) error { + targets := []*migrationTarget{} + connected := true + defer func() { + if connected { + err := disconnectMigrationTargets(ctx, targets) + if err != nil { + log.WithError(err).Error("Failed to disconnect from targets") + } } + }() - err = server.SyncToTarget(ctx, t, runV2V) + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + defer signal.Stop(c) + go func() { + <-c + log.Warn("Received interrupt signal, cleaning up...") + + err := disconnectMigrationTargets(ctx, targets) + if err != nil { + log.WithError(err).Fatal("Failed to disconnect from targets") + } + + os.Exit(1) + }() + + for _, server := range s.Servers { + t, err := target.NewOpenStack(ctx, s.VirtualMachine, server.Disk) if err != nil { return err } + + err = t.Connect(ctx) + if err != nil { + return err + } + + mt := &migrationTarget{ + Server: server, + Target: t, + } + targets = append(targets, mt) + + path, err := t.GetPath(ctx) + if err != nil { + return err + } + mt.Path = path } - return nil + for _, mt := range targets { + _, err := mt.Server.CopyToTarget(ctx, mt.Target, mt.Path) + if err != nil { + return err + } + } + + err := s.RunVirtV2VInPlace(ctx, targets) + if err != nil { + return err + } + + for _, mt := range targets { + err := mt.Target.WriteChangeID(ctx, &vmware.ChangeID{}) + if err != nil { + return err + } + } + + err = disconnectMigrationTargets(ctx, targets) + connected = false + return err } func (s *NbdkitServer) FullCopyToTarget(t target.Target, path string, targetIsClean bool) error { @@ -308,18 +405,34 @@ func (s *NbdkitServer) IncrementalCopyToTarget(ctx context.Context, t target.Tar return nil } -func (s *NbdkitServer) SyncToTarget(ctx context.Context, t target.Target, runV2V bool) error { +func (s *NbdkitServer) CopyToTarget(ctx context.Context, t target.Target, path string) (*vmware.ChangeID, error) { snapshotChangeId, err := vmware.GetChangeID(s.Disk) if err != nil { - return err + return nil, err } needFullCopy, targetIsClean, err := target.NeedsFullCopy(ctx, t) if err != nil { - return err + return nil, err + } + + if needFullCopy { + err = s.FullCopyToTarget(t, path, targetIsClean) + if err != nil { + return nil, err + } + } else { + err = s.IncrementalCopyToTarget(ctx, t, path) + if err != nil { + return nil, err + } } - err = t.Connect(ctx) + return snapshotChangeId, nil +} + +func (s *NbdkitServer) SyncToTarget(ctx context.Context, t target.Target) error { + err := t.Connect(ctx) if err != nil { return err } @@ -344,47 +457,14 @@ func (s *NbdkitServer) SyncToTarget(ctx context.Context, t target.Target, runV2V return err } - if needFullCopy { - err = s.FullCopyToTarget(t, path, targetIsClean) - if err != nil { - return err - } - } else { - err = s.IncrementalCopyToTarget(ctx, t, path) - if err != nil { - return err - } + snapshotChangeId, err := s.CopyToTarget(ctx, t, path) + if err != nil { + return err } - if runV2V { - log.Info("Running virt-v2v-in-place") - - os.Setenv("LIBGUESTFS_BACKEND", "direct") - - var cmd *exec.Cmd - if s.Servers.VddkConfig.Debug { - cmd = exec.Command("virt-v2v-in-place", "-v", "-x", "-i", "disk", path) - } else { - cmd = exec.Command("virt-v2v-in-place", "-i", "disk", path) - } - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - err := cmd.Run() - if err != nil { - return err - } - - err = t.WriteChangeID(ctx, &vmware.ChangeID{}) - if err != nil { - return err - } - } else { - err = t.WriteChangeID(ctx, snapshotChangeId) - if err != nil { - return err - } + err = t.WriteChangeID(ctx, snapshotChangeId) + if err != nil { + return err } return nil