github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 if args.InstanceConfig.Controller != nil { 184 // For controller machines, generate a certificate pair and write 185 // them to the instance's disk in a well-defined location, along 186 // with the server's certificate. 187 certPEM, keyPEM, err := lxdshared.GenerateMemCert() 188 if err != nil { 189 return nil, errors.Trace(err) 190 } 191 cert := lxdclient.NewCert(certPEM, keyPEM) 192 cert.Name = hostname 193 // TODO(axw) 2016-08-24 #1616346 194 // We need to remove this cert when removing 195 // the machine and/or destroying the controller. 196 if err := env.raw.AddCert(cert); err != nil { 197 return nil, errors.Annotatef(err, "adding certificate %q", cert.Name) 198 } 199 serverState, err := env.raw.ServerStatus() 200 if err != nil { 201 return nil, errors.Annotate(err, "getting server status") 202 } 203 cloudcfg.AddRunTextFile(clientCertPath, string(certPEM), 0600) 204 cloudcfg.AddRunTextFile(clientKeyPath, string(keyPEM), 0600) 205 cloudcfg.AddRunTextFile(serverCertPath, serverState.Environment.Certificate, 0600) 206 } 207 208 cloudcfg.SetAttr("hostname", hostname) 209 cloudcfg.SetAttr("manage_etc_hosts", true) 210 211 metadata, err := getMetadata(cloudcfg, args) 212 if err != nil { 213 return nil, errors.Trace(err) 214 } 215 216 //tags := []string{ 217 // env.globalFirewallName(), 218 // machineID, 219 //} 220 // TODO(ericsnow) Use the env ID for the network name (instead of default)? 221 // TODO(ericsnow) Make the network name configurable? 222 // TODO(ericsnow) Support multiple networks? 223 // TODO(ericsnow) Use a different net interface name? Configurable? 224 instSpec := lxdclient.InstanceSpec{ 225 Name: hostname, 226 Image: image, 227 //Type: spec.InstanceType.Name, 228 //Disks: getDisks(spec, args.Constraints), 229 //NetworkInterfaces: []string{"ExternalNAT"}, 230 Metadata: metadata, 231 Profiles: []string{ 232 //TODO(wwitzel3) allow the user to specify lxc profiles to apply. This allows the 233 // user to setup any custom devices order config settings for their environment. 234 // Also we must ensure that a device with the parent: lxcbr0 exists in at least 235 // one of the profiles. 236 "default", 237 env.profileName(), 238 }, 239 //Tags: tags, 240 // Network is omitted (left empty). 241 } 242 243 logger.Infof("starting instance %q (image %q)...", instSpec.Name, instSpec.Image) 244 245 statusCallback(status.Allocating, "preparing image") 246 inst, err := env.raw.AddInstance(instSpec) 247 if err != nil { 248 return nil, errors.Trace(err) 249 } 250 statusCallback(status.Running, "container started") 251 return inst, nil 252 } 253 254 // getMetadata builds the raw "user-defined" metadata for the new 255 // instance (relative to the provided args) and returns it. 256 func getMetadata(cloudcfg cloudinit.CloudConfig, args environs.StartInstanceParams) (map[string]string, error) { 257 renderer := lxdRenderer{} 258 uncompressed, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, renderer) 259 if err != nil { 260 return nil, errors.Annotate(err, "cannot make user data") 261 } 262 logger.Debugf("LXD user data; %d bytes", len(uncompressed)) 263 264 // TODO(ericsnow) Looks like LXD does not handle gzipped userdata 265 // correctly. It likely has to do with the HTTP transport, much 266 // as we have to b64encode the userdata for GCE. Until that is 267 // resolved we simply pass the plain text. 268 //compressed := utils.Gzip(compressed) 269 userdata := string(uncompressed) 270 271 metadata := map[string]string{ 272 // store the cloud-config userdata for cloud-init. 273 metadataKeyCloudInit: userdata, 274 } 275 for k, v := range args.InstanceConfig.Tags { 276 if !strings.HasPrefix(k, tags.JujuTagPrefix) { 277 // Since some metadata is interpreted by LXD, 278 // we cannot allow arbitrary tags to be passed 279 // in by the user. We currently only pass through 280 // Juju-defined tags. 281 // 282 // TODO(axw) 2016-04-11 #1568666 283 // We should reject non-juju tags in config validation. 284 logger.Debugf("ignoring non-juju tag: %s=%s", k, v) 285 continue 286 } 287 metadata[k] = v 288 } 289 290 return metadata, nil 291 } 292 293 // getHardwareCharacteristics compiles hardware-related details about 294 // the given instance and relative to the provided spec and returns it. 295 func (env *environ) getHardwareCharacteristics(args environs.StartInstanceParams, inst *environInstance) *instance.HardwareCharacteristics { 296 raw := inst.raw.Hardware 297 298 archStr := raw.Architecture 299 if archStr == "unknown" || !arch.IsSupportedArch(archStr) { 300 // TODO(ericsnow) This special-case should be improved. 301 archStr = arch.HostArch() 302 } 303 cores := uint64(raw.NumCores) 304 mem := uint64(raw.MemoryMB) 305 return &instance.HardwareCharacteristics{ 306 Arch: &archStr, 307 CpuCores: &cores, 308 Mem: &mem, 309 } 310 } 311 312 // AllInstances implements environs.InstanceBroker. 313 func (env *environ) AllInstances() ([]instance.Instance, error) { 314 environInstances, err := env.allInstances() 315 instances := make([]instance.Instance, len(environInstances)) 316 for i, inst := range environInstances { 317 if inst == nil { 318 continue 319 } 320 instances[i] = inst 321 } 322 return instances, err 323 } 324 325 // StopInstances implements environs.InstanceBroker. 326 func (env *environ) StopInstances(instances ...instance.Id) error { 327 var ids []string 328 for _, id := range instances { 329 ids = append(ids, string(id)) 330 } 331 332 prefix := env.namespace.Prefix() 333 err := env.raw.RemoveInstances(prefix, ids...) 334 return errors.Trace(err) 335 }