github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/provider/lxd/environ_broker.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // +build go1.3 5 6 package lxd 7 8 import ( 9 "strings" 10 11 "github.com/juju/errors" 12 "github.com/juju/utils/arch" 13 lxdshared "github.com/lxc/lxd/shared" 14 15 "github.com/juju/juju/cloudconfig/cloudinit" 16 "github.com/juju/juju/cloudconfig/instancecfg" 17 "github.com/juju/juju/cloudconfig/providerinit" 18 "github.com/juju/juju/environs" 19 "github.com/juju/juju/environs/tags" 20 "github.com/juju/juju/instance" 21 "github.com/juju/juju/state/multiwatcher" 22 "github.com/juju/juju/status" 23 "github.com/juju/juju/tools" 24 "github.com/juju/juju/tools/lxdclient" 25 ) 26 27 func isController(icfg *instancecfg.InstanceConfig) bool { 28 return multiwatcher.AnyJobNeedsState(icfg.Jobs...) 29 } 30 31 // MaintainInstance is specified in the InstanceBroker interface. 32 func (*environ) MaintainInstance(args environs.StartInstanceParams) error { 33 return nil 34 } 35 36 // StartInstance implements environs.InstanceBroker. 37 func (env *environ) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 38 // Start a new instance. 39 40 series := args.Tools.OneSeries() 41 logger.Debugf("StartInstance: %q, %s", args.InstanceConfig.MachineId, series) 42 43 if err := env.finishInstanceConfig(args); err != nil { 44 return nil, errors.Trace(err) 45 } 46 47 // TODO(ericsnow) Handle constraints? 48 49 raw, err := env.newRawInstance(args) 50 if err != nil { 51 if args.StatusCallback != nil { 52 args.StatusCallback(status.ProvisioningError, err.Error(), nil) 53 } 54 return nil, errors.Trace(err) 55 } 56 logger.Infof("started instance %q", raw.Name) 57 inst := newInstance(raw, env) 58 59 // Build the result. 60 hwc := env.getHardwareCharacteristics(args, inst) 61 result := environs.StartInstanceResult{ 62 Instance: inst, 63 Hardware: hwc, 64 } 65 return &result, nil 66 } 67 68 func (env *environ) finishInstanceConfig(args environs.StartInstanceParams) error { 69 // TODO(natefinch): This is only correct so long as the lxd is running on 70 // the local machine. If/when we support a remote lxd environment, we'll 71 // need to change this to match the arch of the remote machine. 72 tools, err := args.Tools.Match(tools.Filter{Arch: arch.HostArch()}) 73 if err != nil { 74 return errors.Trace(err) 75 } 76 if err := args.InstanceConfig.SetTools(tools); err != nil { 77 return errors.Trace(err) 78 } 79 80 if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, env.ecfg.Config); err != nil { 81 return errors.Trace(err) 82 } 83 84 return nil 85 } 86 87 func (env *environ) getImageSources() ([]lxdclient.Remote, error) { 88 metadataSources, err := environs.ImageMetadataSources(env) 89 if err != nil { 90 return nil, errors.Trace(err) 91 } 92 remotes := make([]lxdclient.Remote, 0) 93 for _, source := range metadataSources { 94 url, err := source.URL("") 95 if err != nil { 96 logger.Debugf("failed to get the URL for metadataSource: %s", err) 97 continue 98 } 99 // NOTE(jam) LXD only allows you to pass HTTPS URLs. So strip 100 // off http:// and replace it with https:// 101 // Arguably we could give the user a direct error if 102 // env.ImageMetadataURL is http instead of https, but we also 103 // get http from the DefaultImageSources, which is why we 104 // replace it. 105 // TODO(jam) Maybe we could add a Validate step that ensures 106 // image-metadata-url is an "https://" URL, so that Users get a 107 // "your configuration is wrong" error, rather than silently 108 // changing it and having them get confused. 109 // https://github.com/lxc/lxd/issues/1763 110 if strings.HasPrefix(url, "http://") { 111 url = strings.TrimPrefix(url, "http://") 112 url = "https://" + url 113 logger.Debugf("LXD requires https://, using: %s", url) 114 } 115 remotes = append(remotes, lxdclient.Remote{ 116 Name: source.Description(), 117 Host: url, 118 Protocol: lxdclient.SimplestreamsProtocol, 119 Cert: nil, 120 ServerPEMCert: "", 121 }) 122 } 123 return remotes, nil 124 } 125 126 // newRawInstance is where the new physical instance is actually 127 // provisioned, relative to the provided args and spec. Info for that 128 // low-level instance is returned. 129 func (env *environ) newRawInstance(args environs.StartInstanceParams) (*lxdclient.Instance, error) { 130 hostname, err := env.namespace.Hostname(args.InstanceConfig.MachineId) 131 if err != nil { 132 return nil, errors.Trace(err) 133 } 134 135 // Note: other providers have the ImageMetadata already read for them 136 // and passed in as args.ImageMetadata. However, lxd provider doesn't 137 // use datatype: image-ids, it uses datatype: image-download, and we 138 // don't have a registered cloud/region. 139 imageSources, err := env.getImageSources() 140 if err != nil { 141 return nil, errors.Trace(err) 142 } 143 144 series := args.InstanceConfig.Series 145 // TODO(jam): We should get this information from EnsureImageExists, or 146 // something given to us from 'raw', not assume it ourselves. 147 image := "ubuntu-" + series 148 // TODO: support args.Constraints.Arch, we'll want to map from 149 150 // Keep track of StatusCallback output so we may clean up later. 151 // This is implemented here, close to where the StatusCallback calls 152 // are made, instead of at a higher level in the package, so as not to 153 // assume that all providers will have the same need to be implemented 154 // in the same way. 155 longestMsg := 0 156 statusCallback := func(currentStatus status.Status, msg string) { 157 if args.StatusCallback != nil { 158 args.StatusCallback(currentStatus, msg, nil) 159 } 160 if len(msg) > longestMsg { 161 longestMsg = len(msg) 162 } 163 } 164 cleanupCallback := func() { 165 if args.CleanupCallback != nil { 166 args.CleanupCallback(strings.Repeat(" ", longestMsg)) 167 } 168 } 169 defer cleanupCallback() 170 171 imageCallback := func(copyProgress string) { 172 statusCallback(status.Allocating, copyProgress) 173 } 174 if err := env.raw.EnsureImageExists(series, imageSources, imageCallback); err != nil { 175 return nil, errors.Trace(err) 176 } 177 cleanupCallback() // Clean out any long line of completed download status 178 179 cloudcfg, err := cloudinit.New(series) 180 if err != nil { 181 return nil, errors.Trace(err) 182 } 183 184 var certificateFingerprint string 185 if args.InstanceConfig.Controller != nil { 186 // For controller machines, generate a certificate pair and write 187 // them to the instance's disk in a well-defined location, along 188 // with the server's certificate. 189 certPEM, keyPEM, err := lxdshared.GenerateMemCert(true) 190 if err != nil { 191 return nil, errors.Trace(err) 192 } 193 cert := lxdclient.NewCert(certPEM, keyPEM) 194 cert.Name = hostname 195 196 // We record the certificate's fingerprint in metadata, so we can 197 // remove the certificate along with the instance. 198 certificateFingerprint, err = cert.Fingerprint() 199 if err != nil { 200 return nil, errors.Trace(err) 201 } 202 203 if err := env.raw.AddCert(cert); err != nil { 204 return nil, errors.Annotatef(err, "adding certificate %q", cert.Name) 205 } 206 serverState, err := env.raw.ServerStatus() 207 if err != nil { 208 return nil, errors.Annotate(err, "getting server status") 209 } 210 cloudcfg.AddRunTextFile(clientCertPath, string(certPEM), 0600) 211 cloudcfg.AddRunTextFile(clientKeyPath, string(keyPEM), 0600) 212 cloudcfg.AddRunTextFile(serverCertPath, serverState.Environment.Certificate, 0600) 213 } 214 215 cloudcfg.SetAttr("hostname", hostname) 216 cloudcfg.SetAttr("manage_etc_hosts", true) 217 218 metadata, err := getMetadata(cloudcfg, args) 219 if err != nil { 220 return nil, errors.Trace(err) 221 } 222 if certificateFingerprint != "" { 223 metadata[metadataKeyCertificateFingerprint] = certificateFingerprint 224 } 225 226 // TODO(ericsnow) Use the env ID for the network name (instead of default)? 227 // TODO(ericsnow) Make the network name configurable? 228 // TODO(ericsnow) Support multiple networks? 229 // TODO(ericsnow) Use a different net interface name? Configurable? 230 instSpec := lxdclient.InstanceSpec{ 231 Name: hostname, 232 Image: image, 233 //Type: spec.InstanceType.Name, 234 //Disks: getDisks(spec, args.Constraints), 235 //NetworkInterfaces: []string{"ExternalNAT"}, 236 Metadata: metadata, 237 Profiles: []string{ 238 //TODO(wwitzel3) allow the user to specify lxc profiles to apply. This allows the 239 // user to setup any custom devices order config settings for their environment. 240 // Also we must ensure that a device with the parent: lxcbr0 exists in at least 241 // one of the profiles. 242 "default", 243 env.profileName(), 244 }, 245 // Network is omitted (left empty). 246 } 247 248 logger.Infof("starting instance %q (image %q)...", instSpec.Name, instSpec.Image) 249 250 statusCallback(status.Allocating, "preparing image") 251 inst, err := env.raw.AddInstance(instSpec) 252 if err != nil { 253 return nil, errors.Trace(err) 254 } 255 statusCallback(status.Running, "container started") 256 return inst, nil 257 } 258 259 // getMetadata builds the raw "user-defined" metadata for the new 260 // instance (relative to the provided args) and returns it. 261 func getMetadata(cloudcfg cloudinit.CloudConfig, args environs.StartInstanceParams) (map[string]string, error) { 262 renderer := lxdRenderer{} 263 uncompressed, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, renderer) 264 if err != nil { 265 return nil, errors.Annotate(err, "cannot make user data") 266 } 267 logger.Debugf("LXD user data; %d bytes", len(uncompressed)) 268 269 // TODO(ericsnow) Looks like LXD does not handle gzipped userdata 270 // correctly. It likely has to do with the HTTP transport, much 271 // as we have to b64encode the userdata for GCE. Until that is 272 // resolved we simply pass the plain text. 273 //compressed := utils.Gzip(compressed) 274 userdata := string(uncompressed) 275 276 metadata := map[string]string{ 277 // store the cloud-config userdata for cloud-init. 278 metadataKeyCloudInit: userdata, 279 } 280 for k, v := range args.InstanceConfig.Tags { 281 if !strings.HasPrefix(k, tags.JujuTagPrefix) { 282 // Since some metadata is interpreted by LXD, 283 // we cannot allow arbitrary tags to be passed 284 // in by the user. We currently only pass through 285 // Juju-defined tags. 286 // 287 // TODO(axw) 2016-04-11 #1568666 288 // We should reject non-juju tags in config validation. 289 logger.Debugf("ignoring non-juju tag: %s=%s", k, v) 290 continue 291 } 292 metadata[k] = v 293 } 294 295 return metadata, nil 296 } 297 298 // getHardwareCharacteristics compiles hardware-related details about 299 // the given instance and relative to the provided spec and returns it. 300 func (env *environ) getHardwareCharacteristics(args environs.StartInstanceParams, inst *environInstance) *instance.HardwareCharacteristics { 301 raw := inst.raw.Hardware 302 303 archStr := raw.Architecture 304 if archStr == "unknown" || !arch.IsSupportedArch(archStr) { 305 // TODO(ericsnow) This special-case should be improved. 306 archStr = arch.HostArch() 307 } 308 cores := uint64(raw.NumCores) 309 mem := uint64(raw.MemoryMB) 310 return &instance.HardwareCharacteristics{ 311 Arch: &archStr, 312 CpuCores: &cores, 313 Mem: &mem, 314 } 315 } 316 317 // AllInstances implements environs.InstanceBroker. 318 func (env *environ) AllInstances() ([]instance.Instance, error) { 319 environInstances, err := env.allInstances() 320 instances := make([]instance.Instance, len(environInstances)) 321 for i, inst := range environInstances { 322 if inst == nil { 323 continue 324 } 325 instances[i] = inst 326 } 327 return instances, err 328 } 329 330 // StopInstances implements environs.InstanceBroker. 331 func (env *environ) StopInstances(instances ...instance.Id) error { 332 var ids []string 333 for _, id := range instances { 334 ids = append(ids, string(id)) 335 } 336 337 prefix := env.namespace.Prefix() 338 err := removeInstances(env.raw, prefix, ids) 339 return errors.Trace(err) 340 } 341 342 func removeInstances(raw *rawProvider, prefix string, ids []string) error { 343 // We must first list the instances so we can remove any 344 // controller certificates. 345 allInstances, err := raw.Instances(prefix) 346 if err != nil { 347 return errors.Trace(err) 348 } 349 for _, inst := range allInstances { 350 certificateFingerprint := inst.Metadata()[lxdclient.CertificateFingerprintKey] 351 if certificateFingerprint == "" { 352 continue 353 } 354 var found bool 355 for _, id := range ids { 356 if inst.Name == id { 357 found = true 358 break 359 } 360 } 361 if !found { 362 continue 363 } 364 err := raw.RemoveCertByFingerprint(certificateFingerprint) 365 if err != nil && !errors.IsNotFound(err) { 366 return errors.Annotatef(err, "removing certificate for %q", inst.Name) 367 } 368 } 369 return raw.RemoveInstances(prefix, ids...) 370 }