github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/container/kvm/wrappedcmds.go (about) 1 // Copyright 2013-2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package kvm 5 6 // This file contains wrappers around the following executables: 7 // genisoimage 8 // qemu-img 9 // virsh 10 // Those executables are found in the following packages: 11 // genisoimage 12 // libvirt-bin 13 // qemu-utils 14 // 15 // These executables provide Juju's interface to dealing with kvm containers. 16 // They are the means by which we start, stop and list running containers on 17 // the host 18 19 import ( 20 "encoding/xml" 21 "fmt" 22 "os" 23 "path/filepath" 24 "regexp" 25 "strings" 26 27 "github.com/juju/errors" 28 "github.com/juju/utils/v3" 29 "gopkg.in/yaml.v2" 30 31 "github.com/juju/juju/container/kvm/libvirt" 32 "github.com/juju/juju/core/arch" 33 "github.com/juju/juju/core/paths" 34 ) 35 36 const ( 37 virsh = "virsh" 38 guestDir = "guests" 39 poolName = "juju-pool" 40 kvm = "kvm" 41 metadata = "meta-data" 42 userdata = "user-data" 43 networkconfig = "network-config" 44 45 // This path is only valid on ubuntu, and xenial at this point. 46 // TODO(ro) 2017-01-20 Determine if we will support trusty and update this 47 // as necessary if so. It seems it will require some serious acrobatics to 48 // get trusty to work properly and that may be out of scope for juju. 49 nvramCode = "/usr/share/AAVMF/AAVMF_CODE.fd" 50 ) 51 52 var ( 53 // The regular expression for breaking up the results of 'virsh list' 54 // (?m) - specify that this is a multi-line regex 55 // first part is the opaque identifier we don't care about 56 // then the hostname, and lastly the status. 57 machineListPattern = regexp.MustCompile(`(?m)^\s+\d+\s+(?P<hostname>[-\w]+)\s+(?P<status>.+)\s*$`) 58 ) 59 60 // CreateMachineParams Implements libvirt.domainParams. 61 type CreateMachineParams struct { 62 Hostname string 63 Version string 64 UserDataFile string 65 NetworkConfigData string 66 Memory uint64 67 CpuCores uint64 68 RootDisk uint64 69 Interfaces []libvirt.InterfaceInfo 70 71 disks []libvirt.DiskInfo 72 findPath pathfinderFunc 73 74 runCmd runFunc 75 runCmdAsRoot runFunc 76 arch string 77 } 78 79 // Arch returns the architecture to be used. 80 func (p CreateMachineParams) Arch() string { 81 if p.arch != "" { 82 return p.arch 83 } 84 return arch.HostArch() 85 } 86 87 // Loader is the path to the binary firmware blob used in UEFI booting. At the 88 // time of this writing only ARM64 requires this to run. 89 func (p CreateMachineParams) Loader() string { 90 return nvramCode 91 } 92 93 // Host implements libvirt.domainParams. 94 func (p CreateMachineParams) Host() string { 95 return p.Hostname 96 } 97 98 // CPUs implements libvirt.domainParams. 99 func (p CreateMachineParams) CPUs() uint64 { 100 if p.CpuCores == 0 { 101 return 1 102 } 103 return p.CpuCores 104 } 105 106 // DiskInfo implements libvirt.domainParams. 107 func (p CreateMachineParams) DiskInfo() []libvirt.DiskInfo { 108 return p.disks 109 } 110 111 // RAM implements libvirt.domainParams. 112 func (p CreateMachineParams) RAM() uint64 { 113 if p.Memory == 0 { 114 return 512 115 } 116 return p.Memory 117 } 118 119 // NetworkInfo implements libvirt.domainParams. 120 func (p CreateMachineParams) NetworkInfo() []libvirt.InterfaceInfo { 121 return p.Interfaces 122 } 123 124 // ValidateDomainParams implements libvirt.domainParams. 125 func (p CreateMachineParams) ValidateDomainParams() error { 126 if p.Hostname == "" { 127 return errors.Errorf("missing required hostname") 128 } 129 if len(p.disks) < 2 { 130 // We need at least the drive and the data source disk. 131 return errors.Errorf("got %d disks, need at least 2", len(p.disks)) 132 } 133 var ds, fs bool 134 for _, d := range p.disks { 135 if d.Driver() == "qcow2" { 136 fs = true 137 } 138 if d.Driver() == "raw" { 139 ds = true 140 } 141 } 142 if !ds { 143 return errors.Trace(errors.Errorf("missing data source disk")) 144 } 145 if !fs { 146 return errors.Trace(errors.Errorf("missing system disk")) 147 } 148 return nil 149 } 150 151 // diskInfo is type for implementing libvirt.DiskInfo. 152 type diskInfo struct { 153 driver, source string 154 } 155 156 // Driver implements libvirt.DiskInfo. 157 func (d diskInfo) Driver() string { 158 return d.driver 159 } 160 161 // Source implements libvirt.Source. 162 func (d diskInfo) Source() string { 163 return d.source 164 } 165 166 // CreateMachine creates a virtual machine and starts it. 167 func CreateMachine(params CreateMachineParams) error { 168 if params.Hostname == "" { 169 return fmt.Errorf("hostname is required") 170 } 171 172 setDefaults(¶ms) 173 174 templateDir := filepath.Dir(params.UserDataFile) 175 176 err := writeMetadata(templateDir) 177 if err != nil { 178 return errors.Annotate(err, "failed to write instance metadata") 179 } 180 181 dsPath, err := writeDataSourceVolume(params) 182 if err != nil { 183 return errors.Annotatef(err, "failed to write data source volume for %q", params.Host()) 184 } 185 186 imgPath, err := writeRootDisk(params) 187 if err != nil { 188 return errors.Annotatef(err, "failed to write root volume for %q", params.Host()) 189 } 190 191 params.disks = append(params.disks, diskInfo{source: imgPath, driver: "qcow2"}) 192 params.disks = append(params.disks, diskInfo{source: dsPath, driver: "raw"}) 193 194 domainPath, err := writeDomainXML(templateDir, params) 195 if err != nil { 196 return errors.Annotatef(err, "failed to write domain xml for %q", params.Host()) 197 } 198 199 out, err := params.runCmdAsRoot("", virsh, "define", domainPath) 200 if err != nil { 201 return errors.Annotatef(err, "failed to define the domain for %q from %s:%s", params.Host(), domainPath, out) 202 } 203 logger.Debugf("created domain: %s", out) 204 205 out, err = params.runCmdAsRoot("", virsh, "start", params.Host()) 206 if err != nil { 207 return errors.Annotatef(err, "failed to start domain %q:%s", params.Host(), out) 208 } 209 logger.Debugf("started domain: %s", out) 210 211 return err 212 } 213 214 // Setup the default values for params. 215 func setDefaults(p *CreateMachineParams) { 216 if p.findPath == nil { 217 p.findPath = paths.DataDir 218 } 219 if p.runCmd == nil { 220 p.runCmd = runAsLibvirt 221 } 222 if p.runCmdAsRoot == nil { 223 p.runCmdAsRoot = run 224 } 225 } 226 227 // DestroyMachine destroys the virtual machine represented by the kvmContainer. 228 func DestroyMachine(c *kvmContainer) error { 229 if c.runCmd == nil { 230 c.runCmd = run 231 } 232 if c.pathfinder == nil { 233 c.pathfinder = paths.DataDir 234 } 235 236 // We don't return errors for virsh commands because it is possible that we 237 // didn't succeed in creating the domain. Additionally, we want all the 238 // commands to run. If any fail it is certainly because the thing we're 239 // trying to remove wasn't created. However, we still want to try removing 240 // all the parts. The exception here is getting the guestBase, if that 241 // fails we return the error because we cannot continue without it. 242 243 _, err := c.runCmd("", virsh, "destroy", c.Name()) 244 if err != nil { 245 logger.Infof("`%s destroy %s` failed: %q", virsh, c.Name(), err) 246 } 247 248 // The nvram flag here removes the pflash drive for us. There is also a 249 // `remove-all-storage` flag, but it is unclear if that would also remove 250 // the backing store which we don't want to do. So we remove those manually 251 // after undefining. 252 _, err = c.runCmd("", virsh, "undefine", "--nvram", c.Name()) 253 if err != nil { 254 logger.Infof("`%s undefine --nvram %s` failed: %q", virsh, c.Name(), err) 255 } 256 guestBase, err := guestPath(c.pathfinder) 257 if err != nil { 258 return errors.Trace(err) 259 } 260 err = os.Remove(filepath.Join(guestBase, fmt.Sprintf("%s.qcow", c.Name()))) 261 if err != nil { 262 logger.Errorf("failed to remove system disk for %q: %s", c.Name(), err) 263 } 264 err = os.Remove(filepath.Join(guestBase, fmt.Sprintf("%s-ds.iso", c.Name()))) 265 if err != nil { 266 logger.Errorf("failed to remove cloud-init data disk for %q: %s", c.Name(), err) 267 } 268 269 return nil 270 } 271 272 // AutostartMachine indicates that the virtual machines should automatically 273 // restart when the host restarts. 274 func AutostartMachine(c *kvmContainer) error { 275 if c.runCmd == nil { 276 c.runCmd = run 277 } 278 _, err := c.runCmd("", virsh, "autostart", c.Name()) 279 return errors.Annotatef(err, "failed to autostart domain %q", c.Name()) 280 } 281 282 // ListMachines returns a map of machine name to state, where state is one of: 283 // running, idle, paused, shutdown, shut off, crashed, dying, pmsuspended. 284 func ListMachines(runCmd runFunc) (map[string]string, error) { 285 if runCmd == nil { 286 runCmd = run 287 } 288 289 output, err := runCmd("", virsh, "-q", "list", "--all") 290 if err != nil { 291 return nil, err 292 } 293 // Split the output into lines. 294 // Regex matching is the easiest way to match the lines. 295 // id hostname status 296 // separated by whitespace, with whitespace at the start too. 297 result := make(map[string]string) 298 for _, s := range machineListPattern.FindAllStringSubmatchIndex(output, -1) { 299 hostnameAndStatus := machineListPattern.ExpandString(nil, "$hostname $status", output, s) 300 parts := strings.SplitN(string(hostnameAndStatus), " ", 2) 301 result[parts[0]] = parts[1] 302 } 303 return result, nil 304 } 305 306 // guestPath returns the path to the guest directory from the given 307 // pathfinder. 308 func guestPath(pathfinder pathfinderFunc) (string, error) { 309 baseDir := pathfinder(paths.CurrentOS()) 310 return filepath.Join(baseDir, kvm, guestDir), nil 311 } 312 313 // writeDataSourceVolume creates a data source image for cloud init. 314 func writeDataSourceVolume(params CreateMachineParams) (string, error) { 315 templateDir := filepath.Dir(params.UserDataFile) 316 317 if err := writeMetadata(templateDir); err != nil { 318 return "", errors.Trace(err) 319 } 320 321 if err := writeNetworkConfig(params, templateDir); err != nil { 322 return "", errors.Trace(err) 323 } 324 325 // Creating a working DS volume was a bit troublesome for me. I finally 326 // found the details in the docs. 327 // http://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html 328 // 329 // The arguments passed to create the DS volume for NoCloud must be 330 // `user-data` and `meta-data`. So the `cloud-init` file we generate won't 331 // work. Also, they must be exactly `user-data` and `meta-data` with no 332 // path beforehand, so `$JUJUDIR/containers/juju-someid-0/user-data` also 333 // fails. 334 // 335 // Furthermore, symlinks aren't followed by NoCloud. So we rename our 336 // cloud-init file to user-data. We could change the output name in 337 // juju/cloudconfig/containerinit/container_userdata.go:WriteUserData but 338 // who knows what that will break. 339 userDataPath := filepath.Join(templateDir, userdata) 340 if err := os.Rename(params.UserDataFile, userDataPath); err != nil { 341 return "", errors.Trace(err) 342 } 343 344 // Create data the source volume outputting the iso image to the guests 345 // (AKA libvirt storage pool) directory. 346 guestBase, err := guestPath(params.findPath) 347 if err != nil { 348 return "", errors.Trace(err) 349 } 350 dsPath := filepath.Join(guestBase, fmt.Sprintf("%s-ds.iso", params.Host())) 351 352 // Use the template path as the working directory. 353 // This allows us to run the command with user-data and meta-data as 354 // relative paths to appease the NoCloud script. 355 out, err := params.runCmd( 356 templateDir, 357 "genisoimage", 358 "-output", dsPath, 359 "-volid", "cidata", 360 "-joliet", "-rock", 361 userdata, 362 metadata, 363 networkconfig) 364 if err != nil { 365 return "", errors.Trace(err) 366 } 367 logger.Debugf("create ds image: %s", out) 368 369 return dsPath, nil 370 } 371 372 // writeDomainXML writes out the configuration required to create a new guest 373 // domain. 374 func writeDomainXML(templateDir string, p CreateMachineParams) (string, error) { 375 domainPath := filepath.Join(templateDir, fmt.Sprintf("%s.xml", p.Host())) 376 dom, err := libvirt.NewDomain(p) 377 if err != nil { 378 return "", errors.Trace(err) 379 } 380 381 ml, err := xml.MarshalIndent(&dom, "", " ") 382 if err != nil { 383 return "", errors.Trace(err) 384 } 385 386 f, err := os.Create(domainPath) 387 if err != nil { 388 return "", errors.Trace(err) 389 } 390 defer func() { 391 err = f.Close() 392 if err != nil { 393 logger.Debugf("failed defer %q", errors.Trace(err)) 394 } 395 }() 396 397 _, err = f.Write(ml) 398 if err != nil { 399 return "", errors.Trace(err) 400 } 401 402 return domainPath, nil 403 } 404 405 // writeMetadata writes out a metadata file with an UUID instance-id. The 406 // meta-data file is used in the data source image along with user-data nee 407 // cloud-init. `instance-id` is a required field in meta-data. It is what is 408 // used to determine if this is the first boot, thereby whether or not to run 409 // cloud-init. 410 // See: http://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html 411 func writeMetadata(dir string) error { 412 data := fmt.Sprintf(`{"instance-id": "%s"}`, utils.MustNewUUID()) 413 f, err := os.Create(filepath.Join(dir, metadata)) 414 if err != nil { 415 return errors.Trace(err) 416 } 417 defer func() { 418 if err = f.Close(); err != nil { 419 logger.Errorf("failed to close %q %s", f.Name(), err) 420 } 421 }() 422 _, err = f.WriteString(data) 423 if err != nil { 424 return errors.Trace(err) 425 } 426 return nil 427 } 428 429 func writeNetworkConfig(params CreateMachineParams, dir string) error { 430 f, err := os.Create(filepath.Join(dir, networkconfig)) 431 if err != nil { 432 return errors.Trace(err) 433 } 434 defer func() { 435 if err = f.Close(); err != nil { 436 logger.Errorf("failed to close %q %s", f.Name(), err) 437 } 438 }() 439 _, err = f.WriteString(params.NetworkConfigData) 440 if err != nil { 441 return errors.Trace(err) 442 } 443 return nil 444 } 445 446 // writeRootDisk writes out the root disk for the container. This creates a 447 // system disk backed by our shared series/arch backing store. 448 func writeRootDisk(params CreateMachineParams) (string, error) { 449 guestBase, err := guestPath(params.findPath) 450 if err != nil { 451 return "", errors.Trace(err) 452 } 453 imgPath := filepath.Join(guestBase, fmt.Sprintf("%s.qcow", params.Host())) 454 backingPath := filepath.Join( 455 guestBase, 456 backingFileName(params.Version, params.Arch())) 457 458 cmdArgs := []string{ 459 "create", 460 "-b", backingPath, 461 } 462 463 // Contrary to their extension, the backing files fetched via 464 // simple stream are raw and not qcow2 images. 465 cmdArgs = append(cmdArgs, "-F", "raw") 466 467 cmdArgs = append(cmdArgs, 468 "-f", "qcow2", 469 imgPath, 470 fmt.Sprintf("%dG", params.RootDisk), 471 ) 472 473 out, err := params.runCmd("", "qemu-img", cmdArgs...) 474 logger.Debugf("create root image: %s", out) 475 if err != nil { 476 return "", errors.Trace(err) 477 } 478 479 return imgPath, nil 480 } 481 482 // pool info parses and returns the output of `virsh pool-info <poolname>`. 483 func poolInfo(runCmd runFunc) (*libvirtPool, error) { 484 output, err := runCmd("", virsh, "pool-info", poolName) 485 if err != nil { 486 logger.Debugf("pool %q doesn't appear to exist: %s", poolName, err) 487 return nil, nil 488 } 489 490 p := &libvirtPool{} 491 err = yaml.Unmarshal([]byte(output), p) 492 if err != nil { 493 logger.Errorf("failed to unmarshal info %s", err) 494 return nil, errors.Trace(err) 495 } 496 return p, nil 497 } 498 499 // libvirtPool represents the guest pool information we care about. Additional 500 // fields are available but ignored here. 501 type libvirtPool struct { 502 Name string `yaml:"Name"` 503 State string `yaml:"State"` 504 Autostart string `yaml:"Autostart"` 505 }