github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/cmd/vm-control/importVirshVm.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/xml"
     5  	"fmt"
     6  	"net"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/Cloud-Foundations/Dominator/lib/errors"
    15  	"github.com/Cloud-Foundations/Dominator/lib/json"
    16  	"github.com/Cloud-Foundations/Dominator/lib/log"
    17  	"github.com/Cloud-Foundations/Dominator/lib/srpc"
    18  	proto "github.com/Cloud-Foundations/Dominator/proto/hypervisor"
    19  )
    20  
    21  type bridgeType struct {
    22  	Bridge string `xml:"bridge,attr"`
    23  }
    24  
    25  type cpuType struct {
    26  	Mode string `xml:"mode,attr"`
    27  }
    28  
    29  type devicesInfo struct {
    30  	Volumes     []volumeType    `xml:"disk"`
    31  	Interfaces  []interfaceType `xml:"interface"`
    32  	SerialPorts []serialType    `xml:"serial"`
    33  }
    34  
    35  type driverType struct {
    36  	Name  string `xml:"name,attr"`
    37  	Type  string `xml:"type,attr"`
    38  	Cache string `xml:"cache,attr"`
    39  	Io    string `xml:"io,attr"`
    40  }
    41  
    42  type interfaceType struct {
    43  	Mac    macType    `xml:"mac"`
    44  	Model  modelType  `xml:"model"`
    45  	Source bridgeType `xml:"source"`
    46  	Type   string     `xml:"type,attr"`
    47  }
    48  
    49  type macType struct {
    50  	Address string `xml:"address,attr"`
    51  }
    52  
    53  type memoryInfo struct {
    54  	Value uint64 `xml:",chardata"`
    55  	Unit  string `xml:"unit,attr"`
    56  }
    57  
    58  type modelType struct {
    59  	Type string `xml:"type,attr"`
    60  }
    61  
    62  type osInfo struct {
    63  	Type osTypeInfo `xml:"type"`
    64  }
    65  
    66  type osTypeInfo struct {
    67  	Arch    string `xml:"arch,attr"`
    68  	Machine string `xml:"machine,attr"`
    69  	Value   string `xml:",chardata"`
    70  }
    71  
    72  type serialType struct {
    73  	Source serialSourceType `xml:"source"`
    74  	Type   string           `xml:"type,attr"`
    75  }
    76  
    77  type serialSourceType struct {
    78  	Path string `xml:"path,attr"`
    79  }
    80  
    81  type sourceType struct {
    82  	File string `xml:"file,attr"`
    83  }
    84  
    85  type targetType struct {
    86  	Device string `xml:"dev,attr"`
    87  	Bus    string `xml:"bus,attr"`
    88  }
    89  
    90  type vCpuInfo struct {
    91  	Num       uint   `xml:",chardata"`
    92  	Placement string `xml:"placement,attr"`
    93  }
    94  
    95  type virshInfoType struct {
    96  	XMLName xml.Name    `xml:"domain"`
    97  	Cpu     cpuType     `xml:"cpu"`
    98  	Devices devicesInfo `xml:"devices"`
    99  	Memory  memoryInfo  `xml:"memory"`
   100  	Name    string      `xml:"name"`
   101  	Os      osInfo      `xml:"os"`
   102  	Type    string      `xml:"type,attr"`
   103  	VCpu    vCpuInfo    `xml:"vcpu"`
   104  }
   105  
   106  type volumeType struct {
   107  	Device string     `xml:"device,attr"`
   108  	Driver driverType `xml:"driver"`
   109  	Source sourceType `xml:"source"`
   110  	Target targetType `xml:"target"`
   111  	Type   string     `xml:"type,attr"`
   112  }
   113  
   114  func importVirshVmSubcommand(args []string, logger log.DebugLogger) error {
   115  	macAddr := args[0]
   116  	domainName := args[1]
   117  	args = args[2:]
   118  	if len(args)%2 != 0 {
   119  		return fmt.Errorf("missing IP address for MAC: %s", args[len(args)-1])
   120  	}
   121  	sAddrs := make([]proto.Address, 0, len(args)/2)
   122  	for index := 0; index < len(args); index += 2 {
   123  		ipAddr := args[index+1]
   124  		ipList, err := net.LookupIP(ipAddr)
   125  		if err != nil {
   126  			return err
   127  		}
   128  		if len(ipList) != 1 {
   129  			return fmt.Errorf("number of IPs for %s: %d != 1",
   130  				ipAddr, len(ipList))
   131  		}
   132  		sAddrs = append(sAddrs, proto.Address{
   133  			IpAddress:  ipList[0],
   134  			MacAddress: args[index],
   135  		})
   136  	}
   137  	if err := importVirshVm(macAddr, domainName, sAddrs, logger); err != nil {
   138  		return fmt.Errorf("Error importing VM: %s", err)
   139  	}
   140  	return nil
   141  }
   142  
   143  func ensureDomainIsStopped(domainName string) error {
   144  	state, err := getDomainState(domainName)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	if state == "shut off" {
   149  		return nil
   150  	}
   151  	if state != "running" {
   152  		return fmt.Errorf("domain is in unsupported state \"%s\"", state)
   153  	}
   154  	response, err := askForInputChoice("Cannot import running VM",
   155  		[]string{"shutdown", "quit"})
   156  	if err != nil {
   157  		return err
   158  	}
   159  	if response == "quit" {
   160  		return fmt.Errorf("domain must be shut off but is \"%s\"", state)
   161  	}
   162  	err = exec.Command("virsh", []string{"shutdown", domainName}...).Run()
   163  	if err != nil {
   164  		return fmt.Errorf("error shutting down VM: %s", err)
   165  	}
   166  	for ; ; time.Sleep(time.Second) {
   167  		state, err := getDomainState(domainName)
   168  		if err != nil {
   169  			if strings.Contains(err.Error(), "Domain not found") {
   170  				return nil
   171  			}
   172  			return err
   173  		}
   174  		if state == "shut off" {
   175  			return nil
   176  		}
   177  	}
   178  }
   179  
   180  func getDomainState(domainName string) (string, error) {
   181  	cmd := exec.Command("virsh", []string{"domstate", domainName}...)
   182  	stdout, err := cmd.Output()
   183  	if err != nil {
   184  		return "", fmt.Errorf("error getting VM status: %s",
   185  			err.(*exec.ExitError).Stderr)
   186  	}
   187  	return strings.TrimSpace(string(stdout)), nil
   188  }
   189  
   190  func importVirshVm(macAddr, domainName string, sAddrs []proto.Address,
   191  	logger log.DebugLogger) error {
   192  	ipList, err := net.LookupIP(domainName)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	if len(ipList) != 1 {
   197  		return fmt.Errorf("number of IPs %d != 1", len(ipList))
   198  	}
   199  	tags := vmTags.Copy()
   200  	if _, ok := tags["Name"]; !ok {
   201  		tags["Name"] = domainName
   202  	}
   203  	request := proto.ImportLocalVmRequest{VmInfo: proto.VmInfo{
   204  		ConsoleType:   consoleType,
   205  		DisableVirtIO: *disableVirtIO,
   206  		Hostname:      domainName,
   207  		OwnerGroups:   ownerGroups,
   208  		OwnerUsers:    ownerUsers,
   209  		Tags:          tags,
   210  	}}
   211  	hypervisor := fmt.Sprintf(":%d", *hypervisorPortNum)
   212  	client, err := srpc.DialHTTP("tcp", hypervisor, 0)
   213  	if err != nil {
   214  		return err
   215  	}
   216  	defer client.Close()
   217  	verificationCookie, err := readRootCookie(client, logger)
   218  	if err != nil {
   219  		return err
   220  	}
   221  	directories, err := listVolumeDirectories(client)
   222  	if err != nil {
   223  		return err
   224  	}
   225  	volumeRoots := make(map[string]string, len(directories))
   226  	for _, dirname := range directories {
   227  		volumeRoots[filepath.Dir(dirname)] = dirname
   228  	}
   229  	cmd := exec.Command("virsh",
   230  		[]string{"dumpxml", "--inactive", domainName}...)
   231  	stdout, err := cmd.Output()
   232  	if err != nil {
   233  		return fmt.Errorf("error getting XML data: %s", err)
   234  	}
   235  	var virshInfo virshInfoType
   236  	if err := xml.Unmarshal(stdout, &virshInfo); err != nil {
   237  		return err
   238  	}
   239  	json.WriteWithIndent(os.Stdout, "    ", virshInfo)
   240  	if numIf := len(virshInfo.Devices.Interfaces); numIf != len(sAddrs)+1 {
   241  		return fmt.Errorf("number of interfaces %d != %d",
   242  			numIf, len(sAddrs)+1)
   243  	}
   244  	if macAddr != virshInfo.Devices.Interfaces[0].Mac.Address {
   245  		return fmt.Errorf("MAC address specified: %s != virsh data: %s",
   246  			macAddr, virshInfo.Devices.Interfaces[0].Mac.Address)
   247  	}
   248  	request.VmInfo.Address = proto.Address{
   249  		IpAddress:  ipList[0],
   250  		MacAddress: virshInfo.Devices.Interfaces[0].Mac.Address,
   251  	}
   252  	for index, sAddr := range sAddrs {
   253  		if sAddr.MacAddress !=
   254  			virshInfo.Devices.Interfaces[index+1].Mac.Address {
   255  			return fmt.Errorf("MAC address specified: %s != virsh data: %s",
   256  				sAddr.MacAddress,
   257  				virshInfo.Devices.Interfaces[index+1].Mac.Address)
   258  		}
   259  		request.SecondaryAddresses = append(request.SecondaryAddresses, sAddr)
   260  	}
   261  	switch virshInfo.Memory.Unit {
   262  	case "KiB":
   263  		request.VmInfo.MemoryInMiB = virshInfo.Memory.Value >> 10
   264  	case "MiB":
   265  		request.VmInfo.MemoryInMiB = virshInfo.Memory.Value
   266  	case "GiB":
   267  		request.VmInfo.MemoryInMiB = virshInfo.Memory.Value << 10
   268  	default:
   269  		return fmt.Errorf("unknown memory unit: %s", virshInfo.Memory.Unit)
   270  	}
   271  	request.VmInfo.MilliCPUs = virshInfo.VCpu.Num * 1000
   272  	myPidStr := strconv.Itoa(os.Getpid())
   273  	if err := ensureDomainIsStopped(domainName); err != nil {
   274  		return err
   275  	}
   276  	logger.Debugln(0, "finding volumes")
   277  	for index, inputVolume := range virshInfo.Devices.Volumes {
   278  		if inputVolume.Device != "disk" {
   279  			continue
   280  		}
   281  		var volumeFormat proto.VolumeFormat
   282  		err := volumeFormat.UnmarshalText([]byte(inputVolume.Driver.Type))
   283  		if err != nil {
   284  			return err
   285  		}
   286  		inputFilename := inputVolume.Source.File
   287  		var volumeRoot string
   288  		for dirname := filepath.Dir(inputFilename); ; {
   289  			if vr, ok := volumeRoots[dirname]; ok {
   290  				volumeRoot = vr
   291  				break
   292  			}
   293  			if dirname == "/" {
   294  				break
   295  			}
   296  			dirname = filepath.Dir(dirname)
   297  		}
   298  		if volumeRoot == "" {
   299  			return fmt.Errorf("no Hypervisor directory for: %s", inputFilename)
   300  		}
   301  		outputDirname := filepath.Join(volumeRoot, "import", myPidStr)
   302  		if err := os.MkdirAll(outputDirname, dirPerms); err != nil {
   303  			return err
   304  		}
   305  		defer os.RemoveAll(outputDirname)
   306  		outputFilename := filepath.Join(outputDirname,
   307  			fmt.Sprintf("volume-%d", index))
   308  		if err := os.Link(inputFilename, outputFilename); err != nil {
   309  			return err
   310  		}
   311  		request.VolumeFilenames = append(request.VolumeFilenames,
   312  			outputFilename)
   313  		request.VmInfo.Volumes = append(request.VmInfo.Volumes,
   314  			proto.Volume{Format: volumeFormat})
   315  	}
   316  	json.WriteWithIndent(os.Stdout, "    ", request)
   317  	request.VerificationCookie = verificationCookie
   318  	var reply proto.GetVmInfoResponse
   319  	logger.Debugln(0, "issuing import RPC")
   320  	err = client.RequestReply("Hypervisor.ImportLocalVm", request, &reply)
   321  	if err != nil {
   322  		return fmt.Errorf("Hypervisor.ImportLocalVm RPC failed: %s", err)
   323  	}
   324  	if err := errors.New(reply.Error); err != nil {
   325  		return fmt.Errorf("Hypervisor failed to import: %s", err)
   326  	}
   327  	logger.Debugln(0, "imported VM")
   328  	for _, dirname := range directories {
   329  		os.RemoveAll(filepath.Join(dirname, "import", myPidStr))
   330  	}
   331  	if err := maybeWatchVm(client, hypervisor, ipList[0], logger); err != nil {
   332  		return err
   333  	}
   334  	if err := askForCommitDecision(client, ipList[0]); err != nil {
   335  		if err == errorCommitAbandoned {
   336  			response, _ := askForInputChoice(
   337  				"Do you want to restart the old VM", []string{"y", "n"})
   338  			if response != "y" {
   339  				return err
   340  			}
   341  			cmd = exec.Command("virsh", "start", domainName)
   342  			if output, err := cmd.CombinedOutput(); err != nil {
   343  				logger.Println(string(output))
   344  				return err
   345  			}
   346  		}
   347  		return err
   348  	}
   349  	defer virshInfo.deleteVolumes()
   350  	cmd = exec.Command("virsh",
   351  		[]string{"undefine", "--managed-save", "--snapshots-metadata",
   352  			"--remove-all-storage", domainName}...)
   353  	if output, err := cmd.CombinedOutput(); err != nil {
   354  		logger.Println(string(output))
   355  		return fmt.Errorf("error destroying old VM: %s", err)
   356  	}
   357  	return nil
   358  }
   359  
   360  func (virshInfo virshInfoType) deleteVolumes() {
   361  	for _, inputVolume := range virshInfo.Devices.Volumes {
   362  		if inputVolume.Device != "disk" {
   363  			continue
   364  		}
   365  		os.Remove(inputVolume.Source.File)
   366  	}
   367  }