github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/container/lxd/container.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lxd
     5  
     6  import (
     7  	"fmt"
     8  	"math"
     9  	"reflect"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/canonical/lxd/shared/api"
    14  	"github.com/canonical/lxd/shared/units"
    15  	"github.com/juju/clock"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/retry"
    18  
    19  	"github.com/juju/juju/core/arch"
    20  	"github.com/juju/juju/core/constraints"
    21  	"github.com/juju/juju/core/instance"
    22  	corenetwork "github.com/juju/juju/core/network"
    23  	"github.com/juju/juju/network"
    24  )
    25  
    26  const (
    27  	UserNamespacePrefix = "user."
    28  	UserDataKey         = UserNamespacePrefix + "user-data"
    29  	NetworkConfigKey    = UserNamespacePrefix + "network-config"
    30  	JujuModelKey        = UserNamespacePrefix + "juju-model"
    31  	AutoStartKey        = "boot.autostart"
    32  )
    33  
    34  // ContainerSpec represents the data required to create a new container.
    35  type ContainerSpec struct {
    36  	Architecture string
    37  	Name         string
    38  	Image        SourcedImage
    39  	Devices      map[string]device
    40  	Config       map[string]string
    41  	Profiles     []string
    42  	InstanceType string
    43  	VirtType     instance.VirtType
    44  }
    45  
    46  // ApplyConstraints applies the input constraints as valid LXD container
    47  // configuration to the container spec.
    48  // Note that we pass these through as supplied. If an instance type constraint
    49  // has been specified along with specific cores/mem constraints,
    50  // LXD behaviour is to override with the specific ones even when lower.
    51  func (c *ContainerSpec) ApplyConstraints(serverVersion string, cons constraints.Value) {
    52  	if cons.HasInstanceType() {
    53  		c.InstanceType = *cons.InstanceType
    54  	}
    55  	if cons.HasCpuCores() {
    56  		c.Config["limits.cpu"] = fmt.Sprintf("%d", *cons.CpuCores)
    57  	}
    58  	if cons.HasMem() {
    59  		c.Config["limits.memory"] = fmt.Sprintf("%dMiB", *cons.Mem)
    60  	}
    61  	if cons.HasArch() {
    62  		c.Architecture = *cons.Arch
    63  	}
    64  
    65  	if cons.HasRootDisk() || cons.HasRootDiskSource() {
    66  		// If we have a root disk and no source,
    67  		// assume that it must come from the default pool.
    68  		rootDiskSource := "default"
    69  		if cons.HasRootDiskSource() {
    70  			rootDiskSource = *cons.RootDiskSource
    71  		}
    72  
    73  		if c.Devices == nil {
    74  			c.Devices = map[string]map[string]string{}
    75  		}
    76  
    77  		c.Devices["root"] = map[string]string{
    78  			"type": "disk",
    79  			"pool": rootDiskSource,
    80  			"path": "/",
    81  		}
    82  
    83  		if cons.HasRootDisk() {
    84  			c.Devices["root"]["size"] = fmt.Sprintf("%dMiB", *cons.RootDisk)
    85  		}
    86  	}
    87  
    88  	if cons.HasVirtType() {
    89  
    90  		virtType, err := instance.ParseVirtType(*cons.VirtType)
    91  		if err != nil {
    92  			logger.Errorf("failed to parse virt-type constraint %q, ignoring err: %v", *cons.VirtType, err)
    93  		} else {
    94  			c.VirtType = virtType
    95  		}
    96  	}
    97  }
    98  
    99  // Container extends the upstream LXD container type.
   100  type Container struct {
   101  	api.Instance
   102  }
   103  
   104  // Metadata returns the value from container config for the input key.
   105  // Such values are stored with the "user" namespace prefix.
   106  func (c *Container) Metadata(key string) string {
   107  	return c.Config[UserNamespacePrefix+key]
   108  }
   109  
   110  // Arch returns the architecture of the container.
   111  func (c *Container) Arch() string {
   112  	return arch.NormaliseArch(c.Architecture)
   113  }
   114  
   115  // VirtType returns the virtualisation type of the container.
   116  func (c *Container) VirtType() instance.VirtType {
   117  	return instance.VirtType(c.Type)
   118  }
   119  
   120  // CPUs returns the configured limit for number of container CPU cores.
   121  // If unset, zero is returned.
   122  func (c *Container) CPUs() uint {
   123  	var cores uint
   124  	if v := c.Config["limits.cpu"]; v != "" {
   125  		_, err := fmt.Sscanf(v, "%d", &cores)
   126  		if err != nil {
   127  			logger.Errorf("failed to parse %q into uint, ignoring err: %s", v, err)
   128  		}
   129  	}
   130  	return cores
   131  }
   132  
   133  // Mem returns the configured limit for container memory in MiB.
   134  func (c *Container) Mem() uint {
   135  	v := c.Config["limits.memory"]
   136  	if v == "" {
   137  		return 0
   138  	}
   139  
   140  	bytes, err := units.ParseByteSizeString(v)
   141  	if err != nil {
   142  		logger.Errorf("failed to parse %q into bytes, ignoring err: %s", v, err)
   143  		return 0
   144  	}
   145  
   146  	const oneMiB = 1024 * 1024
   147  	mib := bytes / oneMiB
   148  	if mib > math.MaxUint32 {
   149  		logger.Errorf("byte string %q overflowed uint32, using max value", v)
   150  		return math.MaxUint32
   151  	}
   152  
   153  	return uint(mib)
   154  }
   155  
   156  // AddDisk modifies updates the container's devices map to represent a disk
   157  // device described by the input arguments.
   158  // If the device already exists, an error is returned.
   159  func (c *Container) AddDisk(name, path, source, pool string, readOnly bool) error {
   160  	if _, ok := c.Devices[name]; ok {
   161  		return errors.Errorf("container %q already has a device %q", c.Name, name)
   162  	}
   163  
   164  	if c.Devices == nil {
   165  		c.Devices = map[string]device{}
   166  	}
   167  	c.Devices[name] = map[string]string{
   168  		"path":   path,
   169  		"source": source,
   170  		"type":   "disk",
   171  	}
   172  	if pool != "" {
   173  		c.Devices[name]["pool"] = pool
   174  	}
   175  	if readOnly {
   176  		c.Devices[name]["readonly"] = "true"
   177  	}
   178  	return nil
   179  }
   180  
   181  // aliveStatuses is the list of status strings that indicate
   182  // a container is "alive".
   183  var aliveStatuses = []string{
   184  	api.Ready.String(),
   185  	api.Starting.String(),
   186  	api.Started.String(),
   187  	api.Running.String(),
   188  	api.Stopping.String(),
   189  	api.Stopped.String(),
   190  }
   191  
   192  // AliveContainers returns the list of containers based on the input namespace
   193  // prefixed that are in a status indicating they are "alive".
   194  func (s *Server) AliveContainers(prefix string) ([]Container, error) {
   195  	c, err := s.FilterContainers(prefix, aliveStatuses...)
   196  	return c, errors.Trace(err)
   197  }
   198  
   199  // FilterContainers retrieves the list of containers from the server and filters
   200  // them based on the input namespace prefix and any supplied statuses.
   201  func (s *Server) FilterContainers(prefix string, statuses ...string) ([]Container, error) {
   202  	instances, err := s.GetInstances(api.InstanceTypeAny)
   203  	if err != nil {
   204  		return nil, errors.Trace(err)
   205  	}
   206  
   207  	var results []Container
   208  	for _, c := range instances {
   209  		if prefix != "" && !strings.HasPrefix(c.Name, prefix) {
   210  			continue
   211  		}
   212  		if len(statuses) > 0 && !containerHasStatus(c, statuses) {
   213  			continue
   214  		}
   215  		results = append(results, Container{c})
   216  	}
   217  	return results, nil
   218  }
   219  
   220  // ContainerAddresses gets usable network addresses for the container
   221  // identified by the input name.
   222  func (s *Server) ContainerAddresses(name string) ([]corenetwork.ProviderAddress, error) {
   223  	state, _, err := s.GetInstanceState(name)
   224  	if err != nil {
   225  		return nil, errors.Trace(err)
   226  	}
   227  
   228  	networks := state.Network
   229  	if networks == nil {
   230  		return []corenetwork.ProviderAddress{}, nil
   231  	}
   232  
   233  	var results []corenetwork.ProviderAddress
   234  	for netName, net := range networks {
   235  		if netName == network.DefaultLXDBridge {
   236  			continue
   237  		}
   238  		for _, addr := range net.Addresses {
   239  			netAddr := corenetwork.NewMachineAddress(addr.Address).AsProviderAddress()
   240  			if netAddr.Scope == corenetwork.ScopeLinkLocal || netAddr.Scope == corenetwork.ScopeMachineLocal {
   241  				logger.Tracef("ignoring address %q for container %q", addr, name)
   242  				continue
   243  			}
   244  			results = append(results, netAddr)
   245  		}
   246  	}
   247  	return results, nil
   248  }
   249  
   250  // CreateContainerFromSpec creates a new container based on the input spec,
   251  // and starts it immediately.
   252  // If the container fails to be started, it is removed.
   253  // Upon successful creation and start, the container is returned.
   254  func (s *Server) CreateContainerFromSpec(spec ContainerSpec) (*Container, error) {
   255  	logger.Infof("starting new container %q (image %q)", spec.Name, spec.Image.Image.Filename)
   256  	logger.Debugf("new container has profiles %v", spec.Profiles)
   257  
   258  	ephemeral := false
   259  	req := api.InstancesPost{
   260  		Name:         spec.Name,
   261  		InstanceType: spec.InstanceType,
   262  		Type:         instance.NormaliseVirtType(spec.VirtType),
   263  		InstancePut: api.InstancePut{
   264  			Architecture: spec.Architecture,
   265  			Profiles:     spec.Profiles,
   266  			Devices:      spec.Devices,
   267  			Config:       spec.Config,
   268  			Ephemeral:    ephemeral,
   269  		},
   270  	}
   271  	op, err := s.CreateInstanceFromImage(spec.Image.LXDServer, *spec.Image.Image, req)
   272  	if err != nil {
   273  		return s.handleAlreadyExistsError(err, spec, ephemeral)
   274  	}
   275  
   276  	if err := op.Wait(); err != nil {
   277  		return s.handleAlreadyExistsError(err, spec, ephemeral)
   278  	}
   279  	opInfo, err := op.GetTarget()
   280  	if err != nil {
   281  		return s.handleAlreadyExistsError(err, spec, ephemeral)
   282  	}
   283  	if opInfo.StatusCode != api.Success {
   284  		return nil, fmt.Errorf("container creation failed: %s", opInfo.Err)
   285  	}
   286  
   287  	logger.Debugf("created container %q, waiting for start...", spec.Name)
   288  
   289  	if err := s.StartContainer(spec.Name); err != nil {
   290  		if remErr := s.RemoveContainer(spec.Name); remErr != nil {
   291  			logger.Errorf("failed to remove container after unsuccessful start: %s", remErr.Error())
   292  		}
   293  		return nil, errors.Trace(err)
   294  	}
   295  
   296  	container, _, err := s.GetInstance(spec.Name)
   297  	if err != nil {
   298  		return nil, errors.Trace(err)
   299  	}
   300  	return &Container{
   301  		Instance: *container,
   302  	}, nil
   303  }
   304  
   305  func (s *Server) handleAlreadyExistsError(err error, spec ContainerSpec, ephemeral bool) (*Container, error) {
   306  	if IsLXDAlreadyExists(err) {
   307  		container, runningErr := s.waitForRunningContainer(spec, ephemeral)
   308  		if runningErr != nil {
   309  			// It's actually more helpful to display the original error
   310  			// message, but we'll also log out what the new error message
   311  			// was, when attempting to wait for it.
   312  			logger.Debugf("waiting for container to be running: %v", runningErr)
   313  			return nil, errors.Trace(err)
   314  		}
   315  		c := Container{*container}
   316  		return &c, nil
   317  	}
   318  	return nil, errors.Trace(err)
   319  }
   320  
   321  func (s *Server) waitForRunningContainer(spec ContainerSpec, ephemeral bool) (*api.Instance, error) {
   322  	var container *api.Instance
   323  	err := retry.Call(retry.CallArgs{
   324  		Func: func() error {
   325  			var err error
   326  			container, _, err = s.GetInstance(spec.Name)
   327  			if err != nil {
   328  				return errors.Trace(err)
   329  			}
   330  
   331  			switch container.StatusCode {
   332  			case api.Running:
   333  				return nil
   334  			case api.Started, api.Starting, api.Success:
   335  				return errors.Errorf("waiting for container to be running")
   336  			default:
   337  				return errors.Errorf("waiting for container")
   338  			}
   339  		},
   340  		Attempts:    60,
   341  		MaxDuration: time.Minute * 5,
   342  		Delay:       time.Second * 10,
   343  		Clock:       s.clock,
   344  	})
   345  	if err != nil {
   346  		return nil, errors.Trace(err)
   347  	}
   348  	// Ensure that the container matches the spec we launched it with.
   349  	if matchesContainerSpec(container, spec, ephemeral) {
   350  		return container, nil
   351  	}
   352  	return nil, errors.Errorf("container %q does not match container spec", spec.Name)
   353  }
   354  
   355  func matchesContainerSpec(container *api.Instance, spec ContainerSpec, ephemeral bool) bool {
   356  	// If we don't match the spec from the container, then we're not
   357  	// sure what we've got here. Return the original error message.
   358  	return container.Architecture == spec.Architecture &&
   359  		container.Ephemeral == ephemeral &&
   360  		reflect.DeepEqual(container.Profiles, spec.Profiles) &&
   361  		reflect.DeepEqual(container.Devices, spec.Devices) &&
   362  		reflect.DeepEqual(container.Config, spec.Config)
   363  }
   364  
   365  // StartContainer starts the extant container identified by the input name.
   366  func (s *Server) StartContainer(name string) error {
   367  	req := api.InstanceStatePut{
   368  		Action:   "start",
   369  		Timeout:  -1,
   370  		Force:    false,
   371  		Stateful: false,
   372  	}
   373  	op, err := s.UpdateInstanceState(name, req, "")
   374  	if err != nil {
   375  		return errors.Trace(err)
   376  	}
   377  
   378  	return errors.Trace(op.Wait())
   379  }
   380  
   381  // Remove containers stops and deletes containers matching the input list of
   382  // names. Any failed removals are indicated in the returned error.
   383  func (s *Server) RemoveContainers(names []string) error {
   384  	if len(names) == 0 {
   385  		return nil
   386  	}
   387  
   388  	var failed []string
   389  	for _, name := range names {
   390  		if err := s.RemoveContainer(name); err != nil {
   391  			failed = append(failed, name)
   392  			logger.Errorf("removing container %q: %v", name, err)
   393  		}
   394  	}
   395  	if len(failed) != 0 {
   396  		return errors.Errorf("failed to remove containers: %s", strings.Join(failed, ", "))
   397  	}
   398  	return nil
   399  }
   400  
   401  // Remove container first ensures that the container is stopped,
   402  // then deletes it.
   403  func (s *Server) RemoveContainer(name string) error {
   404  	state, eTag, err := s.GetInstanceState(name)
   405  	if err != nil {
   406  		return errors.Trace(err)
   407  	}
   408  
   409  	if state.StatusCode != api.Stopped {
   410  		req := api.InstanceStatePut{
   411  			Action:   "stop",
   412  			Timeout:  -1,
   413  			Force:    true,
   414  			Stateful: false,
   415  		}
   416  		op, err := s.UpdateInstanceState(name, req, eTag)
   417  		if err != nil {
   418  			return errors.Trace(err)
   419  		}
   420  		if err := op.Wait(); err != nil {
   421  			return errors.Trace(err)
   422  		}
   423  	}
   424  
   425  	// NOTE(achilleasa): the (apt) lxd version that ships with bionic
   426  	// does not automatically remove veth devices if attached to an OVS
   427  	// bridge. The operator must manually remove these devices from the
   428  	// bridge by running "ovs-vsctl --if-exists del-port X". This issue
   429  	// has been fixed in newer lxd versions.
   430  
   431  	// LXD has issues deleting containers, even if they've been stopped. The
   432  	// general advice passed back from the LXD team is to retry it again, to
   433  	// see if this helps clean up the containers.
   434  	// ZFS exacerbates this more for the LXD setup, but there is no way to
   435  	// know as the LXD client doesn't return typed errors.
   436  	retryArgs := retry.CallArgs{
   437  		Clock: s.Clock(),
   438  		IsFatalError: func(err error) bool {
   439  			return errors.IsBadRequest(err)
   440  		},
   441  		Func: func() error {
   442  			op, err := s.DeleteInstance(name)
   443  			if err != nil {
   444  				// sigh, LXD not found container - it's been deleted so, we
   445  				// just need to return nil.
   446  				if IsLXDNotFound(errors.Cause(err)) {
   447  					return nil
   448  				}
   449  				return errors.BadRequestf(err.Error())
   450  			}
   451  			return errors.Trace(op.Wait())
   452  		},
   453  		Delay:    2 * time.Second,
   454  		Attempts: 3,
   455  	}
   456  	if err := retry.Call(retryArgs); err != nil {
   457  		return errors.Trace(errors.Cause(err))
   458  	}
   459  	return nil
   460  }
   461  
   462  // WriteContainer writes the current representation of the input container to
   463  // the server.
   464  func (s *Server) WriteContainer(c *Container) error {
   465  	resp, err := s.UpdateInstance(c.Name, c.Writable(), "")
   466  	if err != nil {
   467  		return errors.Trace(err)
   468  	}
   469  	if err := resp.Wait(); err != nil {
   470  		return errors.Trace(err)
   471  	}
   472  	return nil
   473  }
   474  
   475  func (s *Server) Clock() clock.Clock {
   476  	if s.clock == nil {
   477  		return clock.WallClock
   478  	}
   479  	return s.clock
   480  }
   481  
   482  // containerHasStatus returns true if the input container has a status
   483  // matching one from the input list.
   484  func containerHasStatus(container api.Instance, statuses []string) bool {
   485  	for _, status := range statuses {
   486  		if container.StatusCode.String() == status {
   487  			return true
   488  		}
   489  	}
   490  	return false
   491  }