github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/gce/environ_broker.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package gce 5 6 import ( 7 "fmt" 8 9 "github.com/juju/errors" 10 jujuos "github.com/juju/os" 11 "github.com/juju/os/series" 12 "github.com/juju/utils" 13 14 "github.com/juju/juju/cloudconfig/instancecfg" 15 "github.com/juju/juju/cloudconfig/providerinit" 16 "github.com/juju/juju/core/constraints" 17 "github.com/juju/juju/core/instance" 18 "github.com/juju/juju/environs" 19 "github.com/juju/juju/environs/context" 20 "github.com/juju/juju/environs/imagemetadata" 21 "github.com/juju/juju/environs/instances" 22 "github.com/juju/juju/provider/common" 23 "github.com/juju/juju/provider/gce/google" 24 "github.com/juju/juju/tools" 25 ) 26 27 // MaintainInstance is specified in the InstanceBroker interface. 28 func (*environ) MaintainInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) error { 29 return nil 30 } 31 32 // StartInstance implements environs.InstanceBroker. 33 func (env *environ) StartInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 34 // Start a new instance. 35 36 spec, err := buildInstanceSpec(env, args) 37 if err != nil { 38 return nil, common.ZoneIndependentError(err) 39 } 40 41 if err := env.finishInstanceConfig(args, spec); err != nil { 42 return nil, common.ZoneIndependentError(err) 43 } 44 45 // Validate availability zone. 46 volumeAttachmentsZone, err := volumeAttachmentsZone(args.VolumeAttachments) 47 if err != nil { 48 return nil, common.ZoneIndependentError(err) 49 } 50 if err := validateAvailabilityZoneConsistency(args.AvailabilityZone, volumeAttachmentsZone); err != nil { 51 return nil, errors.Trace(err) 52 } 53 54 raw, err := newRawInstance(env, ctx, args, spec) 55 if err != nil { 56 return nil, errors.Trace(err) 57 } 58 logger.Infof("started instance %q in zone %q", raw.ID, raw.ZoneName) 59 inst := newInstance(raw, env) 60 61 // Build the result. 62 hwc := getHardwareCharacteristics(env, spec, inst) 63 result := environs.StartInstanceResult{ 64 Instance: inst, 65 Hardware: hwc, 66 } 67 return &result, nil 68 } 69 70 var buildInstanceSpec = func(env *environ, args environs.StartInstanceParams) (*instances.InstanceSpec, error) { 71 return env.buildInstanceSpec(args) 72 } 73 74 var newRawInstance = func(env *environ, ctx context.ProviderCallContext, args environs.StartInstanceParams, spec *instances.InstanceSpec) (*google.Instance, error) { 75 return env.newRawInstance(ctx, args, spec) 76 } 77 78 var getHardwareCharacteristics = func(env *environ, spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics { 79 return env.getHardwareCharacteristics(spec, inst) 80 } 81 82 // finishInstanceConfig updates args.InstanceConfig in place. Setting up 83 // the API, StateServing, and SSHkeys information. 84 func (env *environ) finishInstanceConfig(args environs.StartInstanceParams, spec *instances.InstanceSpec) error { 85 envTools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch}) 86 if err != nil { 87 return errors.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) 88 } 89 90 if err := args.InstanceConfig.SetTools(envTools); err != nil { 91 return errors.Trace(err) 92 } 93 return instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config()) 94 } 95 96 // buildInstanceSpec builds an instance spec from the provided args 97 // and returns it. This includes pulling the simplestreams data for the 98 // machine type, region, and other constraints. 99 func (env *environ) buildInstanceSpec(args environs.StartInstanceParams) (*instances.InstanceSpec, error) { 100 arches := args.Tools.Arches() 101 series := args.Tools.OneSeries() 102 spec, err := findInstanceSpec( 103 env, &instances.InstanceConstraint{ 104 Region: env.cloud.Region, 105 Series: series, 106 Arches: arches, 107 Constraints: args.Constraints, 108 }, 109 args.ImageMetadata, 110 ) 111 return spec, errors.Trace(err) 112 } 113 114 var findInstanceSpec = func( 115 env *environ, 116 ic *instances.InstanceConstraint, 117 imageMetadata []*imagemetadata.ImageMetadata, 118 ) (*instances.InstanceSpec, error) { 119 return env.findInstanceSpec(ic, imageMetadata) 120 } 121 122 // findInstanceSpec initializes a new instance spec for the given 123 // constraints and returns it. This only covers populating the 124 // initial data for the spec. 125 func (env *environ) findInstanceSpec( 126 ic *instances.InstanceConstraint, 127 imageMetadata []*imagemetadata.ImageMetadata, 128 ) (*instances.InstanceSpec, error) { 129 images := instances.ImageMetadataToImages(imageMetadata) 130 spec, err := instances.FindInstanceSpec(images, ic, allInstanceTypes) 131 return spec, errors.Trace(err) 132 } 133 134 // newRawInstance is where the new physical instance is actually 135 // provisioned, relative to the provided args and spec. Info for that 136 // low-level instance is returned. 137 func (env *environ) newRawInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams, spec *instances.InstanceSpec) (_ *google.Instance, err error) { 138 139 hostname, err := env.namespace.Hostname(args.InstanceConfig.MachineId) 140 if err != nil { 141 return nil, common.ZoneIndependentError(err) 142 } 143 144 os, err := series.GetOSFromSeries(args.InstanceConfig.Series) 145 if err != nil { 146 return nil, common.ZoneIndependentError(err) 147 } 148 149 metadata, err := getMetadata(args, os) 150 if err != nil { 151 return nil, common.ZoneIndependentError(err) 152 } 153 tags := []string{ 154 env.globalFirewallName(), 155 hostname, 156 } 157 158 disks, err := getDisks( 159 spec, args.Constraints, 160 args.InstanceConfig.Series, 161 env.Config().UUID(), 162 env.Config().ImageStream() == "daily", 163 ) 164 if err != nil { 165 return nil, common.ZoneIndependentError(err) 166 } 167 168 // TODO(ericsnow) Use the env ID for the network name (instead of default)? 169 // TODO(ericsnow) Make the network name configurable? 170 // TODO(ericsnow) Support multiple networks? 171 // TODO(ericsnow) Use a different net interface name? Configurable? 172 inst, err := env.gce.AddInstance(google.InstanceSpec{ 173 ID: hostname, 174 Type: spec.InstanceType.Name, 175 Disks: disks, 176 NetworkInterfaces: []string{"ExternalNAT"}, 177 Metadata: metadata, 178 Tags: tags, 179 AvailabilityZone: args.AvailabilityZone, 180 // Network is omitted (left empty). 181 }) 182 if err != nil { 183 // We currently treat all AddInstance failures 184 // as being zone-specific, so we'll retry in 185 // another zone. 186 return nil, google.HandleCredentialError(errors.Trace(err), ctx) 187 } 188 return inst, nil 189 } 190 191 // getMetadata builds the raw "user-defined" metadata for the new 192 // instance (relative to the provided args) and returns it. 193 func getMetadata(args environs.StartInstanceParams, os jujuos.OSType) (map[string]string, error) { 194 userData, err := providerinit.ComposeUserData(args.InstanceConfig, nil, GCERenderer{}) 195 if err != nil { 196 return nil, errors.Annotate(err, "cannot make user data") 197 } 198 logger.Debugf("GCE user data; %d bytes", len(userData)) 199 200 metadata := make(map[string]string) 201 for tag, value := range args.InstanceConfig.Tags { 202 metadata[tag] = value 203 } 204 switch os { 205 case jujuos.Ubuntu: 206 // We store a gz snapshop of information that is used by 207 // cloud-init and unpacked in to the /var/lib/cloud/instances folder 208 // for the instance. Due to a limitation with GCE and binary blobs 209 // we base64 encode the data before storing it. 210 metadata[metadataKeyCloudInit] = string(userData) 211 // Valid encoding values are determined by the cloudinit GCE data source. 212 // See: http://cloudinit.readthedocs.org 213 metadata[metadataKeyEncoding] = "base64" 214 215 case jujuos.Windows: 216 metadata[metadataKeyWindowsUserdata] = string(userData) 217 218 validChars := append(utils.UpperAlpha, append(utils.LowerAlpha, utils.Digits...)...) 219 220 // The hostname must have maximum 15 characters 221 winHostname := "juju" + utils.RandomString(11, validChars) 222 metadata[metadataKeyWindowsSysprep] = fmt.Sprintf(winSetHostnameScript, winHostname) 223 default: 224 return nil, errors.Errorf("cannot pack metadata for os %s on the gce provider", os.String()) 225 } 226 227 return metadata, nil 228 } 229 230 // getDisks builds the raw spec for the disks that should be attached to 231 // the new instances and returns it. This will always include a root 232 // disk with characteristics determined by the provides args and 233 // constraints. 234 func getDisks(spec *instances.InstanceSpec, cons constraints.Value, ser, eUUID string, daily bool) ([]google.DiskSpec, error) { 235 size := common.MinRootDiskSizeGiB(ser) 236 if cons.RootDisk != nil && *cons.RootDisk > size { 237 size = common.MiBToGiB(*cons.RootDisk) 238 } 239 var imageURL string 240 os, err := series.GetOSFromSeries(ser) 241 if err != nil { 242 return nil, errors.Trace(err) 243 } 244 switch os { 245 case jujuos.Ubuntu: 246 if daily { 247 imageURL = ubuntuDailyImageBasePath 248 } else { 249 imageURL = ubuntuImageBasePath 250 } 251 case jujuos.Windows: 252 imageURL = windowsImageBasePath 253 default: 254 return nil, errors.Errorf("os %s is not supported on the gce provider", os.String()) 255 } 256 dSpec := google.DiskSpec{ 257 Series: ser, 258 SizeHintGB: size, 259 ImageURL: imageURL + spec.Image.Id, 260 Boot: true, 261 AutoDelete: true, 262 } 263 if cons.RootDisk != nil && dSpec.TooSmall() { 264 msg := "Ignoring root-disk constraint of %dM because it is smaller than the GCE image size of %dG" 265 logger.Infof(msg, *cons.RootDisk, google.MinDiskSizeGB(ser)) 266 } 267 return []google.DiskSpec{dSpec}, nil 268 } 269 270 // getHardwareCharacteristics compiles hardware-related details about 271 // the given instance and relative to the provided spec and returns it. 272 func (env *environ) getHardwareCharacteristics(spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics { 273 rootDiskMB := inst.base.RootDiskGB() * 1024 274 hwc := instance.HardwareCharacteristics{ 275 Arch: &spec.Image.Arch, 276 Mem: &spec.InstanceType.Mem, 277 CpuCores: &spec.InstanceType.CpuCores, 278 CpuPower: spec.InstanceType.CpuPower, 279 RootDisk: &rootDiskMB, 280 AvailabilityZone: &inst.base.ZoneName, 281 // Tags: not supported in GCE. 282 } 283 return &hwc 284 } 285 286 // AllInstances implements environs.InstanceBroker. 287 func (env *environ) AllInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) { 288 instances, err := getInstances(env, ctx) 289 return instances, errors.Trace(err) 290 } 291 292 // StopInstances implements environs.InstanceBroker. 293 func (env *environ) StopInstances(ctx context.ProviderCallContext, instances ...instance.Id) error { 294 var ids []string 295 for _, id := range instances { 296 ids = append(ids, string(id)) 297 } 298 299 prefix := env.namespace.Prefix() 300 err := env.gce.RemoveInstances(prefix, ids...) 301 return google.HandleCredentialError(errors.Trace(err), ctx) 302 }