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