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