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: ©, 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 }