github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/lxd/environ_broker.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  
    10  	"github.com/juju/errors"
    11  	"github.com/juju/utils/arch"
    12  
    13  	"github.com/juju/juju/cloudconfig/cloudinit"
    14  	"github.com/juju/juju/cloudconfig/instancecfg"
    15  	"github.com/juju/juju/cloudconfig/providerinit"
    16  	"github.com/juju/juju/container/lxd"
    17  	"github.com/juju/juju/core/instance"
    18  	"github.com/juju/juju/core/status"
    19  	"github.com/juju/juju/environs"
    20  	"github.com/juju/juju/environs/context"
    21  	"github.com/juju/juju/environs/instances"
    22  	"github.com/juju/juju/environs/tags"
    23  	"github.com/juju/juju/provider/common"
    24  	"github.com/juju/juju/tools"
    25  )
    26  
    27  // MaintainInstance is specified in the InstanceBroker interface.
    28  func (*environ) MaintainInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) error {
    29  	return nil
    30  }
    31  
    32  // StartInstance implements environs.InstanceBroker.
    33  func (env *environ) StartInstance(
    34  	ctx context.ProviderCallContext, args environs.StartInstanceParams,
    35  ) (*environs.StartInstanceResult, error) {
    36  	series := args.Tools.OneSeries()
    37  	logger.Debugf("StartInstance: %q, %s", args.InstanceConfig.MachineId, series)
    38  
    39  	arch, err := env.finishInstanceConfig(args)
    40  	if err != nil {
    41  		return nil, errors.Trace(err)
    42  	}
    43  
    44  	container, err := env.newContainer(ctx, args, arch)
    45  	if err != nil {
    46  		common.HandleCredentialError(IsAuthorisationFailure, err, ctx)
    47  		if args.StatusCallback != nil {
    48  			args.StatusCallback(status.ProvisioningError, err.Error(), nil)
    49  		}
    50  		return nil, errors.Trace(err)
    51  	}
    52  	logger.Infof("started instance %q", container.Name)
    53  	inst := newInstance(container, env)
    54  
    55  	// Build the result.
    56  	hwc := env.getHardwareCharacteristics(args, inst)
    57  	result := environs.StartInstanceResult{
    58  		Instance: inst,
    59  		Hardware: hwc,
    60  	}
    61  	return &result, nil
    62  }
    63  
    64  func (env *environ) finishInstanceConfig(args environs.StartInstanceParams) (string, error) {
    65  	arch := env.server.HostArch()
    66  	tools, err := args.Tools.Match(tools.Filter{Arch: arch})
    67  	if err != nil {
    68  		return "", errors.Trace(err)
    69  	}
    70  	if err := args.InstanceConfig.SetTools(tools); err != nil {
    71  		return "", errors.Trace(err)
    72  	}
    73  	if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, env.ecfg.Config); err != nil {
    74  		return "", errors.Trace(err)
    75  	}
    76  	return arch, nil
    77  }
    78  
    79  // newContainer is where the new physical instance is actually
    80  // provisioned, relative to the provided args and spec. Info for that
    81  // low-level instance is returned.
    82  func (env *environ) newContainer(
    83  	ctx context.ProviderCallContext,
    84  	args environs.StartInstanceParams,
    85  	arch string,
    86  ) (*lxd.Container, error) {
    87  	// Note: other providers have the ImageMetadata already read for them
    88  	// and passed in as args.ImageMetadata. However, lxd provider doesn't
    89  	// use datatype: image-ids, it uses datatype: image-download, and we
    90  	// don't have a registered cloud/region.
    91  	imageSources, err := env.getImageSources()
    92  	if err != nil {
    93  		return nil, errors.Trace(err)
    94  	}
    95  
    96  	// Keep track of StatusCallback output so we may clean up later.
    97  	// This is implemented here, close to where the StatusCallback calls
    98  	// are made, instead of at a higher level in the package, so as not to
    99  	// assume that all providers will have the same need to be implemented
   100  	// in the same way.
   101  	longestMsg := 0
   102  	statusCallback := func(currentStatus status.Status, msg string, data map[string]interface{}) error {
   103  		if args.StatusCallback != nil {
   104  			args.StatusCallback(currentStatus, msg, nil)
   105  		}
   106  		if len(msg) > longestMsg {
   107  			longestMsg = len(msg)
   108  		}
   109  		return nil
   110  	}
   111  	cleanupCallback := func() {
   112  		if args.CleanupCallback != nil {
   113  			args.CleanupCallback(strings.Repeat(" ", longestMsg))
   114  		}
   115  	}
   116  	defer cleanupCallback()
   117  
   118  	target, err := env.getTargetServer(ctx, args)
   119  	if err != nil {
   120  		return nil, errors.Trace(err)
   121  	}
   122  
   123  	image, err := target.FindImage(args.InstanceConfig.Series, arch, imageSources, true, statusCallback)
   124  	if err != nil {
   125  		return nil, errors.Trace(err)
   126  	}
   127  	cleanupCallback() // Clean out any long line of completed download status
   128  
   129  	cSpec, err := env.getContainerSpec(image, args)
   130  	if err != nil {
   131  		return nil, errors.Trace(err)
   132  	}
   133  
   134  	statusCallback(status.Allocating, "Creating container", nil)
   135  	container, err := target.CreateContainerFromSpec(cSpec)
   136  	if err != nil {
   137  		return nil, errors.Trace(err)
   138  	}
   139  	statusCallback(status.Running, "Container started", nil)
   140  	return container, nil
   141  }
   142  
   143  func (env *environ) getImageSources() ([]lxd.ServerSpec, error) {
   144  	metadataSources, err := environs.ImageMetadataSources(env)
   145  	if err != nil {
   146  		return nil, errors.Trace(err)
   147  	}
   148  	remotes := make([]lxd.ServerSpec, 0)
   149  	for _, source := range metadataSources {
   150  		url, err := source.URL("")
   151  		if err != nil {
   152  			logger.Debugf("failed to get the URL for metadataSource: %s", err)
   153  			continue
   154  		}
   155  		// NOTE(jam) LXD only allows you to pass HTTPS URLs. So strip
   156  		// off http:// and replace it with https://
   157  		// Arguably we could give the user a direct error if
   158  		// env.ImageMetadataURL is http instead of https, but we also
   159  		// get http from the DefaultImageSources, which is why we
   160  		// replace it.
   161  		// TODO(jam) Maybe we could add a Validate step that ensures
   162  		// image-metadata-url is an "https://" URL, so that Users get a
   163  		// "your configuration is wrong" error, rather than silently
   164  		// changing it and having them get confused.
   165  		// https://github.com/lxc/lxd/issues/1763
   166  		remotes = append(remotes, lxd.MakeSimpleStreamsServerSpec(source.Description(), url))
   167  	}
   168  	return remotes, nil
   169  }
   170  
   171  // getContainerSpec builds a container spec from the input container image and
   172  // start-up parameters.
   173  // Cloud-init config is generated based on the network devices in the default
   174  // profile and included in the spec config.
   175  func (env *environ) getContainerSpec(
   176  	image lxd.SourcedImage, args environs.StartInstanceParams,
   177  ) (lxd.ContainerSpec, error) {
   178  	hostname, err := env.namespace.Hostname(args.InstanceConfig.MachineId)
   179  	if err != nil {
   180  		return lxd.ContainerSpec{}, errors.Trace(err)
   181  	}
   182  	cSpec := lxd.ContainerSpec{
   183  		Name:     hostname,
   184  		Profiles: append([]string{"default", env.profileName()}, args.CharmLXDProfiles...),
   185  		Image:    image,
   186  		Config:   make(map[string]string),
   187  	}
   188  	cSpec.ApplyConstraints(args.Constraints)
   189  
   190  	cloudCfg, err := cloudinit.New(args.InstanceConfig.Series)
   191  	if err != nil {
   192  		return cSpec, errors.Trace(err)
   193  	}
   194  
   195  	// Check to see if there are any non-eth0 devices in the default profile.
   196  	// If there are, we need cloud-init to configure them, and we need to
   197  	// explicitly add them to the container spec.
   198  	nics, err := env.server.GetNICsFromProfile("default")
   199  	if err != nil {
   200  		return cSpec, errors.Trace(err)
   201  	}
   202  	if !(len(nics) == 1 && nics["eth0"] != nil) {
   203  		logger.Debugf("generating custom cloud-init networking")
   204  
   205  		cSpec.Config[lxd.NetworkConfigKey] = cloudinit.CloudInitNetworkConfigDisabled
   206  
   207  		info, err := lxd.InterfaceInfoFromDevices(nics)
   208  		if err != nil {
   209  			return cSpec, errors.Trace(err)
   210  		}
   211  		if err := cloudCfg.AddNetworkConfig(info); err != nil {
   212  			return cSpec, errors.Trace(err)
   213  		}
   214  
   215  		cSpec.Devices = nics
   216  	}
   217  
   218  	userData, err := providerinit.ComposeUserData(args.InstanceConfig, cloudCfg, lxdRenderer{})
   219  	if err != nil {
   220  		return cSpec, errors.Annotate(err, "composing user data")
   221  	}
   222  	logger.Debugf("LXD user data; %d bytes", len(userData))
   223  
   224  	// TODO(ericsnow) Looks like LXD does not handle gzipped userdata
   225  	// correctly.  It likely has to do with the HTTP transport, much
   226  	// as we have to b64encode the userdata for GCE.  Until that is
   227  	// resolved we simply pass the plain text.
   228  	//cfg[lxd.UserDataKey] = utils.Gzip(userData)
   229  	cSpec.Config[lxd.UserDataKey] = string(userData)
   230  
   231  	for k, v := range args.InstanceConfig.Tags {
   232  		if !strings.HasPrefix(k, tags.JujuTagPrefix) {
   233  			// Since some metadata is interpreted by LXD, we cannot allow
   234  			// arbitrary tags to be passed in by the user.
   235  			// We currently only pass through Juju-defined tags.
   236  			logger.Debugf("ignoring non-juju tag: %s=%s", k, v)
   237  			continue
   238  		}
   239  		cSpec.Config[lxd.UserNamespacePrefix+k] = v
   240  	}
   241  
   242  	return cSpec, nil
   243  }
   244  
   245  // getTargetServer checks to see if a valid zone was passed as a placement
   246  // directive in the start-up start-up arguments. If so, a server for the
   247  // specific node is returned.
   248  func (env *environ) getTargetServer(
   249  	ctx context.ProviderCallContext, args environs.StartInstanceParams,
   250  ) (Server, error) {
   251  	p, err := env.parsePlacement(ctx, args.Placement)
   252  	if err != nil {
   253  		return nil, errors.Trace(err)
   254  	}
   255  
   256  	if p.nodeName == "" {
   257  		return env.server, nil
   258  	}
   259  	return env.server.UseTargetServer(p.nodeName)
   260  }
   261  
   262  type lxdPlacement struct {
   263  	nodeName string
   264  }
   265  
   266  func (env *environ) parsePlacement(ctx context.ProviderCallContext, placement string) (*lxdPlacement, error) {
   267  	if placement == "" {
   268  		return &lxdPlacement{}, nil
   269  	}
   270  
   271  	var node string
   272  	pos := strings.IndexRune(placement, '=')
   273  	// Assume that a plain string is a node name.
   274  	if pos == -1 {
   275  		node = placement
   276  	} else {
   277  		if placement[:pos] != "zone" {
   278  			return nil, fmt.Errorf("unknown placement directive: %v", placement)
   279  		}
   280  		node = placement[pos+1:]
   281  	}
   282  
   283  	if node == "" {
   284  		return &lxdPlacement{}, nil
   285  	}
   286  
   287  	if err := common.ValidateAvailabilityZone(env, ctx, node); err != nil {
   288  		return nil, errors.Trace(err)
   289  	}
   290  
   291  	return &lxdPlacement{nodeName: node}, nil
   292  }
   293  
   294  // getHardwareCharacteristics compiles hardware-related details about
   295  // the given instance and relative to the provided spec and returns it.
   296  func (env *environ) getHardwareCharacteristics(
   297  	args environs.StartInstanceParams, inst *environInstance,
   298  ) *instance.HardwareCharacteristics {
   299  	container := inst.container
   300  
   301  	archStr := container.Arch()
   302  	if archStr == "unknown" || !arch.IsSupportedArch(archStr) {
   303  		archStr = env.server.HostArch()
   304  	}
   305  	cores := uint64(container.CPUs())
   306  	mem := uint64(container.Mem())
   307  	return &instance.HardwareCharacteristics{
   308  		Arch:     &archStr,
   309  		CpuCores: &cores,
   310  		Mem:      &mem,
   311  	}
   312  }
   313  
   314  // AllInstances implements environs.InstanceBroker.
   315  func (env *environ) AllInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) {
   316  	environInstances, err := env.allInstances()
   317  	instances := make([]instances.Instance, len(environInstances))
   318  	for i, inst := range environInstances {
   319  		if inst == nil {
   320  			continue
   321  		}
   322  		instances[i] = inst
   323  	}
   324  	return instances, errors.Trace(err)
   325  }
   326  
   327  // StopInstances implements environs.InstanceBroker.
   328  func (env *environ) StopInstances(ctx context.ProviderCallContext, instances ...instance.Id) error {
   329  	prefix := env.namespace.Prefix()
   330  	var names []string
   331  	for _, id := range instances {
   332  		name := string(id)
   333  		if strings.HasPrefix(name, prefix) {
   334  			names = append(names, name)
   335  		} else {
   336  			logger.Warningf("ignoring request to stop container %q - not in namespace %q", name, prefix)
   337  		}
   338  	}
   339  
   340  	err := env.server.RemoveContainers(names)
   341  	if err != nil {
   342  		common.HandleCredentialError(IsAuthorisationFailure, err, ctx)
   343  	}
   344  	return errors.Trace(err)
   345  }