github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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 "strings" 8 9 "github.com/juju/errors" 10 11 "github.com/juju/juju/cloudconfig/instancecfg" 12 "github.com/juju/juju/cloudconfig/providerinit" 13 "github.com/juju/juju/core/constraints" 14 "github.com/juju/juju/core/instance" 15 "github.com/juju/juju/core/os/ostype" 16 "github.com/juju/juju/environs" 17 "github.com/juju/juju/environs/context" 18 "github.com/juju/juju/environs/imagemetadata" 19 "github.com/juju/juju/environs/instances" 20 "github.com/juju/juju/provider/common" 21 "github.com/juju/juju/provider/gce/google" 22 ) 23 24 // StartInstance implements environs.InstanceBroker. 25 func (env *environ) StartInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 26 // Start a new instance. 27 28 spec, err := buildInstanceSpec(env, ctx, args) 29 if err != nil { 30 return nil, environs.ZoneIndependentError(err) 31 } 32 33 if err := env.finishInstanceConfig(args, spec); err != nil { 34 return nil, environs.ZoneIndependentError(err) 35 } 36 37 // Validate availability zone. 38 volumeAttachmentsZone, err := volumeAttachmentsZone(args.VolumeAttachments) 39 if err != nil { 40 return nil, environs.ZoneIndependentError(err) 41 } 42 if err := validateAvailabilityZoneConsistency(args.AvailabilityZone, volumeAttachmentsZone); err != nil { 43 return nil, errors.Trace(err) 44 } 45 46 raw, err := newRawInstance(env, ctx, args, spec) 47 if err != nil { 48 return nil, errors.Trace(err) 49 } 50 logger.Infof("started instance %q in zone %q", raw.ID, raw.ZoneName) 51 inst := newInstance(raw, env) 52 53 // Build the result. 54 hwc := getHardwareCharacteristics(env, spec, inst) 55 result := environs.StartInstanceResult{ 56 Instance: inst, 57 Hardware: hwc, 58 } 59 return &result, nil 60 } 61 62 var buildInstanceSpec = func(env *environ, ctx context.ProviderCallContext, args environs.StartInstanceParams) (*instances.InstanceSpec, error) { 63 return env.buildInstanceSpec(ctx, args) 64 } 65 66 var newRawInstance = func(env *environ, ctx context.ProviderCallContext, args environs.StartInstanceParams, spec *instances.InstanceSpec) (*google.Instance, error) { 67 return env.newRawInstance(ctx, args, spec) 68 } 69 70 var getHardwareCharacteristics = func(env *environ, spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics { 71 return env.getHardwareCharacteristics(spec, inst) 72 } 73 74 // finishInstanceConfig updates args.InstanceConfig in place. Setting up 75 // the API, StateServing, and SSHkeys information. 76 func (env *environ) finishInstanceConfig(args environs.StartInstanceParams, spec *instances.InstanceSpec) error { 77 if err := args.InstanceConfig.SetTools(args.Tools); err != nil { 78 return errors.Trace(err) 79 } 80 return instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config()) 81 } 82 83 // buildInstanceSpec builds an instance spec from the provided args 84 // and returns it. This includes pulling the simplestreams data for the 85 // machine type, region, and other constraints. 86 func (env *environ) buildInstanceSpec(ctx context.ProviderCallContext, args environs.StartInstanceParams) (*instances.InstanceSpec, error) { 87 instTypesAndCosts, err := env.InstanceTypes(ctx, constraints.Value{}) 88 if err != nil { 89 return nil, errors.Trace(err) 90 } 91 92 arch, err := args.Tools.OneArch() 93 if err != nil { 94 return nil, errors.Trace(err) 95 } 96 spec, err := findInstanceSpec( 97 env, &instances.InstanceConstraint{ 98 Region: env.cloud.Region, 99 Base: args.InstanceConfig.Base, 100 Arch: arch, 101 Constraints: args.Constraints, 102 }, 103 args.ImageMetadata, 104 instTypesAndCosts.InstanceTypes, 105 ) 106 return spec, errors.Trace(err) 107 } 108 109 var findInstanceSpec = func( 110 env *environ, 111 ic *instances.InstanceConstraint, 112 imageMetadata []*imagemetadata.ImageMetadata, 113 allInstanceTypes []instances.InstanceType, 114 ) (*instances.InstanceSpec, error) { 115 return env.findInstanceSpec(ic, imageMetadata, allInstanceTypes) 116 } 117 118 // findInstanceSpec initializes a new instance spec for the given 119 // constraints and returns it. This only covers populating the 120 // initial data for the spec. 121 func (env *environ) findInstanceSpec( 122 ic *instances.InstanceConstraint, 123 imageMetadata []*imagemetadata.ImageMetadata, 124 allInstanceTypes []instances.InstanceType, 125 ) (*instances.InstanceSpec, error) { 126 images := instances.ImageMetadataToImages(imageMetadata) 127 spec, err := instances.FindInstanceSpec(images, ic, allInstanceTypes) 128 return spec, errors.Trace(err) 129 } 130 131 func (env *environ) imageURLBase(os ostype.OSType) (string, error) { 132 base, useCustomPath := env.ecfg.baseImagePath() 133 if useCustomPath { 134 return base, nil 135 } 136 137 switch os { 138 case ostype.Ubuntu: 139 switch env.Config().ImageStream() { 140 case "daily": 141 base = ubuntuDailyImageBasePath 142 case "pro": 143 base = ubuntuProImageBasePath 144 default: 145 base = ubuntuImageBasePath 146 } 147 default: 148 return "", errors.Errorf("os %s is not supported on the gce provider", os.String()) 149 } 150 151 return base, nil 152 } 153 154 // newRawInstance is where the new physical instance is actually 155 // provisioned, relative to the provided args and spec. Info for that 156 // low-level instance is returned. 157 func (env *environ) newRawInstance( 158 ctx context.ProviderCallContext, args environs.StartInstanceParams, spec *instances.InstanceSpec, 159 ) (_ *google.Instance, err error) { 160 hostname, err := env.namespace.Hostname(args.InstanceConfig.MachineId) 161 if err != nil { 162 return nil, environs.ZoneIndependentError(err) 163 } 164 165 os := ostype.OSTypeForName(args.InstanceConfig.Base.OS) 166 metadata, err := getMetadata(args, os) 167 if err != nil { 168 return nil, environs.ZoneIndependentError(err) 169 } 170 tags := []string{ 171 env.globalFirewallName(), 172 hostname, 173 } 174 175 imageURLBase, err := env.imageURLBase(os) 176 if err != nil { 177 return nil, environs.ZoneIndependentError(err) 178 } 179 180 disks, err := getDisks( 181 spec, args.Constraints, 182 os, 183 env.Config().UUID(), 184 imageURLBase, 185 ) 186 if err != nil { 187 return nil, environs.ZoneIndependentError(err) 188 } 189 190 allocatePublicIP := true 191 if args.Constraints.HasAllocatePublicIP() { 192 allocatePublicIP = *args.Constraints.AllocatePublicIP 193 } 194 195 inst, err := env.gce.AddInstance(google.InstanceSpec{ 196 ID: hostname, 197 Type: spec.InstanceType.Name, 198 Disks: disks, 199 NetworkInterfaces: []string{"ExternalNAT"}, 200 Metadata: metadata, 201 Tags: tags, 202 AvailabilityZone: args.AvailabilityZone, 203 AllocatePublicIP: allocatePublicIP, 204 }) 205 if err != nil { 206 // We currently treat all AddInstance failures 207 // as being zone-specific, so we'll retry in 208 // another zone. 209 return nil, google.HandleCredentialError(errors.Trace(err), ctx) 210 } 211 212 return inst, nil 213 } 214 215 // getMetadata builds the raw "user-defined" metadata for the new 216 // instance (relative to the provided args) and returns it. 217 func getMetadata(args environs.StartInstanceParams, os ostype.OSType) (map[string]string, error) { 218 userData, err := providerinit.ComposeUserData(args.InstanceConfig, nil, GCERenderer{}) 219 if err != nil { 220 return nil, errors.Annotate(err, "cannot make user data") 221 } 222 logger.Debugf("GCE user data; %d bytes", len(userData)) 223 224 metadata := make(map[string]string) 225 for tag, value := range args.InstanceConfig.Tags { 226 metadata[tag] = value 227 } 228 switch os { 229 case ostype.Ubuntu: 230 // We store a gz snapshop of information that is used by 231 // cloud-init and unpacked in to the /var/lib/cloud/instances folder 232 // for the instance. Due to a limitation with GCE and binary blobs 233 // we base64 encode the data before storing it. 234 metadata[metadataKeyCloudInit] = string(userData) 235 // Valid encoding values are determined by the cloudinit GCE data source. 236 // See: http://cloudinit.readthedocs.org 237 metadata[metadataKeyEncoding] = "base64" 238 239 default: 240 return nil, errors.Errorf("cannot pack metadata for os %s on the gce provider", os.String()) 241 } 242 243 return metadata, nil 244 } 245 246 // getDisks builds the raw spec for the disks that should be attached to 247 // the new instances and returns it. This will always include a root 248 // disk with characteristics determined by the provides args and 249 // constraints. 250 func getDisks(spec *instances.InstanceSpec, cons constraints.Value, os ostype.OSType, eUUID string, imageURLBase string) ([]google.DiskSpec, error) { 251 size := common.MinRootDiskSizeGiB(os) 252 if cons.RootDisk != nil && *cons.RootDisk > size { 253 size = common.MiBToGiB(*cons.RootDisk) 254 } 255 if imageURLBase == "" { 256 return nil, errors.NotValidf("imageURLBase must be set") 257 } 258 imageURL := imageURLBase + spec.Image.Id 259 logger.Infof("fetching disk image from %v", imageURL) 260 dSpec := google.DiskSpec{ 261 OS: strings.ToLower(os.String()), 262 SizeHintGB: size, 263 ImageURL: imageURL, 264 Boot: true, 265 AutoDelete: true, 266 } 267 if cons.RootDisk != nil && dSpec.TooSmall() { 268 msg := "Ignoring root-disk constraint of %dM because it is smaller than the GCE image size of %dG" 269 logger.Infof(msg, *cons.RootDisk, google.MinDiskSizeGB) 270 } 271 return []google.DiskSpec{dSpec}, nil 272 } 273 274 // getHardwareCharacteristics compiles hardware-related details about 275 // the given instance and relative to the provided spec and returns it. 276 func (env *environ) getHardwareCharacteristics(spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics { 277 rootDiskMB := inst.base.RootDiskGB() * 1024 278 hwc := instance.HardwareCharacteristics{ 279 Arch: &spec.Image.Arch, 280 Mem: &spec.InstanceType.Mem, 281 CpuCores: &spec.InstanceType.CpuCores, 282 CpuPower: spec.InstanceType.CpuPower, 283 RootDisk: &rootDiskMB, 284 AvailabilityZone: &inst.base.ZoneName, 285 // Tags: not supported in GCE. 286 } 287 return &hwc 288 } 289 290 // AllInstances implements environs.InstanceBroker. 291 func (env *environ) AllInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) { 292 // We want all statuses here except for "terminated" - these instances are truly dead to us. 293 // According to https://cloud.google.com/compute/docs/instances/instance-life-cycle 294 // there are now only "provisioning", "staging", "running", "stopping" and "terminated" states. 295 // The others might have been needed for older versions of gce... Keeping here for potential 296 // backward compatibility. 297 nonLiveStatuses := []string{ 298 google.StatusDone, 299 google.StatusDown, 300 google.StatusProvisioning, 301 google.StatusStopped, 302 google.StatusStopping, 303 google.StatusUp, 304 } 305 filters := append(instStatuses, nonLiveStatuses...) 306 instances, err := getInstances(env, ctx, filters...) 307 return instances, errors.Trace(err) 308 } 309 310 // AllRunningInstances implements environs.InstanceBroker. 311 func (env *environ) AllRunningInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) { 312 instances, err := getInstances(env, ctx) 313 return instances, errors.Trace(err) 314 } 315 316 // StopInstances implements environs.InstanceBroker. 317 func (env *environ) StopInstances(ctx context.ProviderCallContext, instances ...instance.Id) error { 318 var ids []string 319 for _, id := range instances { 320 ids = append(ids, string(id)) 321 } 322 323 prefix := env.namespace.Prefix() 324 err := env.gce.RemoveInstances(prefix, ids...) 325 return google.HandleCredentialError(errors.Trace(err), ctx) 326 }