github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/provider/gce/environ_broker.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package gce
     5  
     6  import (
     7  	"encoding/base64"
     8  
     9  	"github.com/juju/errors"
    10  
    11  	"github.com/juju/juju/cloudconfig/instancecfg"
    12  	"github.com/juju/juju/cloudconfig/providerinit"
    13  	"github.com/juju/juju/constraints"
    14  	"github.com/juju/juju/environs"
    15  	"github.com/juju/juju/environs/imagemetadata"
    16  	"github.com/juju/juju/environs/instances"
    17  	"github.com/juju/juju/environs/simplestreams"
    18  	"github.com/juju/juju/instance"
    19  	"github.com/juju/juju/network"
    20  	"github.com/juju/juju/provider/common"
    21  	"github.com/juju/juju/provider/gce/google"
    22  	"github.com/juju/juju/state/multiwatcher"
    23  	"github.com/juju/juju/tools"
    24  )
    25  
    26  func isStateServer(icfg *instancecfg.InstanceConfig) bool {
    27  	return multiwatcher.AnyJobNeedsState(icfg.Jobs...)
    28  }
    29  
    30  // MaintainInstance is specified in the InstanceBroker interface.
    31  func (*environ) MaintainInstance(args environs.StartInstanceParams) error {
    32  	return nil
    33  }
    34  
    35  // StartInstance implements environs.InstanceBroker.
    36  func (env *environ) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) {
    37  	// Please note that in order to fulfil the demands made of Instances and
    38  	// AllInstances, it is imperative that some environment feature be used to
    39  	// keep track of which instances were actually started by juju.
    40  	env = env.getSnapshot()
    41  
    42  	// Start a new instance.
    43  
    44  	if args.InstanceConfig.HasNetworks() {
    45  		return nil, errors.New("starting instances with networks is not supported yet")
    46  	}
    47  
    48  	spec, err := buildInstanceSpec(env, args)
    49  	if err != nil {
    50  		return nil, errors.Trace(err)
    51  	}
    52  
    53  	if err := env.finishInstanceConfig(args, spec); err != nil {
    54  		return nil, errors.Trace(err)
    55  	}
    56  
    57  	raw, err := newRawInstance(env, args, spec)
    58  	if err != nil {
    59  		return nil, errors.Trace(err)
    60  	}
    61  	logger.Infof("started instance %q in zone %q", raw.ID, raw.ZoneName)
    62  	inst := newInstance(raw, env)
    63  
    64  	// Ensure the API server port is open (globally for all instances
    65  	// on the network, not just for the specific node of the state
    66  	// server). See LP bug #1436191 for details.
    67  	if isStateServer(args.InstanceConfig) {
    68  		ports := network.PortRange{
    69  			FromPort: args.InstanceConfig.StateServingInfo.APIPort,
    70  			ToPort:   args.InstanceConfig.StateServingInfo.APIPort,
    71  			Protocol: "tcp",
    72  		}
    73  		if err := env.gce.OpenPorts(env.globalFirewallName(), ports); err != nil {
    74  			return nil, errors.Trace(err)
    75  		}
    76  	}
    77  
    78  	// Build the result.
    79  	hwc := getHardwareCharacteristics(env, spec, inst)
    80  	result := environs.StartInstanceResult{
    81  		Instance: inst,
    82  		Hardware: hwc,
    83  	}
    84  	return &result, nil
    85  }
    86  
    87  var buildInstanceSpec = func(env *environ, args environs.StartInstanceParams) (*instances.InstanceSpec, error) {
    88  	return env.buildInstanceSpec(args)
    89  }
    90  
    91  var newRawInstance = func(env *environ, args environs.StartInstanceParams, spec *instances.InstanceSpec) (*google.Instance, error) {
    92  	return env.newRawInstance(args, spec)
    93  }
    94  
    95  var getHardwareCharacteristics = func(env *environ, spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics {
    96  	return env.getHardwareCharacteristics(spec, inst)
    97  }
    98  
    99  // finishInstanceConfig updates args.InstanceConfig in place. Setting up
   100  // the API, StateServing, and SSHkeys information.
   101  func (env *environ) finishInstanceConfig(args environs.StartInstanceParams, spec *instances.InstanceSpec) error {
   102  	envTools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch})
   103  	if err != nil {
   104  		return errors.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches)
   105  	}
   106  
   107  	args.InstanceConfig.Tools = envTools[0]
   108  	return instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config())
   109  }
   110  
   111  // buildInstanceSpec builds an instance spec from the provided args
   112  // and returns it. This includes pulling the simplestreams data for the
   113  // machine type, region, and other constraints.
   114  func (env *environ) buildInstanceSpec(args environs.StartInstanceParams) (*instances.InstanceSpec, error) {
   115  	arches := args.Tools.Arches()
   116  	series := args.Tools.OneSeries()
   117  	spec, err := findInstanceSpec(env, env.Config().ImageStream(), &instances.InstanceConstraint{
   118  		Region:      env.ecfg.region(),
   119  		Series:      series,
   120  		Arches:      arches,
   121  		Constraints: args.Constraints,
   122  	})
   123  	return spec, errors.Trace(err)
   124  }
   125  
   126  var findInstanceSpec = func(env *environ, stream string, ic *instances.InstanceConstraint) (*instances.InstanceSpec, error) {
   127  	return env.findInstanceSpec(stream, ic)
   128  }
   129  
   130  // findInstanceSpec initializes a new instance spec for the given stream
   131  // (and constraints) and returns it. This only covers populating the
   132  // initial data for the spec. However, it does include fetching the
   133  // correct simplestreams image data.
   134  func (env *environ) findInstanceSpec(stream string, ic *instances.InstanceConstraint) (*instances.InstanceSpec, error) {
   135  	sources, err := environs.ImageMetadataSources(env)
   136  	if err != nil {
   137  		return nil, errors.Trace(err)
   138  	}
   139  
   140  	imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
   141  		CloudSpec: env.cloudSpec(ic.Region),
   142  		Series:    []string{ic.Series},
   143  		Arches:    ic.Arches,
   144  		Stream:    stream,
   145  	})
   146  
   147  	signedImageDataOnly := false
   148  	matchingImages, _, err := imageMetadataFetch(sources, imageConstraint, signedImageDataOnly)
   149  	if err != nil {
   150  		return nil, errors.Trace(err)
   151  	}
   152  
   153  	images := instances.ImageMetadataToImages(matchingImages)
   154  	spec, err := instances.FindInstanceSpec(images, ic, allInstanceTypes)
   155  	return spec, errors.Trace(err)
   156  }
   157  
   158  var imageMetadataFetch = imagemetadata.Fetch
   159  
   160  // newRawInstance is where the new physical instance is actually
   161  // provisioned, relative to the provided args and spec. Info for that
   162  // low-level instance is returned.
   163  func (env *environ) newRawInstance(args environs.StartInstanceParams, spec *instances.InstanceSpec) (*google.Instance, error) {
   164  	machineID := common.MachineFullName(env, args.InstanceConfig.MachineId)
   165  
   166  	metadata, err := getMetadata(args)
   167  	if err != nil {
   168  		return nil, errors.Trace(err)
   169  	}
   170  	tags := []string{
   171  		env.globalFirewallName(),
   172  		machineID,
   173  	}
   174  	// TODO(ericsnow) Use the env ID for the network name (instead of default)?
   175  	// TODO(ericsnow) Make the network name configurable?
   176  	// TODO(ericsnow) Support multiple networks?
   177  	// TODO(ericsnow) Use a different net interface name? Configurable?
   178  	instSpec := google.InstanceSpec{
   179  		ID:                machineID,
   180  		Type:              spec.InstanceType.Name,
   181  		Disks:             getDisks(spec, args.Constraints),
   182  		NetworkInterfaces: []string{"ExternalNAT"},
   183  		Metadata:          metadata,
   184  		Tags:              tags,
   185  		// Network is omitted (left empty).
   186  	}
   187  
   188  	zones, err := env.parseAvailabilityZones(args)
   189  	if err != nil {
   190  		return nil, errors.Trace(err)
   191  	}
   192  
   193  	inst, err := env.gce.AddInstance(instSpec, zones...)
   194  	return inst, errors.Trace(err)
   195  }
   196  
   197  // getMetadata builds the raw "user-defined" metadata for the new
   198  // instance (relative to the provided args) and returns it.
   199  func getMetadata(args environs.StartInstanceParams) (map[string]string, error) {
   200  	userData, err := providerinit.ComposeUserData(args.InstanceConfig, nil)
   201  	if err != nil {
   202  		return nil, errors.Annotate(err, "cannot make user data")
   203  	}
   204  	logger.Debugf("GCE user data; %d bytes", len(userData))
   205  
   206  	authKeys, err := google.FormatAuthorizedKeys(args.InstanceConfig.AuthorizedKeys, "ubuntu")
   207  	if err != nil {
   208  		return nil, errors.Trace(err)
   209  	}
   210  
   211  	b64UserData := base64.StdEncoding.EncodeToString([]byte(userData))
   212  	metadata := map[string]string{
   213  		metadataKeyIsState: metadataValueFalse,
   214  		// We store a gz snapshop of information that is used by
   215  		// cloud-init and unpacked in to the /var/lib/cloud/instances folder
   216  		// for the instance. Due to a limitation with GCE and binary blobs
   217  		// we base64 encode the data before storing it.
   218  		metadataKeyCloudInit: b64UserData,
   219  		// Valid encoding values are determined by the cloudinit GCE data source.
   220  		// See: http://cloudinit.readthedocs.org
   221  		metadataKeyEncoding: "base64",
   222  		metadataKeySSHKeys:  authKeys,
   223  	}
   224  	if isStateServer(args.InstanceConfig) {
   225  		metadata[metadataKeyIsState] = metadataValueTrue
   226  	}
   227  
   228  	return metadata, nil
   229  }
   230  
   231  // getDisks builds the raw spec for the disks that should be attached to
   232  // the new instances and returns it. This will always include a root
   233  // disk with characteristics determined by the provides args and
   234  // constraints.
   235  func getDisks(spec *instances.InstanceSpec, cons constraints.Value) []google.DiskSpec {
   236  	size := common.MinRootDiskSizeGiB
   237  	if cons.RootDisk != nil && *cons.RootDisk > size {
   238  		size = common.MiBToGiB(*cons.RootDisk)
   239  	}
   240  	dSpec := google.DiskSpec{
   241  		SizeHintGB: size,
   242  		ImageURL:   imageBasePath + spec.Image.Id,
   243  		Boot:       true,
   244  		AutoDelete: true,
   245  	}
   246  	if cons.RootDisk != nil && dSpec.TooSmall() {
   247  		msg := "Ignoring root-disk constraint of %dM because it is smaller than the GCE image size of %dG"
   248  		logger.Infof(msg, *cons.RootDisk, google.MinDiskSizeGB)
   249  	}
   250  	return []google.DiskSpec{dSpec}
   251  }
   252  
   253  // getHardwareCharacteristics compiles hardware-related details about
   254  // the given instance and relative to the provided spec and returns it.
   255  func (env *environ) getHardwareCharacteristics(spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics {
   256  	rootDiskMB := inst.base.RootDiskGB() * 1024
   257  	hwc := instance.HardwareCharacteristics{
   258  		Arch:             &spec.Image.Arch,
   259  		Mem:              &spec.InstanceType.Mem,
   260  		CpuCores:         &spec.InstanceType.CpuCores,
   261  		CpuPower:         spec.InstanceType.CpuPower,
   262  		RootDisk:         &rootDiskMB,
   263  		AvailabilityZone: &inst.base.ZoneName,
   264  		// Tags: not supported in GCE.
   265  	}
   266  	return &hwc
   267  }
   268  
   269  // AllInstances implements environs.InstanceBroker.
   270  func (env *environ) AllInstances() ([]instance.Instance, error) {
   271  	instances, err := getInstances(env)
   272  	return instances, errors.Trace(err)
   273  }
   274  
   275  // StopInstances implements environs.InstanceBroker.
   276  func (env *environ) StopInstances(instances ...instance.Id) error {
   277  	env = env.getSnapshot()
   278  
   279  	var ids []string
   280  	for _, id := range instances {
   281  		ids = append(ids, string(id))
   282  	}
   283  
   284  	prefix := common.MachineFullName(env, "")
   285  	err := env.gce.RemoveInstances(prefix, ids...)
   286  	return errors.Trace(err)
   287  }