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(&params)
   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  }