github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/lxd/environ_broker.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lxd 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/juju/errors" 11 "github.com/juju/utils/arch" 12 13 "github.com/juju/juju/cloudconfig/cloudinit" 14 "github.com/juju/juju/cloudconfig/instancecfg" 15 "github.com/juju/juju/cloudconfig/providerinit" 16 "github.com/juju/juju/container/lxd" 17 "github.com/juju/juju/core/instance" 18 "github.com/juju/juju/core/status" 19 "github.com/juju/juju/environs" 20 "github.com/juju/juju/environs/context" 21 "github.com/juju/juju/environs/instances" 22 "github.com/juju/juju/environs/tags" 23 "github.com/juju/juju/provider/common" 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( 34 ctx context.ProviderCallContext, args environs.StartInstanceParams, 35 ) (*environs.StartInstanceResult, error) { 36 series := args.Tools.OneSeries() 37 logger.Debugf("StartInstance: %q, %s", args.InstanceConfig.MachineId, series) 38 39 arch, err := env.finishInstanceConfig(args) 40 if err != nil { 41 return nil, errors.Trace(err) 42 } 43 44 container, err := env.newContainer(ctx, args, arch) 45 if err != nil { 46 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 47 if args.StatusCallback != nil { 48 args.StatusCallback(status.ProvisioningError, err.Error(), nil) 49 } 50 return nil, errors.Trace(err) 51 } 52 logger.Infof("started instance %q", container.Name) 53 inst := newInstance(container, env) 54 55 // Build the result. 56 hwc := env.getHardwareCharacteristics(args, inst) 57 result := environs.StartInstanceResult{ 58 Instance: inst, 59 Hardware: hwc, 60 } 61 return &result, nil 62 } 63 64 func (env *environ) finishInstanceConfig(args environs.StartInstanceParams) (string, error) { 65 arch := env.server.HostArch() 66 tools, err := args.Tools.Match(tools.Filter{Arch: arch}) 67 if err != nil { 68 return "", errors.Trace(err) 69 } 70 if err := args.InstanceConfig.SetTools(tools); err != nil { 71 return "", errors.Trace(err) 72 } 73 if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, env.ecfg.Config); err != nil { 74 return "", errors.Trace(err) 75 } 76 return arch, nil 77 } 78 79 // newContainer is where the new physical instance is actually 80 // provisioned, relative to the provided args and spec. Info for that 81 // low-level instance is returned. 82 func (env *environ) newContainer( 83 ctx context.ProviderCallContext, 84 args environs.StartInstanceParams, 85 arch string, 86 ) (*lxd.Container, error) { 87 // Note: other providers have the ImageMetadata already read for them 88 // and passed in as args.ImageMetadata. However, lxd provider doesn't 89 // use datatype: image-ids, it uses datatype: image-download, and we 90 // don't have a registered cloud/region. 91 imageSources, err := env.getImageSources() 92 if err != nil { 93 return nil, errors.Trace(err) 94 } 95 96 // Keep track of StatusCallback output so we may clean up later. 97 // This is implemented here, close to where the StatusCallback calls 98 // are made, instead of at a higher level in the package, so as not to 99 // assume that all providers will have the same need to be implemented 100 // in the same way. 101 longestMsg := 0 102 statusCallback := func(currentStatus status.Status, msg string, data map[string]interface{}) error { 103 if args.StatusCallback != nil { 104 args.StatusCallback(currentStatus, msg, nil) 105 } 106 if len(msg) > longestMsg { 107 longestMsg = len(msg) 108 } 109 return nil 110 } 111 cleanupCallback := func() { 112 if args.CleanupCallback != nil { 113 args.CleanupCallback(strings.Repeat(" ", longestMsg)) 114 } 115 } 116 defer cleanupCallback() 117 118 target, err := env.getTargetServer(ctx, args) 119 if err != nil { 120 return nil, errors.Trace(err) 121 } 122 123 image, err := target.FindImage(args.InstanceConfig.Series, arch, imageSources, true, statusCallback) 124 if err != nil { 125 return nil, errors.Trace(err) 126 } 127 cleanupCallback() // Clean out any long line of completed download status 128 129 cSpec, err := env.getContainerSpec(image, args) 130 if err != nil { 131 return nil, errors.Trace(err) 132 } 133 134 statusCallback(status.Allocating, "Creating container", nil) 135 container, err := target.CreateContainerFromSpec(cSpec) 136 if err != nil { 137 return nil, errors.Trace(err) 138 } 139 statusCallback(status.Running, "Container started", nil) 140 return container, nil 141 } 142 143 func (env *environ) getImageSources() ([]lxd.ServerSpec, error) { 144 metadataSources, err := environs.ImageMetadataSources(env) 145 if err != nil { 146 return nil, errors.Trace(err) 147 } 148 remotes := make([]lxd.ServerSpec, 0) 149 for _, source := range metadataSources { 150 url, err := source.URL("") 151 if err != nil { 152 logger.Debugf("failed to get the URL for metadataSource: %s", err) 153 continue 154 } 155 // NOTE(jam) LXD only allows you to pass HTTPS URLs. So strip 156 // off http:// and replace it with https:// 157 // Arguably we could give the user a direct error if 158 // env.ImageMetadataURL is http instead of https, but we also 159 // get http from the DefaultImageSources, which is why we 160 // replace it. 161 // TODO(jam) Maybe we could add a Validate step that ensures 162 // image-metadata-url is an "https://" URL, so that Users get a 163 // "your configuration is wrong" error, rather than silently 164 // changing it and having them get confused. 165 // https://github.com/lxc/lxd/issues/1763 166 remotes = append(remotes, lxd.MakeSimpleStreamsServerSpec(source.Description(), url)) 167 } 168 return remotes, nil 169 } 170 171 // getContainerSpec builds a container spec from the input container image and 172 // start-up parameters. 173 // Cloud-init config is generated based on the network devices in the default 174 // profile and included in the spec config. 175 func (env *environ) getContainerSpec( 176 image lxd.SourcedImage, args environs.StartInstanceParams, 177 ) (lxd.ContainerSpec, error) { 178 hostname, err := env.namespace.Hostname(args.InstanceConfig.MachineId) 179 if err != nil { 180 return lxd.ContainerSpec{}, errors.Trace(err) 181 } 182 cSpec := lxd.ContainerSpec{ 183 Name: hostname, 184 Profiles: append([]string{"default", env.profileName()}, args.CharmLXDProfiles...), 185 Image: image, 186 Config: make(map[string]string), 187 } 188 cSpec.ApplyConstraints(args.Constraints) 189 190 cloudCfg, err := cloudinit.New(args.InstanceConfig.Series) 191 if err != nil { 192 return cSpec, errors.Trace(err) 193 } 194 195 // Check to see if there are any non-eth0 devices in the default profile. 196 // If there are, we need cloud-init to configure them, and we need to 197 // explicitly add them to the container spec. 198 nics, err := env.server.GetNICsFromProfile("default") 199 if err != nil { 200 return cSpec, errors.Trace(err) 201 } 202 if !(len(nics) == 1 && nics["eth0"] != nil) { 203 logger.Debugf("generating custom cloud-init networking") 204 205 cSpec.Config[lxd.NetworkConfigKey] = cloudinit.CloudInitNetworkConfigDisabled 206 207 info, err := lxd.InterfaceInfoFromDevices(nics) 208 if err != nil { 209 return cSpec, errors.Trace(err) 210 } 211 if err := cloudCfg.AddNetworkConfig(info); err != nil { 212 return cSpec, errors.Trace(err) 213 } 214 215 cSpec.Devices = nics 216 } 217 218 userData, err := providerinit.ComposeUserData(args.InstanceConfig, cloudCfg, lxdRenderer{}) 219 if err != nil { 220 return cSpec, errors.Annotate(err, "composing user data") 221 } 222 logger.Debugf("LXD user data; %d bytes", len(userData)) 223 224 // TODO(ericsnow) Looks like LXD does not handle gzipped userdata 225 // correctly. It likely has to do with the HTTP transport, much 226 // as we have to b64encode the userdata for GCE. Until that is 227 // resolved we simply pass the plain text. 228 //cfg[lxd.UserDataKey] = utils.Gzip(userData) 229 cSpec.Config[lxd.UserDataKey] = string(userData) 230 231 for k, v := range args.InstanceConfig.Tags { 232 if !strings.HasPrefix(k, tags.JujuTagPrefix) { 233 // Since some metadata is interpreted by LXD, we cannot allow 234 // arbitrary tags to be passed in by the user. 235 // We currently only pass through Juju-defined tags. 236 logger.Debugf("ignoring non-juju tag: %s=%s", k, v) 237 continue 238 } 239 cSpec.Config[lxd.UserNamespacePrefix+k] = v 240 } 241 242 return cSpec, nil 243 } 244 245 // getTargetServer checks to see if a valid zone was passed as a placement 246 // directive in the start-up start-up arguments. If so, a server for the 247 // specific node is returned. 248 func (env *environ) getTargetServer( 249 ctx context.ProviderCallContext, args environs.StartInstanceParams, 250 ) (Server, error) { 251 p, err := env.parsePlacement(ctx, args.Placement) 252 if err != nil { 253 return nil, errors.Trace(err) 254 } 255 256 if p.nodeName == "" { 257 return env.server, nil 258 } 259 return env.server.UseTargetServer(p.nodeName) 260 } 261 262 type lxdPlacement struct { 263 nodeName string 264 } 265 266 func (env *environ) parsePlacement(ctx context.ProviderCallContext, placement string) (*lxdPlacement, error) { 267 if placement == "" { 268 return &lxdPlacement{}, nil 269 } 270 271 var node string 272 pos := strings.IndexRune(placement, '=') 273 // Assume that a plain string is a node name. 274 if pos == -1 { 275 node = placement 276 } else { 277 if placement[:pos] != "zone" { 278 return nil, fmt.Errorf("unknown placement directive: %v", placement) 279 } 280 node = placement[pos+1:] 281 } 282 283 if node == "" { 284 return &lxdPlacement{}, nil 285 } 286 287 if err := common.ValidateAvailabilityZone(env, ctx, node); err != nil { 288 return nil, errors.Trace(err) 289 } 290 291 return &lxdPlacement{nodeName: node}, nil 292 } 293 294 // getHardwareCharacteristics compiles hardware-related details about 295 // the given instance and relative to the provided spec and returns it. 296 func (env *environ) getHardwareCharacteristics( 297 args environs.StartInstanceParams, inst *environInstance, 298 ) *instance.HardwareCharacteristics { 299 container := inst.container 300 301 archStr := container.Arch() 302 if archStr == "unknown" || !arch.IsSupportedArch(archStr) { 303 archStr = env.server.HostArch() 304 } 305 cores := uint64(container.CPUs()) 306 mem := uint64(container.Mem()) 307 return &instance.HardwareCharacteristics{ 308 Arch: &archStr, 309 CpuCores: &cores, 310 Mem: &mem, 311 } 312 } 313 314 // AllInstances implements environs.InstanceBroker. 315 func (env *environ) AllInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) { 316 environInstances, err := env.allInstances() 317 instances := make([]instances.Instance, len(environInstances)) 318 for i, inst := range environInstances { 319 if inst == nil { 320 continue 321 } 322 instances[i] = inst 323 } 324 return instances, errors.Trace(err) 325 } 326 327 // StopInstances implements environs.InstanceBroker. 328 func (env *environ) StopInstances(ctx context.ProviderCallContext, instances ...instance.Id) error { 329 prefix := env.namespace.Prefix() 330 var names []string 331 for _, id := range instances { 332 name := string(id) 333 if strings.HasPrefix(name, prefix) { 334 names = append(names, name) 335 } else { 336 logger.Warningf("ignoring request to stop container %q - not in namespace %q", name, prefix) 337 } 338 } 339 340 err := env.server.RemoveContainers(names) 341 if err != nil { 342 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 343 } 344 return errors.Trace(err) 345 }