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