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