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