github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/provider/lxd/environ_broker.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // +build go1.3
     5  
     6  package lxd
     7  
     8  import (
     9  	"strings"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/utils/arch"
    13  	lxdshared "github.com/lxc/lxd/shared"
    14  
    15  	"github.com/juju/juju/cloudconfig/cloudinit"
    16  	"github.com/juju/juju/cloudconfig/instancecfg"
    17  	"github.com/juju/juju/cloudconfig/providerinit"
    18  	"github.com/juju/juju/environs"
    19  	"github.com/juju/juju/environs/tags"
    20  	"github.com/juju/juju/instance"
    21  	"github.com/juju/juju/state/multiwatcher"
    22  	"github.com/juju/juju/status"
    23  	"github.com/juju/juju/tools"
    24  	"github.com/juju/juju/tools/lxdclient"
    25  )
    26  
    27  func isController(icfg *instancecfg.InstanceConfig) bool {
    28  	return multiwatcher.AnyJobNeedsState(icfg.Jobs...)
    29  }
    30  
    31  // MaintainInstance is specified in the InstanceBroker interface.
    32  func (*environ) MaintainInstance(args environs.StartInstanceParams) error {
    33  	return nil
    34  }
    35  
    36  // StartInstance implements environs.InstanceBroker.
    37  func (env *environ) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) {
    38  	// Start a new instance.
    39  
    40  	series := args.Tools.OneSeries()
    41  	logger.Debugf("StartInstance: %q, %s", args.InstanceConfig.MachineId, series)
    42  
    43  	if err := env.finishInstanceConfig(args); err != nil {
    44  		return nil, errors.Trace(err)
    45  	}
    46  
    47  	// TODO(ericsnow) Handle constraints?
    48  
    49  	raw, err := env.newRawInstance(args)
    50  	if err != nil {
    51  		if args.StatusCallback != nil {
    52  			args.StatusCallback(status.ProvisioningError, err.Error(), nil)
    53  		}
    54  		return nil, errors.Trace(err)
    55  	}
    56  	logger.Infof("started instance %q", raw.Name)
    57  	inst := newInstance(raw, env)
    58  
    59  	// Build the result.
    60  	hwc := env.getHardwareCharacteristics(args, inst)
    61  	result := environs.StartInstanceResult{
    62  		Instance: inst,
    63  		Hardware: hwc,
    64  	}
    65  	return &result, nil
    66  }
    67  
    68  func (env *environ) finishInstanceConfig(args environs.StartInstanceParams) error {
    69  	// TODO(natefinch): This is only correct so long as the lxd is running on
    70  	// the local machine.  If/when we support a remote lxd environment, we'll
    71  	// need to change this to match the arch of the remote machine.
    72  	tools, err := args.Tools.Match(tools.Filter{Arch: arch.HostArch()})
    73  	if err != nil {
    74  		return errors.Trace(err)
    75  	}
    76  	if err := args.InstanceConfig.SetTools(tools); err != nil {
    77  		return errors.Trace(err)
    78  	}
    79  
    80  	if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, env.ecfg.Config); err != nil {
    81  		return errors.Trace(err)
    82  	}
    83  
    84  	return nil
    85  }
    86  
    87  func (env *environ) getImageSources() ([]lxdclient.Remote, error) {
    88  	metadataSources, err := environs.ImageMetadataSources(env)
    89  	if err != nil {
    90  		return nil, errors.Trace(err)
    91  	}
    92  	remotes := make([]lxdclient.Remote, 0)
    93  	for _, source := range metadataSources {
    94  		url, err := source.URL("")
    95  		if err != nil {
    96  			logger.Debugf("failed to get the URL for metadataSource: %s", err)
    97  			continue
    98  		}
    99  		// NOTE(jam) LXD only allows you to pass HTTPS URLs. So strip
   100  		// off http:// and replace it with https://
   101  		// Arguably we could give the user a direct error if
   102  		// env.ImageMetadataURL is http instead of https, but we also
   103  		// get http from the DefaultImageSources, which is why we
   104  		// replace it.
   105  		// TODO(jam) Maybe we could add a Validate step that ensures
   106  		// image-metadata-url is an "https://" URL, so that Users get a
   107  		// "your configuration is wrong" error, rather than silently
   108  		// changing it and having them get confused.
   109  		// https://github.com/lxc/lxd/issues/1763
   110  		if strings.HasPrefix(url, "http://") {
   111  			url = strings.TrimPrefix(url, "http://")
   112  			url = "https://" + url
   113  			logger.Debugf("LXD requires https://, using: %s", url)
   114  		}
   115  		remotes = append(remotes, lxdclient.Remote{
   116  			Name:          source.Description(),
   117  			Host:          url,
   118  			Protocol:      lxdclient.SimplestreamsProtocol,
   119  			Cert:          nil,
   120  			ServerPEMCert: "",
   121  		})
   122  	}
   123  	return remotes, nil
   124  }
   125  
   126  // newRawInstance is where the new physical instance is actually
   127  // provisioned, relative to the provided args and spec. Info for that
   128  // low-level instance is returned.
   129  func (env *environ) newRawInstance(args environs.StartInstanceParams) (*lxdclient.Instance, error) {
   130  	hostname, err := env.namespace.Hostname(args.InstanceConfig.MachineId)
   131  	if err != nil {
   132  		return nil, errors.Trace(err)
   133  	}
   134  
   135  	// Note: other providers have the ImageMetadata already read for them
   136  	// and passed in as args.ImageMetadata. However, lxd provider doesn't
   137  	// use datatype: image-ids, it uses datatype: image-download, and we
   138  	// don't have a registered cloud/region.
   139  	imageSources, err := env.getImageSources()
   140  	if err != nil {
   141  		return nil, errors.Trace(err)
   142  	}
   143  
   144  	series := args.InstanceConfig.Series
   145  	// TODO(jam): We should get this information from EnsureImageExists, or
   146  	// something given to us from 'raw', not assume it ourselves.
   147  	image := "ubuntu-" + series
   148  	// TODO: support args.Constraints.Arch, we'll want to map from
   149  
   150  	// Keep track of StatusCallback output so we may clean up later.
   151  	// This is implemented here, close to where the StatusCallback calls
   152  	// are made, instead of at a higher level in the package, so as not to
   153  	// assume that all providers will have the same need to be implemented
   154  	// in the same way.
   155  	longestMsg := 0
   156  	statusCallback := func(currentStatus status.Status, msg string) {
   157  		if args.StatusCallback != nil {
   158  			args.StatusCallback(currentStatus, msg, nil)
   159  		}
   160  		if len(msg) > longestMsg {
   161  			longestMsg = len(msg)
   162  		}
   163  	}
   164  	cleanupCallback := func() {
   165  		if args.CleanupCallback != nil {
   166  			args.CleanupCallback(strings.Repeat(" ", longestMsg))
   167  		}
   168  	}
   169  	defer cleanupCallback()
   170  
   171  	imageCallback := func(copyProgress string) {
   172  		statusCallback(status.Allocating, copyProgress)
   173  	}
   174  	if err := env.raw.EnsureImageExists(series, imageSources, imageCallback); err != nil {
   175  		return nil, errors.Trace(err)
   176  	}
   177  	cleanupCallback() // Clean out any long line of completed download status
   178  
   179  	cloudcfg, err := cloudinit.New(series)
   180  	if err != nil {
   181  		return nil, errors.Trace(err)
   182  	}
   183  
   184  	var certificateFingerprint string
   185  	if args.InstanceConfig.Controller != nil {
   186  		// For controller machines, generate a certificate pair and write
   187  		// them to the instance's disk in a well-defined location, along
   188  		// with the server's certificate.
   189  		certPEM, keyPEM, err := lxdshared.GenerateMemCert(true)
   190  		if err != nil {
   191  			return nil, errors.Trace(err)
   192  		}
   193  		cert := lxdclient.NewCert(certPEM, keyPEM)
   194  		cert.Name = hostname
   195  
   196  		// We record the certificate's fingerprint in metadata, so we can
   197  		// remove the certificate along with the instance.
   198  		certificateFingerprint, err = cert.Fingerprint()
   199  		if err != nil {
   200  			return nil, errors.Trace(err)
   201  		}
   202  
   203  		if err := env.raw.AddCert(cert); err != nil {
   204  			return nil, errors.Annotatef(err, "adding certificate %q", cert.Name)
   205  		}
   206  		serverState, err := env.raw.ServerStatus()
   207  		if err != nil {
   208  			return nil, errors.Annotate(err, "getting server status")
   209  		}
   210  		cloudcfg.AddRunTextFile(clientCertPath, string(certPEM), 0600)
   211  		cloudcfg.AddRunTextFile(clientKeyPath, string(keyPEM), 0600)
   212  		cloudcfg.AddRunTextFile(serverCertPath, serverState.Environment.Certificate, 0600)
   213  	}
   214  
   215  	cloudcfg.SetAttr("hostname", hostname)
   216  	cloudcfg.SetAttr("manage_etc_hosts", true)
   217  
   218  	metadata, err := getMetadata(cloudcfg, args)
   219  	if err != nil {
   220  		return nil, errors.Trace(err)
   221  	}
   222  	if certificateFingerprint != "" {
   223  		metadata[metadataKeyCertificateFingerprint] = certificateFingerprint
   224  	}
   225  
   226  	// TODO(ericsnow) Use the env ID for the network name (instead of default)?
   227  	// TODO(ericsnow) Make the network name configurable?
   228  	// TODO(ericsnow) Support multiple networks?
   229  	// TODO(ericsnow) Use a different net interface name? Configurable?
   230  	instSpec := lxdclient.InstanceSpec{
   231  		Name:  hostname,
   232  		Image: image,
   233  		//Type:              spec.InstanceType.Name,
   234  		//Disks:             getDisks(spec, args.Constraints),
   235  		//NetworkInterfaces: []string{"ExternalNAT"},
   236  		Metadata: metadata,
   237  		Profiles: []string{
   238  			//TODO(wwitzel3) allow the user to specify lxc profiles to apply. This allows the
   239  			// user to setup any custom devices order config settings for their environment.
   240  			// Also we must ensure that a device with the parent: lxcbr0 exists in at least
   241  			// one of the profiles.
   242  			"default",
   243  			env.profileName(),
   244  		},
   245  		// Network is omitted (left empty).
   246  	}
   247  
   248  	logger.Infof("starting instance %q (image %q)...", instSpec.Name, instSpec.Image)
   249  
   250  	statusCallback(status.Allocating, "preparing image")
   251  	inst, err := env.raw.AddInstance(instSpec)
   252  	if err != nil {
   253  		return nil, errors.Trace(err)
   254  	}
   255  	statusCallback(status.Running, "container started")
   256  	return inst, nil
   257  }
   258  
   259  // getMetadata builds the raw "user-defined" metadata for the new
   260  // instance (relative to the provided args) and returns it.
   261  func getMetadata(cloudcfg cloudinit.CloudConfig, args environs.StartInstanceParams) (map[string]string, error) {
   262  	renderer := lxdRenderer{}
   263  	uncompressed, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, renderer)
   264  	if err != nil {
   265  		return nil, errors.Annotate(err, "cannot make user data")
   266  	}
   267  	logger.Debugf("LXD user data; %d bytes", len(uncompressed))
   268  
   269  	// TODO(ericsnow) Looks like LXD does not handle gzipped userdata
   270  	// correctly.  It likely has to do with the HTTP transport, much
   271  	// as we have to b64encode the userdata for GCE.  Until that is
   272  	// resolved we simply pass the plain text.
   273  	//compressed := utils.Gzip(compressed)
   274  	userdata := string(uncompressed)
   275  
   276  	metadata := map[string]string{
   277  		// store the cloud-config userdata for cloud-init.
   278  		metadataKeyCloudInit: userdata,
   279  	}
   280  	for k, v := range args.InstanceConfig.Tags {
   281  		if !strings.HasPrefix(k, tags.JujuTagPrefix) {
   282  			// Since some metadata is interpreted by LXD,
   283  			// we cannot allow arbitrary tags to be passed
   284  			// in by the user. We currently only pass through
   285  			// Juju-defined tags.
   286  			//
   287  			// TODO(axw) 2016-04-11 #1568666
   288  			// We should reject non-juju tags in config validation.
   289  			logger.Debugf("ignoring non-juju tag: %s=%s", k, v)
   290  			continue
   291  		}
   292  		metadata[k] = v
   293  	}
   294  
   295  	return metadata, nil
   296  }
   297  
   298  // getHardwareCharacteristics compiles hardware-related details about
   299  // the given instance and relative to the provided spec and returns it.
   300  func (env *environ) getHardwareCharacteristics(args environs.StartInstanceParams, inst *environInstance) *instance.HardwareCharacteristics {
   301  	raw := inst.raw.Hardware
   302  
   303  	archStr := raw.Architecture
   304  	if archStr == "unknown" || !arch.IsSupportedArch(archStr) {
   305  		// TODO(ericsnow) This special-case should be improved.
   306  		archStr = arch.HostArch()
   307  	}
   308  	cores := uint64(raw.NumCores)
   309  	mem := uint64(raw.MemoryMB)
   310  	return &instance.HardwareCharacteristics{
   311  		Arch:     &archStr,
   312  		CpuCores: &cores,
   313  		Mem:      &mem,
   314  	}
   315  }
   316  
   317  // AllInstances implements environs.InstanceBroker.
   318  func (env *environ) AllInstances() ([]instance.Instance, error) {
   319  	environInstances, err := env.allInstances()
   320  	instances := make([]instance.Instance, len(environInstances))
   321  	for i, inst := range environInstances {
   322  		if inst == nil {
   323  			continue
   324  		}
   325  		instances[i] = inst
   326  	}
   327  	return instances, err
   328  }
   329  
   330  // StopInstances implements environs.InstanceBroker.
   331  func (env *environ) StopInstances(instances ...instance.Id) error {
   332  	var ids []string
   333  	for _, id := range instances {
   334  		ids = append(ids, string(id))
   335  	}
   336  
   337  	prefix := env.namespace.Prefix()
   338  	err := removeInstances(env.raw, prefix, ids)
   339  	return errors.Trace(err)
   340  }
   341  
   342  func removeInstances(raw *rawProvider, prefix string, ids []string) error {
   343  	// We must first list the instances so we can remove any
   344  	// controller certificates.
   345  	allInstances, err := raw.Instances(prefix)
   346  	if err != nil {
   347  		return errors.Trace(err)
   348  	}
   349  	for _, inst := range allInstances {
   350  		certificateFingerprint := inst.Metadata()[lxdclient.CertificateFingerprintKey]
   351  		if certificateFingerprint == "" {
   352  			continue
   353  		}
   354  		var found bool
   355  		for _, id := range ids {
   356  			if inst.Name == id {
   357  				found = true
   358  				break
   359  			}
   360  		}
   361  		if !found {
   362  			continue
   363  		}
   364  		err := raw.RemoveCertByFingerprint(certificateFingerprint)
   365  		if err != nil && !errors.IsNotFound(err) {
   366  			return errors.Annotatef(err, "removing certificate for %q", inst.Name)
   367  		}
   368  	}
   369  	return raw.RemoveInstances(prefix, ids...)
   370  }