github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/container/lxd/lxd.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 "fmt" 10 11 "github.com/juju/errors" 12 "github.com/juju/loggo" 13 14 "github.com/juju/juju/cloudconfig/containerinit" 15 "github.com/juju/juju/cloudconfig/instancecfg" 16 "github.com/juju/juju/constraints" 17 "github.com/juju/juju/container" 18 "github.com/juju/juju/instance" 19 "github.com/juju/juju/network" 20 "github.com/juju/juju/status" 21 "github.com/juju/juju/tools/lxdclient" 22 ) 23 24 var ( 25 logger = loggo.GetLogger("juju.container.lxd") 26 ) 27 28 const lxdDefaultProfileName = "default" 29 30 // XXX: should we allow managing containers on other hosts? this is 31 // functionality LXD gives us and from discussion juju would use eventually for 32 // the local provider, so the APIs probably need to be changed to pass extra 33 // args around. I'm punting for now. 34 type containerManager struct { 35 modelUUID string 36 namespace instance.Namespace 37 // A cached client. 38 client *lxdclient.Client 39 } 40 41 // containerManager implements container.Manager. 42 var _ container.Manager = (*containerManager)(nil) 43 44 func ConnectLocal() (*lxdclient.Client, error) { 45 cfg := lxdclient.Config{ 46 Remote: lxdclient.Local, 47 } 48 49 cfg, err := cfg.WithDefaults() 50 if err != nil { 51 return nil, errors.Trace(err) 52 } 53 54 client, err := lxdclient.Connect(cfg) 55 if err != nil { 56 return nil, errors.Trace(err) 57 } 58 59 return client, nil 60 } 61 62 // NewContainerManager creates the entity that knows how to create and manage 63 // LXD containers. 64 // TODO(jam): This needs to grow support for things like LXC's ImageURLGetter 65 // functionality. 66 func NewContainerManager(conf container.ManagerConfig) (container.Manager, error) { 67 modelUUID := conf.PopValue(container.ConfigModelUUID) 68 if modelUUID == "" { 69 return nil, errors.Errorf("model UUID is required") 70 } 71 namespace, err := instance.NewNamespace(modelUUID) 72 if err != nil { 73 return nil, errors.Trace(err) 74 } 75 76 conf.WarnAboutUnused() 77 return &containerManager{ 78 modelUUID: modelUUID, 79 namespace: namespace, 80 }, nil 81 } 82 83 // Namespace implements container.Manager. 84 func (manager *containerManager) Namespace() instance.Namespace { 85 return manager.namespace 86 } 87 88 func (manager *containerManager) CreateContainer( 89 instanceConfig *instancecfg.InstanceConfig, 90 cons constraints.Value, 91 series string, 92 networkConfig *container.NetworkConfig, 93 storageConfig *container.StorageConfig, 94 callback container.StatusCallback, 95 ) (inst instance.Instance, _ *instance.HardwareCharacteristics, err error) { 96 97 defer func() { 98 if err != nil { 99 callback(status.ProvisioningError, fmt.Sprintf("Creating container: %v", err), nil) 100 } 101 }() 102 103 if manager.client == nil { 104 manager.client, err = ConnectLocal() 105 if err != nil { 106 err = errors.Annotatef(err, "failed to connect to local LXD") 107 return 108 } 109 } 110 111 err = manager.client.EnsureImageExists(series, 112 lxdclient.DefaultImageSources, 113 func(progress string) { 114 callback(status.Provisioning, progress, nil) 115 }) 116 if err != nil { 117 err = errors.Annotatef(err, "failed to ensure LXD image") 118 return 119 } 120 121 name, err := manager.namespace.Hostname(instanceConfig.MachineId) 122 if err != nil { 123 return nil, nil, errors.Trace(err) 124 } 125 126 // Do not pass networkConfig, as we want to directly inject our own ENI 127 // rather than using cloud-init. 128 userData, err := containerinit.CloudInitUserData(instanceConfig, nil) 129 if err != nil { 130 return 131 } 132 133 metadata := map[string]string{ 134 lxdclient.UserdataKey: string(userData), 135 // An extra piece of info to let people figure out where this 136 // thing came from. 137 "user.juju-model": manager.modelUUID, 138 139 // Make sure these come back up on host reboot. 140 "boot.autostart": "true", 141 } 142 143 nics, err := networkDevices(networkConfig) 144 if err != nil { 145 return 146 } 147 148 // TODO(macgreagoir) This might be dead code. Do we always get 149 // len(nics) > 0? 150 profiles := []string{} 151 152 if len(nics) == 0 { 153 logger.Infof("instance %q configured with %q profile", name, lxdDefaultProfileName) 154 profiles = append(profiles, lxdDefaultProfileName) 155 } else { 156 logger.Infof("instance %q configured with %v network devices", name, nics) 157 } 158 159 // Push the required /etc/network/interfaces file to the container. 160 // By pushing this file (which happens after LXD init, and before LXD 161 // start) we ensure that we get Juju's version of ENI, as opposed to 162 // the default LXD version, which may assume it can do DHCP over eth0. 163 // Especially on a multi-nic host, it is possible for MAAS to provide 164 // DHCP on a different space to that which the container eth0 interface 165 // will be bridged, or not provide DHCP at all. 166 eni, err := containerinit.GenerateNetworkConfig(networkConfig) 167 if err != nil { 168 err = errors.Annotatef(err, "failed to generate /etc/network/interfaces content") 169 return 170 } 171 172 spec := lxdclient.InstanceSpec{ 173 Name: name, 174 Image: manager.client.ImageNameForSeries(series), 175 Metadata: metadata, 176 Devices: nics, 177 Profiles: profiles, 178 Files: lxdclient.Files{ 179 lxdclient.File{ 180 Content: []byte(eni), 181 Path: "/etc/network/interfaces", 182 GID: 0, 183 UID: 0, 184 Mode: 0644, 185 }, 186 }, 187 } 188 189 logger.Infof("starting instance %q (image %q)...", spec.Name, spec.Image) 190 callback(status.Provisioning, "Starting container", nil) 191 _, err = manager.client.AddInstance(spec) 192 if err != nil { 193 return 194 } 195 196 callback(status.Running, "Container started", nil) 197 inst = &lxdInstance{name, manager.client} 198 return 199 } 200 201 func (manager *containerManager) DestroyContainer(id instance.Id) error { 202 if manager.client == nil { 203 var err error 204 manager.client, err = ConnectLocal() 205 if err != nil { 206 return err 207 } 208 } 209 return errors.Trace(manager.client.RemoveInstances(manager.namespace.Prefix(), string(id))) 210 } 211 212 func (manager *containerManager) ListContainers() (result []instance.Instance, err error) { 213 result = []instance.Instance{} 214 if manager.client == nil { 215 manager.client, err = ConnectLocal() 216 if err != nil { 217 return 218 } 219 } 220 221 lxdInstances, err := manager.client.Instances(manager.namespace.Prefix()) 222 if err != nil { 223 return 224 } 225 226 for _, i := range lxdInstances { 227 result = append(result, &lxdInstance{i.Name, manager.client}) 228 } 229 230 return 231 } 232 233 func (manager *containerManager) IsInitialized() bool { 234 if manager.client != nil { 235 return true 236 } 237 238 // NewClient does a roundtrip to the server to make sure it understands 239 // the versions, so all we need to do is connect above and we're done. 240 var err error 241 manager.client, err = ConnectLocal() 242 return err == nil 243 } 244 245 // HasLXDSupport returns false when this juju binary was not built with LXD 246 // support (i.e. it was built on a golang version < 1.2 247 func HasLXDSupport() bool { 248 return true 249 } 250 251 func nicDevice(deviceName, parentDevice, hwAddr string, mtu int) (lxdclient.Device, error) { 252 device := make(lxdclient.Device) 253 254 device["type"] = "nic" 255 device["nictype"] = "bridged" 256 257 if deviceName == "" { 258 return nil, errors.Errorf("invalid device name") 259 } 260 device["name"] = deviceName 261 262 if parentDevice == "" { 263 return nil, errors.Errorf("invalid parent device name") 264 } 265 device["parent"] = parentDevice 266 267 if hwAddr != "" { 268 device["hwaddr"] = hwAddr 269 } 270 271 if mtu > 0 { 272 device["mtu"] = fmt.Sprintf("%v", mtu) 273 } 274 275 return device, nil 276 } 277 278 func networkDevices(networkConfig *container.NetworkConfig) (lxdclient.Devices, error) { 279 nics := make(lxdclient.Devices) 280 281 if len(networkConfig.Interfaces) > 0 { 282 for _, v := range networkConfig.Interfaces { 283 if v.InterfaceType == network.LoopbackInterface { 284 continue 285 } 286 if v.InterfaceType != network.EthernetInterface { 287 return nil, errors.Errorf("interface type %q not supported", v.InterfaceType) 288 } 289 parentDevice := v.ParentInterfaceName 290 device, err := nicDevice(v.InterfaceName, parentDevice, v.MACAddress, v.MTU) 291 if err != nil { 292 return nil, errors.Trace(err) 293 } 294 nics[v.InterfaceName] = device 295 } 296 } else if networkConfig.Device != "" { 297 device, err := nicDevice("eth0", networkConfig.Device, "", networkConfig.MTU) 298 if err != nil { 299 return nil, errors.Trace(err) 300 } 301 nics["eth0"] = device 302 } 303 304 return nics, nil 305 }