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