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