github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	if args.InstanceConfig.Controller != nil {
   184  		// For controller machines, generate a certificate pair and write
   185  		// them to the instance's disk in a well-defined location, along
   186  		// with the server's certificate.
   187  		certPEM, keyPEM, err := lxdshared.GenerateMemCert()
   188  		if err != nil {
   189  			return nil, errors.Trace(err)
   190  		}
   191  		cert := lxdclient.NewCert(certPEM, keyPEM)
   192  		cert.Name = hostname
   193  		// TODO(axw) 2016-08-24 #1616346
   194  		// We need to remove this cert when removing
   195  		// the machine and/or destroying the controller.
   196  		if err := env.raw.AddCert(cert); err != nil {
   197  			return nil, errors.Annotatef(err, "adding certificate %q", cert.Name)
   198  		}
   199  		serverState, err := env.raw.ServerStatus()
   200  		if err != nil {
   201  			return nil, errors.Annotate(err, "getting server status")
   202  		}
   203  		cloudcfg.AddRunTextFile(clientCertPath, string(certPEM), 0600)
   204  		cloudcfg.AddRunTextFile(clientKeyPath, string(keyPEM), 0600)
   205  		cloudcfg.AddRunTextFile(serverCertPath, serverState.Environment.Certificate, 0600)
   206  	}
   207  
   208  	cloudcfg.SetAttr("hostname", hostname)
   209  	cloudcfg.SetAttr("manage_etc_hosts", true)
   210  
   211  	metadata, err := getMetadata(cloudcfg, args)
   212  	if err != nil {
   213  		return nil, errors.Trace(err)
   214  	}
   215  
   216  	//tags := []string{
   217  	//	env.globalFirewallName(),
   218  	//	machineID,
   219  	//}
   220  	// TODO(ericsnow) Use the env ID for the network name (instead of default)?
   221  	// TODO(ericsnow) Make the network name configurable?
   222  	// TODO(ericsnow) Support multiple networks?
   223  	// TODO(ericsnow) Use a different net interface name? Configurable?
   224  	instSpec := lxdclient.InstanceSpec{
   225  		Name:  hostname,
   226  		Image: image,
   227  		//Type:              spec.InstanceType.Name,
   228  		//Disks:             getDisks(spec, args.Constraints),
   229  		//NetworkInterfaces: []string{"ExternalNAT"},
   230  		Metadata: metadata,
   231  		Profiles: []string{
   232  			//TODO(wwitzel3) allow the user to specify lxc profiles to apply. This allows the
   233  			// user to setup any custom devices order config settings for their environment.
   234  			// Also we must ensure that a device with the parent: lxcbr0 exists in at least
   235  			// one of the profiles.
   236  			"default",
   237  			env.profileName(),
   238  		},
   239  		//Tags:              tags,
   240  		// Network is omitted (left empty).
   241  	}
   242  
   243  	logger.Infof("starting instance %q (image %q)...", instSpec.Name, instSpec.Image)
   244  
   245  	statusCallback(status.Allocating, "preparing image")
   246  	inst, err := env.raw.AddInstance(instSpec)
   247  	if err != nil {
   248  		return nil, errors.Trace(err)
   249  	}
   250  	statusCallback(status.Running, "container started")
   251  	return inst, nil
   252  }
   253  
   254  // getMetadata builds the raw "user-defined" metadata for the new
   255  // instance (relative to the provided args) and returns it.
   256  func getMetadata(cloudcfg cloudinit.CloudConfig, args environs.StartInstanceParams) (map[string]string, error) {
   257  	renderer := lxdRenderer{}
   258  	uncompressed, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, renderer)
   259  	if err != nil {
   260  		return nil, errors.Annotate(err, "cannot make user data")
   261  	}
   262  	logger.Debugf("LXD user data; %d bytes", len(uncompressed))
   263  
   264  	// TODO(ericsnow) Looks like LXD does not handle gzipped userdata
   265  	// correctly.  It likely has to do with the HTTP transport, much
   266  	// as we have to b64encode the userdata for GCE.  Until that is
   267  	// resolved we simply pass the plain text.
   268  	//compressed := utils.Gzip(compressed)
   269  	userdata := string(uncompressed)
   270  
   271  	metadata := map[string]string{
   272  		// store the cloud-config userdata for cloud-init.
   273  		metadataKeyCloudInit: userdata,
   274  	}
   275  	for k, v := range args.InstanceConfig.Tags {
   276  		if !strings.HasPrefix(k, tags.JujuTagPrefix) {
   277  			// Since some metadata is interpreted by LXD,
   278  			// we cannot allow arbitrary tags to be passed
   279  			// in by the user. We currently only pass through
   280  			// Juju-defined tags.
   281  			//
   282  			// TODO(axw) 2016-04-11 #1568666
   283  			// We should reject non-juju tags in config validation.
   284  			logger.Debugf("ignoring non-juju tag: %s=%s", k, v)
   285  			continue
   286  		}
   287  		metadata[k] = v
   288  	}
   289  
   290  	return metadata, nil
   291  }
   292  
   293  // getHardwareCharacteristics compiles hardware-related details about
   294  // the given instance and relative to the provided spec and returns it.
   295  func (env *environ) getHardwareCharacteristics(args environs.StartInstanceParams, inst *environInstance) *instance.HardwareCharacteristics {
   296  	raw := inst.raw.Hardware
   297  
   298  	archStr := raw.Architecture
   299  	if archStr == "unknown" || !arch.IsSupportedArch(archStr) {
   300  		// TODO(ericsnow) This special-case should be improved.
   301  		archStr = arch.HostArch()
   302  	}
   303  	cores := uint64(raw.NumCores)
   304  	mem := uint64(raw.MemoryMB)
   305  	return &instance.HardwareCharacteristics{
   306  		Arch:     &archStr,
   307  		CpuCores: &cores,
   308  		Mem:      &mem,
   309  	}
   310  }
   311  
   312  // AllInstances implements environs.InstanceBroker.
   313  func (env *environ) AllInstances() ([]instance.Instance, error) {
   314  	environInstances, err := env.allInstances()
   315  	instances := make([]instance.Instance, len(environInstances))
   316  	for i, inst := range environInstances {
   317  		if inst == nil {
   318  			continue
   319  		}
   320  		instances[i] = inst
   321  	}
   322  	return instances, err
   323  }
   324  
   325  // StopInstances implements environs.InstanceBroker.
   326  func (env *environ) StopInstances(instances ...instance.Id) error {
   327  	var ids []string
   328  	for _, id := range instances {
   329  		ids = append(ids, string(id))
   330  	}
   331  
   332  	prefix := env.namespace.Prefix()
   333  	err := env.raw.RemoveInstances(prefix, ids...)
   334  	return errors.Trace(err)
   335  }