github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/provider/joyent/environ_instance.go (about)

     1  // Copyright 2013 Joyent Inc.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package joyent
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/joyent/gocommon/client"
    13  	"github.com/joyent/gosdc/cloudapi"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/names"
    16  	"github.com/juju/utils/arch"
    17  
    18  	"github.com/juju/juju/cloudconfig/cloudinit"
    19  	"github.com/juju/juju/cloudconfig/instancecfg"
    20  	"github.com/juju/juju/cloudconfig/providerinit"
    21  	"github.com/juju/juju/constraints"
    22  	"github.com/juju/juju/environs"
    23  	"github.com/juju/juju/environs/imagemetadata"
    24  	"github.com/juju/juju/environs/instances"
    25  	"github.com/juju/juju/environs/tags"
    26  	"github.com/juju/juju/instance"
    27  	"github.com/juju/juju/tools"
    28  )
    29  
    30  var (
    31  	vTypeSmartmachine   = "smartmachine"
    32  	vTypeVirtualmachine = "kvm"
    33  	defaultCpuCores     = uint64(1)
    34  )
    35  
    36  type joyentCompute struct {
    37  	sync.Mutex
    38  	ecfg     *environConfig
    39  	cloudapi *cloudapi.Client
    40  }
    41  
    42  func newCompute(cfg *environConfig) (*joyentCompute, error) {
    43  	creds, err := credentials(cfg)
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	client := client.NewClient(cfg.sdcUrl(), cloudapi.DefaultAPIVersion, creds, &logger)
    48  
    49  	return &joyentCompute{
    50  		ecfg:     cfg,
    51  		cloudapi: cloudapi.New(client)}, nil
    52  }
    53  
    54  func (env *joyentEnviron) machineFullName(machineId string) string {
    55  	return fmt.Sprintf("juju-%s-%s", env.Config().Name(), names.NewMachineTag(machineId))
    56  }
    57  
    58  var unsupportedConstraints = []string{
    59  	constraints.CpuPower,
    60  	constraints.Tags,
    61  	constraints.VirtType,
    62  }
    63  
    64  // ConstraintsValidator is defined on the Environs interface.
    65  func (env *joyentEnviron) ConstraintsValidator() (constraints.Validator, error) {
    66  	validator := constraints.NewValidator()
    67  	validator.RegisterUnsupported(unsupportedConstraints)
    68  	supportedArches, err := env.SupportedArchitectures()
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	validator.RegisterVocabulary(constraints.Arch, supportedArches)
    73  	packages, err := env.compute.cloudapi.ListPackages(nil)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	instTypeNames := make([]string, len(packages))
    78  	for i, pkg := range packages {
    79  		instTypeNames[i] = pkg.Name
    80  	}
    81  	validator.RegisterVocabulary(constraints.InstanceType, instTypeNames)
    82  	return validator, nil
    83  }
    84  
    85  // MaintainInstance is specified in the InstanceBroker interface.
    86  func (*joyentEnviron) MaintainInstance(args environs.StartInstanceParams) error {
    87  	return nil
    88  }
    89  
    90  func (env *joyentEnviron) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) {
    91  	series := args.Tools.OneSeries()
    92  	arches := args.Tools.Arches()
    93  	spec, err := env.FindInstanceSpec(&instances.InstanceConstraint{
    94  		Region:      env.Ecfg().Region(),
    95  		Series:      series,
    96  		Arches:      arches,
    97  		Constraints: args.Constraints,
    98  	}, args.ImageMetadata)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	tools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch})
   103  	if err != nil {
   104  		return nil, errors.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches)
   105  	}
   106  
   107  	if err := args.InstanceConfig.SetTools(tools); err != nil {
   108  		return nil, errors.Trace(err)
   109  	}
   110  
   111  	if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config()); err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	// This is a hack that ensures that instances can communicate over
   116  	// the internal network. Joyent sometimes gives instances
   117  	// different 10.x.x.x/21 networks and adding this route allows
   118  	// them to talk despite this. See:
   119  	// https://bugs.launchpad.net/juju-core/+bug/1401130
   120  	cloudcfg, err := cloudinit.New(args.InstanceConfig.Series)
   121  	if err != nil {
   122  		return nil, errors.Annotate(err, "cannot create cloudinit template")
   123  	}
   124  	ifupScript := `
   125  #!/bin/bash
   126  
   127  # These guards help to ensure that this hack only runs if Joyent's
   128  # internal network still works as it does at time of writing.
   129  [ "$IFACE" == "eth1" ] || [ "$IFACE" == "--all" ] || exit 0
   130  /sbin/ip -4 --oneline addr show dev eth1 | fgrep --quiet " inet 10." || exit 0
   131  
   132  /sbin/ip route add 10.0.0.0/8 dev eth1
   133  `[1:]
   134  	cloudcfg.AddBootTextFile("/etc/network/if-up.d/joyent", ifupScript, 0755)
   135  
   136  	userData, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, JoyentRenderer{})
   137  	if err != nil {
   138  		return nil, errors.Annotate(err, "cannot make user data")
   139  	}
   140  	logger.Debugf("joyent user data: %d bytes", len(userData))
   141  
   142  	instanceTags := make(map[string]string)
   143  	for tag, value := range args.InstanceConfig.Tags {
   144  		instanceTags[tagKey(tag)] = value
   145  	}
   146  	instanceTags[tagKey("group")] = "juju"
   147  	instanceTags[tagKey("model")] = env.Config().Name()
   148  
   149  	args.InstanceConfig.Tags = instanceTags
   150  	logger.Debugf("Now tags are:  %+v", args.InstanceConfig.Tags)
   151  
   152  	var machine *cloudapi.Machine
   153  	machine, err = env.compute.cloudapi.CreateMachine(cloudapi.CreateMachineOpts{
   154  		Package:  spec.InstanceType.Name,
   155  		Image:    spec.Image.Id,
   156  		Metadata: map[string]string{"metadata.cloud-init:user-data": string(userData)},
   157  		Tags:     args.InstanceConfig.Tags,
   158  	})
   159  	if err != nil {
   160  		return nil, errors.Annotate(err, "cannot create instances")
   161  	}
   162  	machineId := machine.Id
   163  
   164  	logger.Infof("provisioning instance %q", machineId)
   165  	logger.Infof("machine created with tags %+v", machine.Tags)
   166  
   167  	machine, err = env.compute.cloudapi.GetMachine(machineId)
   168  	if err != nil {
   169  		return nil, errors.Annotate(err, "cannot start instances")
   170  	}
   171  
   172  	// wait for machine to start
   173  	for !strings.EqualFold(machine.State, "running") {
   174  		time.Sleep(1 * time.Second)
   175  
   176  		machine, err = env.compute.cloudapi.GetMachine(machineId)
   177  		if err != nil {
   178  			return nil, errors.Annotate(err, "cannot start instances")
   179  		}
   180  	}
   181  
   182  	logger.Infof("started instance %q", machineId)
   183  
   184  	inst := &joyentInstance{
   185  		machine: machine,
   186  		env:     env,
   187  	}
   188  
   189  	disk64 := uint64(machine.Disk)
   190  	hc := instance.HardwareCharacteristics{
   191  		Arch:     &spec.Image.Arch,
   192  		Mem:      &spec.InstanceType.Mem,
   193  		CpuCores: &spec.InstanceType.CpuCores,
   194  		CpuPower: spec.InstanceType.CpuPower,
   195  		RootDisk: &disk64,
   196  	}
   197  
   198  	return &environs.StartInstanceResult{
   199  		Instance: inst,
   200  		Hardware: &hc,
   201  	}, nil
   202  }
   203  
   204  // Joyent tag must be prefixed with "tag."
   205  func tagKey(aKey string) string {
   206  	return "tag." + aKey
   207  }
   208  
   209  func (env *joyentEnviron) AllInstances() ([]instance.Instance, error) {
   210  	instances := []instance.Instance{}
   211  
   212  	filter := cloudapi.NewFilter()
   213  	filter.Set(tagKey("group"), "juju")
   214  	filter.Set(tagKey(tags.JujuModel), env.Config().UUID())
   215  
   216  	machines, err := env.compute.cloudapi.ListMachines(filter)
   217  	if err != nil {
   218  		return nil, errors.Annotate(err, "cannot retrieve instances")
   219  	}
   220  
   221  	for _, m := range machines {
   222  		if strings.EqualFold(m.State, "provisioning") || strings.EqualFold(m.State, "running") {
   223  			copy := m
   224  			instances = append(instances, &joyentInstance{machine: &copy, env: env})
   225  		}
   226  	}
   227  
   228  	return instances, nil
   229  }
   230  
   231  func (env *joyentEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) {
   232  	if len(ids) == 0 {
   233  		return nil, nil
   234  	}
   235  
   236  	logger.Debugf("Looking for instances %q", ids)
   237  
   238  	instances := make([]instance.Instance, len(ids))
   239  	found := 0
   240  
   241  	allInstances, err := env.AllInstances()
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  
   246  	for i, id := range ids {
   247  		for _, instance := range allInstances {
   248  			if instance.Id() == id {
   249  				instances[i] = instance
   250  				found++
   251  			}
   252  		}
   253  	}
   254  
   255  	logger.Debugf("Found %d instances %q", found, instances)
   256  
   257  	if found == 0 {
   258  		return nil, environs.ErrNoInstances
   259  	} else if found < len(ids) {
   260  		return instances, environs.ErrPartialInstances
   261  	}
   262  
   263  	return instances, nil
   264  }
   265  
   266  func (env *joyentEnviron) StopInstances(ids ...instance.Id) error {
   267  	// Remove all the instances in parallel so that we incur less round-trips.
   268  	var wg sync.WaitGroup
   269  	//var err error
   270  	wg.Add(len(ids))
   271  	errc := make(chan error, len(ids))
   272  	for _, id := range ids {
   273  		id := id // copy to new free var for closure
   274  		go func() {
   275  			defer wg.Done()
   276  			if err := env.stopInstance(string(id)); err != nil {
   277  				errc <- err
   278  			}
   279  		}()
   280  	}
   281  	wg.Wait()
   282  	select {
   283  	case err := <-errc:
   284  		return errors.Annotate(err, "cannot stop all instances")
   285  	default:
   286  	}
   287  	return nil
   288  }
   289  
   290  func (env *joyentEnviron) stopInstance(id string) error {
   291  	// wait for machine to be running
   292  	// if machine is still provisioning stop will fail
   293  	for !env.pollMachineState(id, "running") {
   294  		time.Sleep(1 * time.Second)
   295  	}
   296  
   297  	err := env.compute.cloudapi.StopMachine(id)
   298  	if err != nil {
   299  		return errors.Annotatef(err, "cannot stop instance %v", id)
   300  	}
   301  
   302  	// wait for machine to be stopped
   303  	for !env.pollMachineState(id, "stopped") {
   304  		time.Sleep(1 * time.Second)
   305  	}
   306  
   307  	err = env.compute.cloudapi.DeleteMachine(id)
   308  	if err != nil {
   309  		return errors.Annotatef(err, "cannot delete instance %v", id)
   310  	}
   311  
   312  	return nil
   313  }
   314  
   315  func (env *joyentEnviron) pollMachineState(machineId, state string) bool {
   316  	instanceConfig, err := env.compute.cloudapi.GetMachine(machineId)
   317  	if err != nil {
   318  		return false
   319  	}
   320  	return strings.EqualFold(instanceConfig.State, state)
   321  }
   322  
   323  func (env *joyentEnviron) listInstanceTypes() ([]instances.InstanceType, error) {
   324  	packages, err := env.compute.cloudapi.ListPackages(nil)
   325  	if err != nil {
   326  		return nil, err
   327  	}
   328  	allInstanceTypes := []instances.InstanceType{}
   329  	for _, pkg := range packages {
   330  		// ListPackages does not include the virt type of the package.
   331  		// However, Joyent says the smart packages have zero VCPUs.
   332  		var virtType *string
   333  		if pkg.VCPUs > 0 {
   334  			virtType = &vTypeVirtualmachine
   335  		} else {
   336  			virtType = &vTypeSmartmachine
   337  		}
   338  		instanceType := instances.InstanceType{
   339  			Id:       pkg.Id,
   340  			Name:     pkg.Name,
   341  			Arches:   []string{arch.AMD64},
   342  			Mem:      uint64(pkg.Memory),
   343  			CpuCores: uint64(pkg.VCPUs),
   344  			RootDisk: uint64(pkg.Disk * 1024),
   345  			VirtType: virtType,
   346  		}
   347  		allInstanceTypes = append(allInstanceTypes, instanceType)
   348  	}
   349  	return allInstanceTypes, nil
   350  }
   351  
   352  // FindInstanceSpec returns an InstanceSpec satisfying the supplied instanceConstraint.
   353  func (env *joyentEnviron) FindInstanceSpec(
   354  	ic *instances.InstanceConstraint,
   355  	imageMetadata []*imagemetadata.ImageMetadata,
   356  ) (*instances.InstanceSpec, error) {
   357  	// Require at least one VCPU so we get KVM rather than smart package.
   358  	if ic.Constraints.CpuCores == nil {
   359  		ic.Constraints.CpuCores = &defaultCpuCores
   360  	}
   361  	allInstanceTypes, err := env.listInstanceTypes()
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  	images := instances.ImageMetadataToImages(imageMetadata)
   366  	spec, err := instances.FindInstanceSpec(images, ic, allInstanceTypes)
   367  	if err != nil {
   368  		return nil, err
   369  	}
   370  	return spec, nil
   371  }