Permalink
Switch branches/tags
Find file
Fetching contributors…
Cannot retrieve contributors at this time
362 lines (309 sloc) 9.87 KB
// Copyright 2013 Joyent Inc.
// Licensed under the AGPLv3, see LICENCE file for details.
package joyent
import (
"fmt"
"strings"
"sync"
"time"
"github.com/joyent/gocommon/client"
"github.com/joyent/gosdc/cloudapi"
"github.com/juju/errors"
"github.com/juju/utils/arch"
"gopkg.in/juju/names.v2"
"github.com/juju/juju/cloudconfig/cloudinit"
"github.com/juju/juju/cloudconfig/instancecfg"
"github.com/juju/juju/cloudconfig/providerinit"
"github.com/juju/juju/constraints"
"github.com/juju/juju/environs"
"github.com/juju/juju/environs/imagemetadata"
"github.com/juju/juju/environs/instances"
"github.com/juju/juju/environs/tags"
"github.com/juju/juju/instance"
"github.com/juju/juju/tools"
)
var (
vTypeSmartmachine = "smartmachine"
vTypeVirtualmachine = "kvm"
defaultCpuCores = uint64(1)
)
type joyentCompute struct {
cloudapi *cloudapi.Client
}
func newCompute(cloud environs.CloudSpec) (*joyentCompute, error) {
creds, err := credentials(cloud)
if err != nil {
return nil, err
}
client := client.NewClient(cloud.Endpoint, cloudapi.DefaultAPIVersion, creds, newGoLogger())
return &joyentCompute{cloudapi: cloudapi.New(client)}, nil
}
func (env *joyentEnviron) machineFullName(machineId string) string {
return fmt.Sprintf("juju-%s-%s", env.name, names.NewMachineTag(machineId))
}
var unsupportedConstraints = []string{
constraints.CpuPower,
constraints.Tags,
constraints.VirtType,
}
// ConstraintsValidator is defined on the Environs interface.
func (env *joyentEnviron) ConstraintsValidator() (constraints.Validator, error) {
validator := constraints.NewValidator()
validator.RegisterUnsupported(unsupportedConstraints)
packages, err := env.compute.cloudapi.ListPackages(nil)
if err != nil {
return nil, err
}
instTypeNames := make([]string, len(packages))
for i, pkg := range packages {
instTypeNames[i] = pkg.Name
}
validator.RegisterVocabulary(constraints.InstanceType, instTypeNames)
return validator, nil
}
// MaintainInstance is specified in the InstanceBroker interface.
func (*joyentEnviron) MaintainInstance(args environs.StartInstanceParams) error {
return nil
}
func (env *joyentEnviron) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) {
series := args.Tools.OneSeries()
arches := args.Tools.Arches()
spec, err := env.FindInstanceSpec(&instances.InstanceConstraint{
Region: env.cloud.Region,
Series: series,
Arches: arches,
Constraints: args.Constraints,
}, args.ImageMetadata)
if err != nil {
return nil, err
}
tools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch})
if err != nil {
return nil, errors.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches)
}
if err := args.InstanceConfig.SetTools(tools); err != nil {
return nil, errors.Trace(err)
}
if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config()); err != nil {
return nil, err
}
// This is a hack that ensures that instances can communicate over
// the internal network. Joyent sometimes gives instances
// different 10.x.x.x/21 networks and adding this route allows
// them to talk despite this. See:
// https://bugs.launchpad.net/juju-core/+bug/1401130
cloudcfg, err := cloudinit.New(args.InstanceConfig.Series)
if err != nil {
return nil, errors.Annotate(err, "cannot create cloudinit template")
}
ifupScript := `
#!/bin/bash
# These guards help to ensure that this hack only runs if Joyent's
# internal network still works as it does at time of writing.
[ "$IFACE" == "eth1" ] || [ "$IFACE" == "--all" ] || exit 0
/sbin/ip -4 --oneline addr show dev eth1 | fgrep --quiet " inet 10." || exit 0
/sbin/ip route add 10.0.0.0/8 dev eth1
`[1:]
cloudcfg.AddBootTextFile("/etc/network/if-up.d/joyent", ifupScript, 0755)
userData, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, JoyentRenderer{})
if err != nil {
return nil, errors.Annotate(err, "cannot make user data")
}
logger.Debugf("joyent user data: %d bytes", len(userData))
instanceTags := make(map[string]string)
for tag, value := range args.InstanceConfig.Tags {
instanceTags[tagKey(tag)] = value
}
instanceTags[tagKey("group")] = "juju"
instanceTags[tagKey("model")] = env.Config().Name()
args.InstanceConfig.Tags = instanceTags
logger.Debugf("Now tags are: %+v", args.InstanceConfig.Tags)
var machine *cloudapi.Machine
machine, err = env.compute.cloudapi.CreateMachine(cloudapi.CreateMachineOpts{
Package: spec.InstanceType.Name,
Image: spec.Image.Id,
Metadata: map[string]string{"metadata.cloud-init:user-data": string(userData)},
Tags: args.InstanceConfig.Tags,
})
if err != nil {
return nil, errors.Annotate(err, "cannot create instances")
}
machineId := machine.Id
logger.Infof("provisioning instance %q", machineId)
logger.Infof("machine created with tags %+v", machine.Tags)
machine, err = env.compute.cloudapi.GetMachine(machineId)
if err != nil {
return nil, errors.Annotate(err, "cannot start instances")
}
// wait for machine to start
for !strings.EqualFold(machine.State, "running") {
time.Sleep(1 * time.Second)
machine, err = env.compute.cloudapi.GetMachine(machineId)
if err != nil {
return nil, errors.Annotate(err, "cannot start instances")
}
}
logger.Infof("started instance %q", machineId)
inst := &joyentInstance{
machine: machine,
env: env,
}
disk64 := uint64(machine.Disk)
hc := instance.HardwareCharacteristics{
Arch: &spec.Image.Arch,
Mem: &spec.InstanceType.Mem,
CpuCores: &spec.InstanceType.CpuCores,
CpuPower: spec.InstanceType.CpuPower,
RootDisk: &disk64,
}
return &environs.StartInstanceResult{
Instance: inst,
Hardware: &hc,
}, nil
}
// Joyent tag must be prefixed with "tag."
func tagKey(aKey string) string {
return "tag." + aKey
}
func (env *joyentEnviron) AllInstances() ([]instance.Instance, error) {
instances := []instance.Instance{}
filter := cloudapi.NewFilter()
filter.Set(tagKey("group"), "juju")
filter.Set(tagKey(tags.JujuModel), env.Config().UUID())
machines, err := env.compute.cloudapi.ListMachines(filter)
if err != nil {
return nil, errors.Annotate(err, "cannot retrieve instances")
}
for _, m := range machines {
if strings.EqualFold(m.State, "provisioning") || strings.EqualFold(m.State, "running") {
copy := m
instances = append(instances, &joyentInstance{machine: &copy, env: env})
}
}
return instances, nil
}
func (env *joyentEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) {
if len(ids) == 0 {
return nil, nil
}
logger.Debugf("Looking for instances %q", ids)
instances := make([]instance.Instance, len(ids))
found := 0
allInstances, err := env.AllInstances()
if err != nil {
return nil, err
}
for i, id := range ids {
for _, instance := range allInstances {
if instance.Id() == id {
instances[i] = instance
found++
}
}
}
logger.Debugf("Found %d instances %q", found, instances)
if found == 0 {
return nil, environs.ErrNoInstances
} else if found < len(ids) {
return instances, environs.ErrPartialInstances
}
return instances, nil
}
func (env *joyentEnviron) StopInstances(ids ...instance.Id) error {
// Remove all the instances in parallel so that we incur less round-trips.
var wg sync.WaitGroup
//var err error
wg.Add(len(ids))
errc := make(chan error, len(ids))
for _, id := range ids {
id := id // copy to new free var for closure
go func() {
defer wg.Done()
if err := env.stopInstance(string(id)); err != nil {
errc <- err
}
}()
}
wg.Wait()
select {
case err := <-errc:
return errors.Annotate(err, "cannot stop all instances")
default:
}
return nil
}
func (env *joyentEnviron) stopInstance(id string) error {
// wait for machine to be running
// if machine is still provisioning stop will fail
for !env.pollMachineState(id, "running") {
time.Sleep(1 * time.Second)
}
err := env.compute.cloudapi.StopMachine(id)
if err != nil {
return errors.Annotatef(err, "cannot stop instance %v", id)
}
// wait for machine to be stopped
for !env.pollMachineState(id, "stopped") {
time.Sleep(1 * time.Second)
}
err = env.compute.cloudapi.DeleteMachine(id)
if err != nil {
return errors.Annotatef(err, "cannot delete instance %v", id)
}
return nil
}
func (env *joyentEnviron) pollMachineState(machineId, state string) bool {
instanceConfig, err := env.compute.cloudapi.GetMachine(machineId)
if err != nil {
return false
}
return strings.EqualFold(instanceConfig.State, state)
}
func (env *joyentEnviron) listInstanceTypes() ([]instances.InstanceType, error) {
packages, err := env.compute.cloudapi.ListPackages(nil)
if err != nil {
return nil, err
}
allInstanceTypes := []instances.InstanceType{}
for _, pkg := range packages {
// ListPackages does not include the virt type of the package.
// However, Joyent says the smart packages have zero VCPUs.
var virtType *string
if pkg.VCPUs > 0 {
virtType = &vTypeVirtualmachine
} else {
virtType = &vTypeSmartmachine
}
instanceType := instances.InstanceType{
Id: pkg.Id,
Name: pkg.Name,
Arches: []string{arch.AMD64},
Mem: uint64(pkg.Memory),
CpuCores: uint64(pkg.VCPUs),
RootDisk: uint64(pkg.Disk * 1024),
VirtType: virtType,
}
allInstanceTypes = append(allInstanceTypes, instanceType)
}
return allInstanceTypes, nil
}
// FindInstanceSpec returns an InstanceSpec satisfying the supplied instanceConstraint.
func (env *joyentEnviron) FindInstanceSpec(
ic *instances.InstanceConstraint,
imageMetadata []*imagemetadata.ImageMetadata,
) (*instances.InstanceSpec, error) {
// Require at least one VCPU so we get KVM rather than smart package.
if ic.Constraints.CpuCores == nil {
ic.Constraints.CpuCores = &defaultCpuCores
}
allInstanceTypes, err := env.listInstanceTypes()
if err != nil {
return nil, err
}
images := instances.ImageMetadataToImages(imageMetadata)
spec, err := instances.FindInstanceSpec(images, ic, allInstanceTypes)
if err != nil {
return nil, err
}
return spec, nil
}