github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	"fmt"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/utils"
    11  	jujuos "github.com/juju/utils/os"
    12  	"github.com/juju/utils/series"
    13  
    14  	"github.com/juju/juju/cloudconfig/instancecfg"
    15  	"github.com/juju/juju/cloudconfig/providerinit"
    16  	"github.com/juju/juju/constraints"
    17  	"github.com/juju/juju/environs"
    18  	"github.com/juju/juju/environs/imagemetadata"
    19  	"github.com/juju/juju/environs/instances"
    20  	"github.com/juju/juju/instance"
    21  	"github.com/juju/juju/provider/common"
    22  	"github.com/juju/juju/provider/gce/google"
    23  	"github.com/juju/juju/state/multiwatcher"
    24  	"github.com/juju/juju/tools"
    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  	spec, err := buildInstanceSpec(env, args)
    41  	if err != nil {
    42  		return nil, errors.Trace(err)
    43  	}
    44  
    45  	if err := env.finishInstanceConfig(args, spec); err != nil {
    46  		return nil, errors.Trace(err)
    47  	}
    48  
    49  	raw, err := newRawInstance(env, args, spec)
    50  	if err != nil {
    51  		return nil, errors.Trace(err)
    52  	}
    53  	logger.Infof("started instance %q in zone %q", raw.ID, raw.ZoneName)
    54  	inst := newInstance(raw, env)
    55  
    56  	// Build the result.
    57  	hwc := getHardwareCharacteristics(env, spec, inst)
    58  	result := environs.StartInstanceResult{
    59  		Instance: inst,
    60  		Hardware: hwc,
    61  	}
    62  	return &result, nil
    63  }
    64  
    65  var buildInstanceSpec = func(env *environ, args environs.StartInstanceParams) (*instances.InstanceSpec, error) {
    66  	return env.buildInstanceSpec(args)
    67  }
    68  
    69  var newRawInstance = func(env *environ, args environs.StartInstanceParams, spec *instances.InstanceSpec) (*google.Instance, error) {
    70  	return env.newRawInstance(args, spec)
    71  }
    72  
    73  var getHardwareCharacteristics = func(env *environ, spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics {
    74  	return env.getHardwareCharacteristics(spec, inst)
    75  }
    76  
    77  // finishInstanceConfig updates args.InstanceConfig in place. Setting up
    78  // the API, StateServing, and SSHkeys information.
    79  func (env *environ) finishInstanceConfig(args environs.StartInstanceParams, spec *instances.InstanceSpec) error {
    80  	envTools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch})
    81  	if err != nil {
    82  		return errors.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches)
    83  	}
    84  
    85  	if err := args.InstanceConfig.SetTools(envTools); err != nil {
    86  		return errors.Trace(err)
    87  	}
    88  	return instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config())
    89  }
    90  
    91  // buildInstanceSpec builds an instance spec from the provided args
    92  // and returns it. This includes pulling the simplestreams data for the
    93  // machine type, region, and other constraints.
    94  func (env *environ) buildInstanceSpec(args environs.StartInstanceParams) (*instances.InstanceSpec, error) {
    95  	arches := args.Tools.Arches()
    96  	series := args.Tools.OneSeries()
    97  	spec, err := findInstanceSpec(
    98  		env, &instances.InstanceConstraint{
    99  			Region:      env.cloud.Region,
   100  			Series:      series,
   101  			Arches:      arches,
   102  			Constraints: args.Constraints,
   103  		},
   104  		args.ImageMetadata,
   105  	)
   106  	return spec, errors.Trace(err)
   107  }
   108  
   109  var findInstanceSpec = func(
   110  	env *environ,
   111  	ic *instances.InstanceConstraint,
   112  	imageMetadata []*imagemetadata.ImageMetadata,
   113  ) (*instances.InstanceSpec, error) {
   114  	return env.findInstanceSpec(ic, imageMetadata)
   115  }
   116  
   117  // findInstanceSpec initializes a new instance spec for the given
   118  // constraints and returns it. This only covers populating the
   119  // initial data for the spec.
   120  func (env *environ) findInstanceSpec(
   121  	ic *instances.InstanceConstraint,
   122  	imageMetadata []*imagemetadata.ImageMetadata,
   123  ) (*instances.InstanceSpec, error) {
   124  	images := instances.ImageMetadataToImages(imageMetadata)
   125  	spec, err := instances.FindInstanceSpec(images, ic, allInstanceTypes)
   126  	return spec, errors.Trace(err)
   127  }
   128  
   129  // newRawInstance is where the new physical instance is actually
   130  // provisioned, relative to the provided args and spec. Info for that
   131  // low-level instance is returned.
   132  func (env *environ) newRawInstance(args environs.StartInstanceParams, spec *instances.InstanceSpec) (*google.Instance, error) {
   133  	hostname, err := env.namespace.Hostname(args.InstanceConfig.MachineId)
   134  	if err != nil {
   135  		return nil, errors.Trace(err)
   136  	}
   137  
   138  	os, err := series.GetOSFromSeries(args.InstanceConfig.Series)
   139  	if err != nil {
   140  		return nil, errors.Trace(err)
   141  	}
   142  
   143  	metadata, err := getMetadata(args, os)
   144  	if err != nil {
   145  		return nil, errors.Trace(err)
   146  	}
   147  	tags := []string{
   148  		env.globalFirewallName(),
   149  		hostname,
   150  	}
   151  
   152  	disks, err := getDisks(
   153  		spec, args.Constraints, args.InstanceConfig.Series, env.Config().UUID(), env.Config().ImageStream() == "daily",
   154  	)
   155  	if err != nil {
   156  		return nil, errors.Trace(err)
   157  	}
   158  
   159  	// TODO(ericsnow) Use the env ID for the network name (instead of default)?
   160  	// TODO(ericsnow) Make the network name configurable?
   161  	// TODO(ericsnow) Support multiple networks?
   162  	// TODO(ericsnow) Use a different net interface name? Configurable?
   163  	instSpec := google.InstanceSpec{
   164  		ID:                hostname,
   165  		Type:              spec.InstanceType.Name,
   166  		Disks:             disks,
   167  		NetworkInterfaces: []string{"ExternalNAT"},
   168  		Metadata:          metadata,
   169  		Tags:              tags,
   170  		// Network is omitted (left empty).
   171  	}
   172  
   173  	zones, err := env.parseAvailabilityZones(args)
   174  	if err != nil {
   175  		return nil, errors.Trace(err)
   176  	}
   177  
   178  	inst, err := env.gce.AddInstance(instSpec, zones...)
   179  	return inst, errors.Trace(err)
   180  }
   181  
   182  // getMetadata builds the raw "user-defined" metadata for the new
   183  // instance (relative to the provided args) and returns it.
   184  func getMetadata(args environs.StartInstanceParams, os jujuos.OSType) (map[string]string, error) {
   185  	userData, err := providerinit.ComposeUserData(args.InstanceConfig, nil, GCERenderer{})
   186  	if err != nil {
   187  		return nil, errors.Annotate(err, "cannot make user data")
   188  	}
   189  	logger.Debugf("GCE user data; %d bytes", len(userData))
   190  
   191  	metadata := make(map[string]string)
   192  	for tag, value := range args.InstanceConfig.Tags {
   193  		metadata[tag] = value
   194  	}
   195  	switch os {
   196  	case jujuos.Ubuntu:
   197  		// We store a gz snapshop of information that is used by
   198  		// cloud-init and unpacked in to the /var/lib/cloud/instances folder
   199  		// for the instance. Due to a limitation with GCE and binary blobs
   200  		// we base64 encode the data before storing it.
   201  		metadata[metadataKeyCloudInit] = string(userData)
   202  		// Valid encoding values are determined by the cloudinit GCE data source.
   203  		// See: http://cloudinit.readthedocs.org
   204  		metadata[metadataKeyEncoding] = "base64"
   205  
   206  	case jujuos.Windows:
   207  		metadata[metadataKeyWindowsUserdata] = string(userData)
   208  
   209  		validChars := append(utils.UpperAlpha, append(utils.LowerAlpha, utils.Digits...)...)
   210  
   211  		// The hostname must have maximum 15 characters
   212  		winHostname := "juju" + utils.RandomString(11, validChars)
   213  		metadata[metadataKeyWindowsSysprep] = fmt.Sprintf(winSetHostnameScript, winHostname)
   214  	default:
   215  		return nil, errors.Errorf("cannot pack metadata for os %s on the gce provider", os.String())
   216  	}
   217  
   218  	return metadata, nil
   219  }
   220  
   221  // getDisks builds the raw spec for the disks that should be attached to
   222  // the new instances and returns it. This will always include a root
   223  // disk with characteristics determined by the provides args and
   224  // constraints.
   225  func getDisks(spec *instances.InstanceSpec, cons constraints.Value, ser, eUUID string, daily bool) ([]google.DiskSpec, error) {
   226  	size := common.MinRootDiskSizeGiB(ser)
   227  	if cons.RootDisk != nil && *cons.RootDisk > size {
   228  		size = common.MiBToGiB(*cons.RootDisk)
   229  	}
   230  	var imageURL string
   231  	os, err := series.GetOSFromSeries(ser)
   232  	if err != nil {
   233  		return nil, errors.Trace(err)
   234  	}
   235  	switch os {
   236  	case jujuos.Ubuntu:
   237  		if daily {
   238  			imageURL = ubuntuDailyImageBasePath
   239  		} else {
   240  			imageURL = ubuntuImageBasePath
   241  		}
   242  	case jujuos.Windows:
   243  		imageURL = windowsImageBasePath
   244  	default:
   245  		return nil, errors.Errorf("os %s is not supported on the gce provider", os.String())
   246  	}
   247  	dSpec := google.DiskSpec{
   248  		Series:      ser,
   249  		SizeHintGB:  size,
   250  		ImageURL:    imageURL + spec.Image.Id,
   251  		Boot:        true,
   252  		AutoDelete:  true,
   253  		Description: eUUID,
   254  	}
   255  	if cons.RootDisk != nil && dSpec.TooSmall() {
   256  		msg := "Ignoring root-disk constraint of %dM because it is smaller than the GCE image size of %dG"
   257  		logger.Infof(msg, *cons.RootDisk, google.MinDiskSizeGB(ser))
   258  	}
   259  	return []google.DiskSpec{dSpec}, nil
   260  }
   261  
   262  // getHardwareCharacteristics compiles hardware-related details about
   263  // the given instance and relative to the provided spec and returns it.
   264  func (env *environ) getHardwareCharacteristics(spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics {
   265  	rootDiskMB := inst.base.RootDiskGB() * 1024
   266  	hwc := instance.HardwareCharacteristics{
   267  		Arch:             &spec.Image.Arch,
   268  		Mem:              &spec.InstanceType.Mem,
   269  		CpuCores:         &spec.InstanceType.CpuCores,
   270  		CpuPower:         spec.InstanceType.CpuPower,
   271  		RootDisk:         &rootDiskMB,
   272  		AvailabilityZone: &inst.base.ZoneName,
   273  		// Tags: not supported in GCE.
   274  	}
   275  	return &hwc
   276  }
   277  
   278  // AllInstances implements environs.InstanceBroker.
   279  func (env *environ) AllInstances() ([]instance.Instance, error) {
   280  	instances, err := getInstances(env)
   281  	return instances, errors.Trace(err)
   282  }
   283  
   284  // StopInstances implements environs.InstanceBroker.
   285  func (env *environ) StopInstances(instances ...instance.Id) error {
   286  	var ids []string
   287  	for _, id := range instances {
   288  		ids = append(ids, string(id))
   289  	}
   290  
   291  	prefix := env.namespace.Prefix()
   292  	err := env.gce.RemoveInstances(prefix, ids...)
   293  	return errors.Trace(err)
   294  }