github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/container/lxd/manager.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lxd
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/loggo"
    13  	jujuarch "github.com/juju/utils/arch"
    14  	"github.com/lxc/lxd/shared/api"
    15  	"gopkg.in/juju/charm.v6"
    16  
    17  	"github.com/juju/juju/cloudconfig/cloudinit"
    18  	"github.com/juju/juju/cloudconfig/containerinit"
    19  	"github.com/juju/juju/cloudconfig/instancecfg"
    20  	"github.com/juju/juju/container"
    21  	"github.com/juju/juju/core/constraints"
    22  	"github.com/juju/juju/core/instance"
    23  	"github.com/juju/juju/core/status"
    24  	"github.com/juju/juju/environs"
    25  	"github.com/juju/juju/environs/config"
    26  	"github.com/juju/juju/environs/imagemetadata"
    27  	"github.com/juju/juju/environs/instances"
    28  	"github.com/juju/juju/network"
    29  )
    30  
    31  var (
    32  	logger = loggo.GetLogger("juju.container.lxd")
    33  )
    34  
    35  const lxdDefaultProfileName = "default"
    36  
    37  type containerManager struct {
    38  	server *Server
    39  
    40  	modelUUID        string
    41  	namespace        instance.Namespace
    42  	availabilityZone string
    43  
    44  	imageMetadataURL string
    45  	imageStream      string
    46  	imageMutex       sync.Mutex
    47  }
    48  
    49  // containerManager implements container.Manager.
    50  var _ container.Manager = (*containerManager)(nil)
    51  
    52  // NewContainerManager creates the entity that knows how to create and manage
    53  // LXD containers.
    54  // TODO(jam): This needs to grow support for things like LXC's ImageURLGetter
    55  // functionality.
    56  func NewContainerManager(cfg container.ManagerConfig, svr *Server) (container.Manager, error) {
    57  	modelUUID := cfg.PopValue(container.ConfigModelUUID)
    58  	if modelUUID == "" {
    59  		return nil, errors.Errorf("model UUID is required")
    60  	}
    61  	namespace, err := instance.NewNamespace(modelUUID)
    62  	if err != nil {
    63  		return nil, errors.Trace(err)
    64  	}
    65  
    66  	availabilityZone := cfg.PopValue(container.ConfigAvailabilityZone)
    67  	if availabilityZone == "" {
    68  		logger.Infof("Availability zone will be empty for this container manager")
    69  	}
    70  
    71  	imageMetaDataURL := cfg.PopValue(config.ContainerImageMetadataURLKey)
    72  	imageStream := cfg.PopValue(config.ContainerImageStreamKey)
    73  
    74  	cfg.WarnAboutUnused()
    75  	return &containerManager{
    76  		server:           svr,
    77  		modelUUID:        modelUUID,
    78  		namespace:        namespace,
    79  		availabilityZone: availabilityZone,
    80  		imageMetadataURL: imageMetaDataURL,
    81  		imageStream:      imageStream,
    82  	}, nil
    83  }
    84  
    85  // Namespace implements container.Manager.
    86  func (m *containerManager) Namespace() instance.Namespace {
    87  	return m.namespace
    88  }
    89  
    90  // DestroyContainer implements container.Manager.
    91  func (m *containerManager) DestroyContainer(id instance.Id) error {
    92  	return errors.Trace(m.server.RemoveContainer(string(id)))
    93  }
    94  
    95  // CreateContainer implements container.Manager.
    96  func (m *containerManager) CreateContainer(
    97  	instanceConfig *instancecfg.InstanceConfig,
    98  	cons constraints.Value,
    99  	series string,
   100  	networkConfig *container.NetworkConfig,
   101  	storageConfig *container.StorageConfig,
   102  	callback environs.StatusCallbackFunc,
   103  ) (instances.Instance, *instance.HardwareCharacteristics, error) {
   104  	callback(status.Provisioning, "Creating container spec", nil)
   105  	spec, err := m.getContainerSpec(instanceConfig, cons, series, networkConfig, storageConfig, callback)
   106  	if err != nil {
   107  		callback(status.ProvisioningError, fmt.Sprintf("Creating container spec: %v", err), nil)
   108  		return nil, nil, errors.Trace(err)
   109  	}
   110  
   111  	callback(status.Provisioning, "Creating container", nil)
   112  	c, err := m.server.CreateContainerFromSpec(spec)
   113  	if err != nil {
   114  		callback(status.ProvisioningError, fmt.Sprintf("Creating container: %v", err), nil)
   115  		return nil, nil, errors.Trace(err)
   116  	}
   117  	callback(status.Running, "Container started", nil)
   118  
   119  	return &lxdInstance{c.Name, m.server.ContainerServer},
   120  		&instance.HardwareCharacteristics{AvailabilityZone: &m.availabilityZone}, nil
   121  }
   122  
   123  // ListContainers implements container.Manager.
   124  func (m *containerManager) ListContainers() ([]instances.Instance, error) {
   125  	containers, err := m.server.FilterContainers(m.namespace.Prefix())
   126  	if err != nil {
   127  		return nil, errors.Trace(err)
   128  	}
   129  
   130  	var result []instances.Instance
   131  	for _, i := range containers {
   132  		result = append(result, &lxdInstance{i.Name, m.server.ContainerServer})
   133  	}
   134  	return result, nil
   135  }
   136  
   137  // IsInitialized implements container.Manager.
   138  func (m *containerManager) IsInitialized() bool {
   139  	return m.server != nil
   140  }
   141  
   142  // getContainerSpec generates a spec for creating a new container.
   143  // It sources an image based on the input series, and transforms the input
   144  // config objects into LXD configuration, including cloud init user data.
   145  func (m *containerManager) getContainerSpec(
   146  	instanceConfig *instancecfg.InstanceConfig,
   147  	cons constraints.Value,
   148  	series string,
   149  	networkConfig *container.NetworkConfig,
   150  	storageConfig *container.StorageConfig,
   151  	callback environs.StatusCallbackFunc,
   152  ) (ContainerSpec, error) {
   153  	imageSources, err := m.getImageSources()
   154  	if err != nil {
   155  		return ContainerSpec{}, errors.Trace(err)
   156  	}
   157  
   158  	// Lock around finding an image.
   159  	// The provisioner works concurrently to create containers.
   160  	// If an image needs to be copied from a remote, we don't many goroutines
   161  	// attempting to do it at once.
   162  	m.imageMutex.Lock()
   163  	found, err := m.server.FindImage(series, jujuarch.HostArch(), imageSources, true, callback)
   164  	m.imageMutex.Unlock()
   165  	if err != nil {
   166  		return ContainerSpec{}, errors.Annotatef(err, "acquiring LXD image")
   167  	}
   168  
   169  	name, err := m.namespace.Hostname(instanceConfig.MachineId)
   170  	if err != nil {
   171  		return ContainerSpec{}, errors.Trace(err)
   172  	}
   173  
   174  	nics, unknown, err := m.networkDevicesFromConfig(networkConfig)
   175  	if err != nil {
   176  		return ContainerSpec{}, errors.Trace(err)
   177  	}
   178  
   179  	logger.Debugf("configuring container %q with network devices: %v", name, nics)
   180  
   181  	// If the default LXD bridge was supplied in network config,
   182  	// but without a CIDR, attempt to ensure it is configured for IPv4.
   183  	// If there are others with incomplete info, log a warning.
   184  	if len(unknown) > 0 {
   185  		if len(unknown) == 1 && unknown[0] == network.DefaultLXDBridge && m.server.networkAPISupport {
   186  			mod, err := m.server.EnsureIPv4(network.DefaultLXDBridge)
   187  			if err != nil {
   188  				return ContainerSpec{}, errors.Annotate(err, "ensuring default bridge IPv4 config")
   189  			}
   190  			if mod {
   191  				logger.Infof(`added "auto" IPv4 configuration to default LXD bridge`)
   192  			}
   193  		} else {
   194  			logger.Warningf("no CIDR was detected for the following networks: %v", unknown)
   195  		}
   196  	}
   197  
   198  	// If there was no incoming interface info, then at this point we know
   199  	// that nics were generated by falling back to either a single "eth0",
   200  	// or devices from the profile.
   201  	// Ensure that the devices are represented in the cloud-init user-data.
   202  	if len(networkConfig.Interfaces) == 0 {
   203  		interfaces, err := InterfaceInfoFromDevices(nics)
   204  		if err != nil {
   205  			return ContainerSpec{}, errors.Trace(err)
   206  		}
   207  		networkConfig.Interfaces = interfaces
   208  	}
   209  
   210  	// CloudInitUserData creates our own ENI/netplan.
   211  	// We need to disable cloud-init networking to make it work.
   212  	userData, err := containerinit.CloudInitUserData(instanceConfig, networkConfig)
   213  	if err != nil {
   214  		return ContainerSpec{}, errors.Trace(err)
   215  	}
   216  
   217  	cfg := map[string]string{
   218  		UserDataKey:      string(userData),
   219  		NetworkConfigKey: cloudinit.CloudInitNetworkConfigDisabled,
   220  		AutoStartKey:     "true",
   221  		// Extra info to indicate the origin of this container.
   222  		JujuModelKey: m.modelUUID,
   223  	}
   224  
   225  	spec := ContainerSpec{
   226  		Name:     name,
   227  		Image:    found,
   228  		Config:   cfg,
   229  		Profiles: instanceConfig.Profiles,
   230  		Devices:  nics,
   231  	}
   232  	spec.ApplyConstraints(cons)
   233  
   234  	return spec, nil
   235  }
   236  
   237  // getImageSources returns a list of LXD remote image sources based on the
   238  // configuration that was passed into the container manager.
   239  func (m *containerManager) getImageSources() ([]ServerSpec, error) {
   240  	imURL := m.imageMetadataURL
   241  
   242  	// Unless the configuration explicitly requests the daily stream,
   243  	// an empty image metadata URL results in a search of the default sources.
   244  	if imURL == "" && m.imageStream != "daily" {
   245  		logger.Debugf("checking default image metadata sources")
   246  		return []ServerSpec{CloudImagesRemote, CloudImagesDailyRemote}, nil
   247  	}
   248  	// Otherwise only check the daily stream.
   249  	if imURL == "" {
   250  		return []ServerSpec{CloudImagesDailyRemote}, nil
   251  	}
   252  
   253  	imURL, err := imagemetadata.ImageMetadataURL(imURL, m.imageStream)
   254  	if err != nil {
   255  		return nil, errors.Annotatef(err, "generating image metadata source")
   256  	}
   257  	imURL = EnsureHTTPS(imURL)
   258  	remote := ServerSpec{
   259  		Name:     strings.Replace(imURL, "https://", "", 1),
   260  		Host:     imURL,
   261  		Protocol: SimpleStreamsProtocol,
   262  	}
   263  
   264  	// If the daily stream was configured with custom image metadata URL,
   265  	// only use the Ubuntu daily as a fallback.
   266  	if m.imageStream == "daily" {
   267  		return []ServerSpec{remote, CloudImagesDailyRemote}, nil
   268  	}
   269  	return []ServerSpec{remote, CloudImagesRemote, CloudImagesDailyRemote}, nil
   270  }
   271  
   272  // networkDevicesFromConfig uses the input container network configuration to
   273  // create a map of network device configuration in the LXD format.
   274  // If there are no interfaces in the input config, but there is a bridge device
   275  // name, return a single "eth0" device with the bridge as its parent.
   276  // The last fall-back is to return the NIC devices from the default profile.
   277  // Names for any networks without a known CIDR are returned in a slice.
   278  func (m *containerManager) networkDevicesFromConfig(netConfig *container.NetworkConfig) (map[string]device, []string, error) {
   279  	if len(netConfig.Interfaces) > 0 {
   280  		return DevicesFromInterfaceInfo(netConfig.Interfaces)
   281  	} else if netConfig.Device != "" {
   282  		return map[string]device{
   283  			"eth0": newNICDevice("eth0", netConfig.Device, network.GenerateVirtualMACAddress(), netConfig.MTU),
   284  		}, nil, nil
   285  	}
   286  
   287  	nics, err := m.server.GetNICsFromProfile(lxdDefaultProfileName)
   288  	return nics, nil, errors.Trace(err)
   289  }
   290  
   291  // MaybeWriteLXDProfile implements container.LXDProfileManager.
   292  func (m *containerManager) MaybeWriteLXDProfile(pName string, put *charm.LXDProfile) error {
   293  	hasProfile, err := m.server.HasProfile(pName)
   294  	if err != nil {
   295  		return errors.Trace(err)
   296  	}
   297  	if hasProfile {
   298  		logger.Debugf("lxd profile %q already exists, not written again", pName)
   299  		return nil
   300  	}
   301  	post := api.ProfilesPost{
   302  		Name:       pName,
   303  		ProfilePut: api.ProfilePut(*put),
   304  	}
   305  	if err = m.server.CreateProfile(post); err != nil {
   306  		return errors.Trace(err)
   307  	}
   308  	logger.Debugf("wrote lxd profile %q", pName)
   309  	return nil
   310  }
   311  
   312  // LXDProfileNames implements container.LXDProfileManager
   313  func (m *containerManager) LXDProfileNames(containerName string) ([]string, error) {
   314  	return m.server.GetContainerProfiles(containerName)
   315  }
   316  
   317  // ReplaceOrAddLXDProfile implements environs.LXDProfiler.
   318  func (m *containerManager) ReplaceOrAddInstanceProfile(instId, oldProfile, newProfile string, put *charm.LXDProfile) ([]string, error) {
   319  	if put != nil {
   320  		if err := m.MaybeWriteLXDProfile(newProfile, put); err != nil {
   321  			return []string{}, errors.Trace(err)
   322  		}
   323  	}
   324  	if err := m.server.ReplaceOrAddContainerProfile(instId, oldProfile, newProfile); err != nil {
   325  		return []string{}, errors.Trace(err)
   326  	}
   327  	if oldProfile != "" {
   328  		if err := m.server.DeleteProfile(oldProfile); err != nil {
   329  			// most likely the failure is because the profile is already in use
   330  			logger.Debugf("failed to delete profile %q: %s", oldProfile, err)
   331  		}
   332  	}
   333  	return m.LXDProfileNames(instId)
   334  }