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