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