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 }