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