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