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 }