github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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  	"strings"
     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/core/constraints"
    14  	"github.com/juju/juju/core/instance"
    15  	"github.com/juju/juju/core/os/ostype"
    16  	"github.com/juju/juju/environs"
    17  	"github.com/juju/juju/environs/context"
    18  	"github.com/juju/juju/environs/imagemetadata"
    19  	"github.com/juju/juju/environs/instances"
    20  	"github.com/juju/juju/provider/common"
    21  	"github.com/juju/juju/provider/gce/google"
    22  )
    23  
    24  // StartInstance implements environs.InstanceBroker.
    25  func (env *environ) StartInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) (*environs.StartInstanceResult, error) {
    26  	// Start a new instance.
    27  
    28  	spec, err := buildInstanceSpec(env, ctx, args)
    29  	if err != nil {
    30  		return nil, environs.ZoneIndependentError(err)
    31  	}
    32  
    33  	if err := env.finishInstanceConfig(args, spec); err != nil {
    34  		return nil, environs.ZoneIndependentError(err)
    35  	}
    36  
    37  	// Validate availability zone.
    38  	volumeAttachmentsZone, err := volumeAttachmentsZone(args.VolumeAttachments)
    39  	if err != nil {
    40  		return nil, environs.ZoneIndependentError(err)
    41  	}
    42  	if err := validateAvailabilityZoneConsistency(args.AvailabilityZone, volumeAttachmentsZone); err != nil {
    43  		return nil, errors.Trace(err)
    44  	}
    45  
    46  	raw, err := newRawInstance(env, ctx, args, spec)
    47  	if err != nil {
    48  		return nil, errors.Trace(err)
    49  	}
    50  	logger.Infof("started instance %q in zone %q", raw.ID, raw.ZoneName)
    51  	inst := newInstance(raw, env)
    52  
    53  	// Build the result.
    54  	hwc := getHardwareCharacteristics(env, spec, inst)
    55  	result := environs.StartInstanceResult{
    56  		Instance: inst,
    57  		Hardware: hwc,
    58  	}
    59  	return &result, nil
    60  }
    61  
    62  var buildInstanceSpec = func(env *environ, ctx context.ProviderCallContext, args environs.StartInstanceParams) (*instances.InstanceSpec, error) {
    63  	return env.buildInstanceSpec(ctx, args)
    64  }
    65  
    66  var newRawInstance = func(env *environ, ctx context.ProviderCallContext, args environs.StartInstanceParams, spec *instances.InstanceSpec) (*google.Instance, error) {
    67  	return env.newRawInstance(ctx, args, spec)
    68  }
    69  
    70  var getHardwareCharacteristics = func(env *environ, spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics {
    71  	return env.getHardwareCharacteristics(spec, inst)
    72  }
    73  
    74  // finishInstanceConfig updates args.InstanceConfig in place. Setting up
    75  // the API, StateServing, and SSHkeys information.
    76  func (env *environ) finishInstanceConfig(args environs.StartInstanceParams, spec *instances.InstanceSpec) error {
    77  	if err := args.InstanceConfig.SetTools(args.Tools); err != nil {
    78  		return errors.Trace(err)
    79  	}
    80  	return instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config())
    81  }
    82  
    83  // buildInstanceSpec builds an instance spec from the provided args
    84  // and returns it. This includes pulling the simplestreams data for the
    85  // machine type, region, and other constraints.
    86  func (env *environ) buildInstanceSpec(ctx context.ProviderCallContext, args environs.StartInstanceParams) (*instances.InstanceSpec, error) {
    87  	instTypesAndCosts, err := env.InstanceTypes(ctx, constraints.Value{})
    88  	if err != nil {
    89  		return nil, errors.Trace(err)
    90  	}
    91  
    92  	arch, err := args.Tools.OneArch()
    93  	if err != nil {
    94  		return nil, errors.Trace(err)
    95  	}
    96  	spec, err := findInstanceSpec(
    97  		env, &instances.InstanceConstraint{
    98  			Region:      env.cloud.Region,
    99  			Base:        args.InstanceConfig.Base,
   100  			Arch:        arch,
   101  			Constraints: args.Constraints,
   102  		},
   103  		args.ImageMetadata,
   104  		instTypesAndCosts.InstanceTypes,
   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  	allInstanceTypes []instances.InstanceType,
   114  ) (*instances.InstanceSpec, error) {
   115  	return env.findInstanceSpec(ic, imageMetadata, allInstanceTypes)
   116  }
   117  
   118  // findInstanceSpec initializes a new instance spec for the given
   119  // constraints and returns it. This only covers populating the
   120  // initial data for the spec.
   121  func (env *environ) findInstanceSpec(
   122  	ic *instances.InstanceConstraint,
   123  	imageMetadata []*imagemetadata.ImageMetadata,
   124  	allInstanceTypes []instances.InstanceType,
   125  ) (*instances.InstanceSpec, error) {
   126  	images := instances.ImageMetadataToImages(imageMetadata)
   127  	spec, err := instances.FindInstanceSpec(images, ic, allInstanceTypes)
   128  	return spec, errors.Trace(err)
   129  }
   130  
   131  func (env *environ) imageURLBase(os ostype.OSType) (string, error) {
   132  	base, useCustomPath := env.ecfg.baseImagePath()
   133  	if useCustomPath {
   134  		return base, nil
   135  	}
   136  
   137  	switch os {
   138  	case ostype.Ubuntu:
   139  		switch env.Config().ImageStream() {
   140  		case "daily":
   141  			base = ubuntuDailyImageBasePath
   142  		case "pro":
   143  			base = ubuntuProImageBasePath
   144  		default:
   145  			base = ubuntuImageBasePath
   146  		}
   147  	default:
   148  		return "", errors.Errorf("os %s is not supported on the gce provider", os.String())
   149  	}
   150  
   151  	return base, nil
   152  }
   153  
   154  // newRawInstance is where the new physical instance is actually
   155  // provisioned, relative to the provided args and spec. Info for that
   156  // low-level instance is returned.
   157  func (env *environ) newRawInstance(
   158  	ctx context.ProviderCallContext, args environs.StartInstanceParams, spec *instances.InstanceSpec,
   159  ) (_ *google.Instance, err error) {
   160  	hostname, err := env.namespace.Hostname(args.InstanceConfig.MachineId)
   161  	if err != nil {
   162  		return nil, environs.ZoneIndependentError(err)
   163  	}
   164  
   165  	os := ostype.OSTypeForName(args.InstanceConfig.Base.OS)
   166  	metadata, err := getMetadata(args, os)
   167  	if err != nil {
   168  		return nil, environs.ZoneIndependentError(err)
   169  	}
   170  	tags := []string{
   171  		env.globalFirewallName(),
   172  		hostname,
   173  	}
   174  
   175  	imageURLBase, err := env.imageURLBase(os)
   176  	if err != nil {
   177  		return nil, environs.ZoneIndependentError(err)
   178  	}
   179  
   180  	disks, err := getDisks(
   181  		spec, args.Constraints,
   182  		os,
   183  		env.Config().UUID(),
   184  		imageURLBase,
   185  	)
   186  	if err != nil {
   187  		return nil, environs.ZoneIndependentError(err)
   188  	}
   189  
   190  	allocatePublicIP := true
   191  	if args.Constraints.HasAllocatePublicIP() {
   192  		allocatePublicIP = *args.Constraints.AllocatePublicIP
   193  	}
   194  
   195  	inst, err := env.gce.AddInstance(google.InstanceSpec{
   196  		ID:                hostname,
   197  		Type:              spec.InstanceType.Name,
   198  		Disks:             disks,
   199  		NetworkInterfaces: []string{"ExternalNAT"},
   200  		Metadata:          metadata,
   201  		Tags:              tags,
   202  		AvailabilityZone:  args.AvailabilityZone,
   203  		AllocatePublicIP:  allocatePublicIP,
   204  	})
   205  	if err != nil {
   206  		// We currently treat all AddInstance failures
   207  		// as being zone-specific, so we'll retry in
   208  		// another zone.
   209  		return nil, google.HandleCredentialError(errors.Trace(err), ctx)
   210  	}
   211  
   212  	return inst, nil
   213  }
   214  
   215  // getMetadata builds the raw "user-defined" metadata for the new
   216  // instance (relative to the provided args) and returns it.
   217  func getMetadata(args environs.StartInstanceParams, os ostype.OSType) (map[string]string, error) {
   218  	userData, err := providerinit.ComposeUserData(args.InstanceConfig, nil, GCERenderer{})
   219  	if err != nil {
   220  		return nil, errors.Annotate(err, "cannot make user data")
   221  	}
   222  	logger.Debugf("GCE user data; %d bytes", len(userData))
   223  
   224  	metadata := make(map[string]string)
   225  	for tag, value := range args.InstanceConfig.Tags {
   226  		metadata[tag] = value
   227  	}
   228  	switch os {
   229  	case ostype.Ubuntu:
   230  		// We store a gz snapshop of information that is used by
   231  		// cloud-init and unpacked in to the /var/lib/cloud/instances folder
   232  		// for the instance. Due to a limitation with GCE and binary blobs
   233  		// we base64 encode the data before storing it.
   234  		metadata[metadataKeyCloudInit] = string(userData)
   235  		// Valid encoding values are determined by the cloudinit GCE data source.
   236  		// See: http://cloudinit.readthedocs.org
   237  		metadata[metadataKeyEncoding] = "base64"
   238  
   239  	default:
   240  		return nil, errors.Errorf("cannot pack metadata for os %s on the gce provider", os.String())
   241  	}
   242  
   243  	return metadata, nil
   244  }
   245  
   246  // getDisks builds the raw spec for the disks that should be attached to
   247  // the new instances and returns it. This will always include a root
   248  // disk with characteristics determined by the provides args and
   249  // constraints.
   250  func getDisks(spec *instances.InstanceSpec, cons constraints.Value, os ostype.OSType, eUUID string, imageURLBase string) ([]google.DiskSpec, error) {
   251  	size := common.MinRootDiskSizeGiB(os)
   252  	if cons.RootDisk != nil && *cons.RootDisk > size {
   253  		size = common.MiBToGiB(*cons.RootDisk)
   254  	}
   255  	if imageURLBase == "" {
   256  		return nil, errors.NotValidf("imageURLBase must be set")
   257  	}
   258  	imageURL := imageURLBase + spec.Image.Id
   259  	logger.Infof("fetching disk image from %v", imageURL)
   260  	dSpec := google.DiskSpec{
   261  		OS:         strings.ToLower(os.String()),
   262  		SizeHintGB: size,
   263  		ImageURL:   imageURL,
   264  		Boot:       true,
   265  		AutoDelete: true,
   266  	}
   267  	if cons.RootDisk != nil && dSpec.TooSmall() {
   268  		msg := "Ignoring root-disk constraint of %dM because it is smaller than the GCE image size of %dG"
   269  		logger.Infof(msg, *cons.RootDisk, google.MinDiskSizeGB)
   270  	}
   271  	return []google.DiskSpec{dSpec}, nil
   272  }
   273  
   274  // getHardwareCharacteristics compiles hardware-related details about
   275  // the given instance and relative to the provided spec and returns it.
   276  func (env *environ) getHardwareCharacteristics(spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics {
   277  	rootDiskMB := inst.base.RootDiskGB() * 1024
   278  	hwc := instance.HardwareCharacteristics{
   279  		Arch:             &spec.Image.Arch,
   280  		Mem:              &spec.InstanceType.Mem,
   281  		CpuCores:         &spec.InstanceType.CpuCores,
   282  		CpuPower:         spec.InstanceType.CpuPower,
   283  		RootDisk:         &rootDiskMB,
   284  		AvailabilityZone: &inst.base.ZoneName,
   285  		// Tags: not supported in GCE.
   286  	}
   287  	return &hwc
   288  }
   289  
   290  // AllInstances implements environs.InstanceBroker.
   291  func (env *environ) AllInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) {
   292  	// We want all statuses here except for "terminated" - these instances are truly dead to us.
   293  	// According to https://cloud.google.com/compute/docs/instances/instance-life-cycle
   294  	// there are now only "provisioning", "staging", "running", "stopping" and "terminated" states.
   295  	// The others might have been needed for older versions of gce... Keeping here for potential
   296  	// backward compatibility.
   297  	nonLiveStatuses := []string{
   298  		google.StatusDone,
   299  		google.StatusDown,
   300  		google.StatusProvisioning,
   301  		google.StatusStopped,
   302  		google.StatusStopping,
   303  		google.StatusUp,
   304  	}
   305  	filters := append(instStatuses, nonLiveStatuses...)
   306  	instances, err := getInstances(env, ctx, filters...)
   307  	return instances, errors.Trace(err)
   308  }
   309  
   310  // AllRunningInstances implements environs.InstanceBroker.
   311  func (env *environ) AllRunningInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) {
   312  	instances, err := getInstances(env, ctx)
   313  	return instances, errors.Trace(err)
   314  }
   315  
   316  // StopInstances implements environs.InstanceBroker.
   317  func (env *environ) StopInstances(ctx context.ProviderCallContext, instances ...instance.Id) error {
   318  	var ids []string
   319  	for _, id := range instances {
   320  		ids = append(ids, string(id))
   321  	}
   322  
   323  	prefix := env.namespace.Prefix()
   324  	err := env.gce.RemoveInstances(prefix, ids...)
   325  	return google.HandleCredentialError(errors.Trace(err), ctx)
   326  }