github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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 "github.com/juju/utils" 11 jujuos "github.com/juju/utils/os" 12 "github.com/juju/utils/series" 13 14 "github.com/juju/juju/cloudconfig/instancecfg" 15 "github.com/juju/juju/cloudconfig/providerinit" 16 "github.com/juju/juju/constraints" 17 "github.com/juju/juju/environs" 18 "github.com/juju/juju/environs/imagemetadata" 19 "github.com/juju/juju/environs/instances" 20 "github.com/juju/juju/environs/simplestreams" 21 "github.com/juju/juju/instance" 22 "github.com/juju/juju/network" 23 "github.com/juju/juju/provider/common" 24 "github.com/juju/juju/provider/gce/google" 25 "github.com/juju/juju/state/multiwatcher" 26 "github.com/juju/juju/tools" 27 ) 28 29 func isStateServer(icfg *instancecfg.InstanceConfig) bool { 30 return multiwatcher.AnyJobNeedsState(icfg.Jobs...) 31 } 32 33 // MaintainInstance is specified in the InstanceBroker interface. 34 func (*environ) MaintainInstance(args environs.StartInstanceParams) error { 35 return nil 36 } 37 38 // StartInstance implements environs.InstanceBroker. 39 func (env *environ) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 40 // Please note that in order to fulfil the demands made of Instances and 41 // AllInstances, it is imperative that some environment feature be used to 42 // keep track of which instances were actually started by juju. 43 env = env.getSnapshot() 44 45 // Start a new instance. 46 47 if args.InstanceConfig.HasNetworks() { 48 return nil, errors.New("starting instances with networks is not supported yet") 49 } 50 51 spec, err := buildInstanceSpec(env, args) 52 if err != nil { 53 return nil, errors.Trace(err) 54 } 55 56 if err := env.finishInstanceConfig(args, spec); err != nil { 57 return nil, errors.Trace(err) 58 } 59 60 raw, err := newRawInstance(env, args, spec) 61 if err != nil { 62 return nil, errors.Trace(err) 63 } 64 logger.Infof("started instance %q in zone %q", raw.ID, raw.ZoneName) 65 inst := newInstance(raw, env) 66 67 // Ensure the API server port is open (globally for all instances 68 // on the network, not just for the specific node of the state 69 // server). See LP bug #1436191 for details. 70 if isStateServer(args.InstanceConfig) { 71 ports := network.PortRange{ 72 FromPort: args.InstanceConfig.StateServingInfo.APIPort, 73 ToPort: args.InstanceConfig.StateServingInfo.APIPort, 74 Protocol: "tcp", 75 } 76 if err := env.gce.OpenPorts(env.globalFirewallName(), ports); err != nil { 77 return nil, errors.Trace(err) 78 } 79 } 80 81 // Build the result. 82 hwc := getHardwareCharacteristics(env, spec, inst) 83 result := environs.StartInstanceResult{ 84 Instance: inst, 85 Hardware: hwc, 86 } 87 return &result, nil 88 } 89 90 var buildInstanceSpec = func(env *environ, args environs.StartInstanceParams) (*instances.InstanceSpec, error) { 91 return env.buildInstanceSpec(args) 92 } 93 94 var newRawInstance = func(env *environ, args environs.StartInstanceParams, spec *instances.InstanceSpec) (*google.Instance, error) { 95 return env.newRawInstance(args, spec) 96 } 97 98 var getHardwareCharacteristics = func(env *environ, spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics { 99 return env.getHardwareCharacteristics(spec, inst) 100 } 101 102 // finishInstanceConfig updates args.InstanceConfig in place. Setting up 103 // the API, StateServing, and SSHkeys information. 104 func (env *environ) finishInstanceConfig(args environs.StartInstanceParams, spec *instances.InstanceSpec) error { 105 envTools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch}) 106 if err != nil { 107 return errors.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) 108 } 109 110 args.InstanceConfig.Tools = envTools[0] 111 return instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config()) 112 } 113 114 // buildInstanceSpec builds an instance spec from the provided args 115 // and returns it. This includes pulling the simplestreams data for the 116 // machine type, region, and other constraints. 117 func (env *environ) buildInstanceSpec(args environs.StartInstanceParams) (*instances.InstanceSpec, error) { 118 arches := args.Tools.Arches() 119 series := args.Tools.OneSeries() 120 spec, err := findInstanceSpec(env, env.Config().ImageStream(), &instances.InstanceConstraint{ 121 Region: env.ecfg.region(), 122 Series: series, 123 Arches: arches, 124 Constraints: args.Constraints, 125 }) 126 return spec, errors.Trace(err) 127 } 128 129 var findInstanceSpec = func(env *environ, stream string, ic *instances.InstanceConstraint) (*instances.InstanceSpec, error) { 130 return env.findInstanceSpec(stream, ic) 131 } 132 133 // findInstanceSpec initializes a new instance spec for the given stream 134 // (and constraints) and returns it. This only covers populating the 135 // initial data for the spec. However, it does include fetching the 136 // correct simplestreams image data. 137 func (env *environ) findInstanceSpec(stream string, ic *instances.InstanceConstraint) (*instances.InstanceSpec, error) { 138 sources, err := environs.ImageMetadataSources(env) 139 if err != nil { 140 return nil, errors.Trace(err) 141 } 142 143 imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ 144 CloudSpec: env.cloudSpec(ic.Region), 145 Series: []string{ic.Series}, 146 Arches: ic.Arches, 147 Stream: stream, 148 }) 149 150 signedImageDataOnly := false 151 matchingImages, _, err := imageMetadataFetch(sources, imageConstraint, signedImageDataOnly) 152 if err != nil { 153 return nil, errors.Trace(err) 154 } 155 156 images := instances.ImageMetadataToImages(matchingImages) 157 spec, err := instances.FindInstanceSpec(images, ic, allInstanceTypes) 158 return spec, errors.Trace(err) 159 } 160 161 var imageMetadataFetch = imagemetadata.Fetch 162 163 // newRawInstance is where the new physical instance is actually 164 // provisioned, relative to the provided args and spec. Info for that 165 // low-level instance is returned. 166 func (env *environ) newRawInstance(args environs.StartInstanceParams, spec *instances.InstanceSpec) (*google.Instance, error) { 167 machineID := common.MachineFullName(env, args.InstanceConfig.MachineId) 168 169 os, err := series.GetOSFromSeries(args.InstanceConfig.Series) 170 if err != nil { 171 return nil, errors.Trace(err) 172 } 173 174 metadata, err := getMetadata(args, os) 175 if err != nil { 176 return nil, errors.Trace(err) 177 } 178 tags := []string{ 179 env.globalFirewallName(), 180 machineID, 181 } 182 disks, err := getDisks(spec, args.Constraints, os) 183 if err != nil { 184 return nil, errors.Trace(err) 185 } 186 187 // TODO(ericsnow) Use the env ID for the network name (instead of default)? 188 // TODO(ericsnow) Make the network name configurable? 189 // TODO(ericsnow) Support multiple networks? 190 // TODO(ericsnow) Use a different net interface name? Configurable? 191 instSpec := google.InstanceSpec{ 192 ID: machineID, 193 Type: spec.InstanceType.Name, 194 Disks: disks, 195 NetworkInterfaces: []string{"ExternalNAT"}, 196 Metadata: metadata, 197 Tags: tags, 198 // Network is omitted (left empty). 199 } 200 201 zones, err := env.parseAvailabilityZones(args) 202 if err != nil { 203 return nil, errors.Trace(err) 204 } 205 206 inst, err := env.gce.AddInstance(instSpec, zones...) 207 return inst, errors.Trace(err) 208 } 209 210 // getMetadata builds the raw "user-defined" metadata for the new 211 // instance (relative to the provided args) and returns it. 212 func getMetadata(args environs.StartInstanceParams, os jujuos.OSType) (map[string]string, error) { 213 userData, err := providerinit.ComposeUserData(args.InstanceConfig, nil, GCERenderer{}) 214 if err != nil { 215 return nil, errors.Annotate(err, "cannot make user data") 216 } 217 logger.Debugf("GCE user data; %d bytes", len(userData)) 218 219 metadata := make(map[string]string) 220 if isStateServer(args.InstanceConfig) { 221 metadata[metadataKeyIsState] = metadataValueTrue 222 } else { 223 metadata[metadataKeyIsState] = metadataValueFalse 224 } 225 switch os { 226 case jujuos.Ubuntu: 227 // We store a gz snapshop of information that is used by 228 // cloud-init and unpacked in to the /var/lib/cloud/instances folder 229 // for the instance. Due to a limitation with GCE and binary blobs 230 // we base64 encode the data before storing it. 231 metadata[metadataKeyCloudInit] = string(userData) 232 // Valid encoding values are determined by the cloudinit GCE data source. 233 // See: http://cloudinit.readthedocs.org 234 metadata[metadataKeyEncoding] = "base64" 235 236 authKeys, err := google.FormatAuthorizedKeys(args.InstanceConfig.AuthorizedKeys, "ubuntu") 237 if err != nil { 238 return nil, errors.Trace(err) 239 } 240 241 metadata[metadataKeySSHKeys] = authKeys 242 case jujuos.Windows: 243 metadata[metadataKeyWindowsUserdata] = string(userData) 244 245 validChars := append(utils.UpperAlpha, append(utils.LowerAlpha, utils.Digits...)...) 246 247 // The hostname must have maximum 15 characters 248 winHostname := "juju" + utils.RandomString(11, validChars) 249 metadata[metadataKeyWindowsSysprep] = fmt.Sprintf(winSetHostnameScript, winHostname) 250 default: 251 return nil, errors.Errorf("cannot pack metadata for os %s on the gce provider", os.String()) 252 } 253 254 return metadata, nil 255 } 256 257 // getDisks builds the raw spec for the disks that should be attached to 258 // the new instances and returns it. This will always include a root 259 // disk with characteristics determined by the provides args and 260 // constraints. 261 func getDisks(spec *instances.InstanceSpec, cons constraints.Value, os jujuos.OSType) ([]google.DiskSpec, error) { 262 size := common.MinRootDiskSizeGiB 263 if cons.RootDisk != nil && *cons.RootDisk > size { 264 size = common.MiBToGiB(*cons.RootDisk) 265 } 266 var imageURL string 267 switch os { 268 case jujuos.Ubuntu: 269 imageURL = ubuntuImageBasePath 270 case jujuos.Windows: 271 imageURL = windowsImageBasePath 272 default: 273 return nil, errors.Errorf("os %s is not supported on the gce provider", os.String()) 274 } 275 dSpec := google.DiskSpec{ 276 SizeHintGB: size, 277 ImageURL: imageURL + spec.Image.Id, 278 Boot: true, 279 AutoDelete: true, 280 } 281 if cons.RootDisk != nil && dSpec.TooSmall() { 282 msg := "Ignoring root-disk constraint of %dM because it is smaller than the GCE image size of %dG" 283 logger.Infof(msg, *cons.RootDisk, google.MinDiskSizeGB) 284 } 285 return []google.DiskSpec{dSpec}, nil 286 } 287 288 // getHardwareCharacteristics compiles hardware-related details about 289 // the given instance and relative to the provided spec and returns it. 290 func (env *environ) getHardwareCharacteristics(spec *instances.InstanceSpec, inst *environInstance) *instance.HardwareCharacteristics { 291 rootDiskMB := inst.base.RootDiskGB() * 1024 292 hwc := instance.HardwareCharacteristics{ 293 Arch: &spec.Image.Arch, 294 Mem: &spec.InstanceType.Mem, 295 CpuCores: &spec.InstanceType.CpuCores, 296 CpuPower: spec.InstanceType.CpuPower, 297 RootDisk: &rootDiskMB, 298 AvailabilityZone: &inst.base.ZoneName, 299 // Tags: not supported in GCE. 300 } 301 return &hwc 302 } 303 304 // AllInstances implements environs.InstanceBroker. 305 func (env *environ) AllInstances() ([]instance.Instance, error) { 306 instances, err := getInstances(env) 307 return instances, errors.Trace(err) 308 } 309 310 // StopInstances implements environs.InstanceBroker. 311 func (env *environ) StopInstances(instances ...instance.Id) error { 312 env = env.getSnapshot() 313 314 var ids []string 315 for _, id := range instances { 316 ids = append(ids, string(id)) 317 } 318 319 prefix := common.MachineFullName(env, "") 320 err := env.gce.RemoveInstances(prefix, ids...) 321 return errors.Trace(err) 322 }