github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/container/lxc/lxc.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lxc 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/juju/loggo" 17 "github.com/juju/names" 18 "launchpad.net/golxc" 19 20 "github.com/juju/juju/agent" 21 "github.com/juju/juju/container" 22 "github.com/juju/juju/environs/cloudinit" 23 "github.com/juju/juju/instance" 24 "github.com/juju/juju/version" 25 ) 26 27 var logger = loggo.GetLogger("juju.container.lxc") 28 29 var ( 30 defaultTemplate = "ubuntu-cloud" 31 LxcContainerDir = "/var/lib/lxc" 32 LxcRestartDir = "/etc/lxc/auto" 33 LxcObjectFactory = golxc.Factory() 34 ) 35 36 const ( 37 // DefaultLxcBridge is the package created container bridge 38 DefaultLxcBridge = "lxcbr0" 39 // Btrfs is special as we treat it differently for create and clone. 40 Btrfs = "btrfs" 41 ) 42 43 // DefaultNetworkConfig returns a valid NetworkConfig to use the 44 // defaultLxcBridge that is created by the lxc package. 45 func DefaultNetworkConfig() *container.NetworkConfig { 46 return container.BridgeNetworkConfig(DefaultLxcBridge) 47 } 48 49 // FsCommandOutput calls cmd.Output, this is used as an overloading point so 50 // we can test what *would* be run without actually executing another program 51 var FsCommandOutput = (*exec.Cmd).CombinedOutput 52 53 func containerDirFilesystem() (string, error) { 54 cmd := exec.Command("df", "--output=fstype", LxcContainerDir) 55 out, err := FsCommandOutput(cmd) 56 if err != nil { 57 return "", err 58 } 59 // The filesystem is the second line. 60 lines := strings.Split(string(out), "\n") 61 if len(lines) < 2 { 62 logger.Errorf("unexpected output: %q", out) 63 return "", fmt.Errorf("could not determine filesystem type") 64 } 65 return lines[1], nil 66 } 67 68 type containerManager struct { 69 name string 70 logdir string 71 createWithClone bool 72 useAUFS bool 73 backingFilesystem string 74 } 75 76 // containerManager implements container.Manager. 77 var _ container.Manager = (*containerManager)(nil) 78 79 // NewContainerManager returns a manager object that can start and stop lxc 80 // containers. The containers that are created are namespaced by the name 81 // parameter. 82 func NewContainerManager(conf container.ManagerConfig) (container.Manager, error) { 83 name := conf.PopValue(container.ConfigName) 84 if name == "" { 85 return nil, fmt.Errorf("name is required") 86 } 87 logDir := conf.PopValue(container.ConfigLogDir) 88 if logDir == "" { 89 logDir = agent.DefaultLogDir 90 } 91 var useClone bool 92 useCloneVal := conf.PopValue("use-clone") 93 if useCloneVal != "" { 94 // Explicitly ignore the error result from ParseBool. 95 // If it fails to parse, the value is false, and this suits 96 // us fine. 97 useClone, _ = strconv.ParseBool(useCloneVal) 98 } else { 99 // If no lxc-clone value is explicitly set in config, then 100 // see if the Ubuntu series we are running on supports it 101 // and if it does, we will use clone. 102 useClone = preferFastLXC(releaseVersion()) 103 } 104 useAUFS, _ := strconv.ParseBool(conf.PopValue("use-aufs")) 105 backingFS, err := containerDirFilesystem() 106 if err != nil { 107 // Especially in tests, or a bot, the lxc dir may not exist 108 // causing the test to fail. Since we only really care if the 109 // backingFS is 'btrfs' and we treat the rest the same, just 110 // call it 'unknown'. 111 backingFS = "unknown" 112 } 113 logger.Tracef("backing filesystem: %q", backingFS) 114 conf.WarnAboutUnused() 115 return &containerManager{ 116 name: name, 117 logdir: logDir, 118 createWithClone: useClone, 119 useAUFS: useAUFS, 120 backingFilesystem: backingFS, 121 }, nil 122 } 123 124 // releaseVersion is a function that returns a string representing the 125 // DISTRIB_RELEASE from the /etc/lsb-release file. 126 var releaseVersion = version.ReleaseVersion 127 128 // preferFastLXC returns true if the host is capable of 129 // LXC cloning from a template. 130 func preferFastLXC(release string) bool { 131 if release == "" { 132 return false 133 } 134 value, err := strconv.ParseFloat(release, 64) 135 if err != nil { 136 return false 137 } 138 return value >= 14.04 139 } 140 141 func (manager *containerManager) CreateContainer( 142 machineConfig *cloudinit.MachineConfig, 143 series string, 144 network *container.NetworkConfig, 145 ) (instance.Instance, *instance.HardwareCharacteristics, error) { 146 start := time.Now() 147 name := names.MachineTag(machineConfig.MachineId) 148 if manager.name != "" { 149 name = fmt.Sprintf("%s-%s", manager.name, name) 150 } 151 // Create the cloud-init. 152 directory, err := container.NewDirectory(name) 153 if err != nil { 154 return nil, nil, err 155 } 156 logger.Tracef("write cloud-init") 157 if manager.createWithClone { 158 // If we are using clone, disable the apt-get steps 159 machineConfig.DisablePackageCommands = true 160 } 161 userDataFilename, err := container.WriteUserData(machineConfig, directory) 162 if err != nil { 163 logger.Errorf("failed to write user data: %v", err) 164 return nil, nil, err 165 } 166 logger.Tracef("write the lxc.conf file") 167 configFile, err := writeLxcConfig(network, directory) 168 if err != nil { 169 logger.Errorf("failed to write config file: %v", err) 170 return nil, nil, err 171 } 172 173 var lxcContainer golxc.Container 174 if manager.createWithClone { 175 templateContainer, err := EnsureCloneTemplate( 176 manager.backingFilesystem, 177 series, 178 network, 179 machineConfig.AuthorizedKeys, 180 machineConfig.AptProxySettings, 181 ) 182 if err != nil { 183 return nil, nil, err 184 } 185 templateParams := []string{ 186 "--debug", // Debug errors in the cloud image 187 "--userdata", userDataFilename, // Our groovey cloud-init 188 "--hostid", name, // Use the container name as the hostid 189 } 190 var extraCloneArgs []string 191 if manager.backingFilesystem == Btrfs || manager.useAUFS { 192 extraCloneArgs = append(extraCloneArgs, "--snapshot") 193 } 194 if manager.backingFilesystem != Btrfs && manager.useAUFS { 195 extraCloneArgs = append(extraCloneArgs, "--backingstore", "aufs") 196 } 197 198 lock, err := AcquireTemplateLock(templateContainer.Name(), "clone") 199 if err != nil { 200 return nil, nil, fmt.Errorf("failed to acquire lock on template: %v", err) 201 } 202 defer lock.Unlock() 203 lxcContainer, err = templateContainer.Clone(name, extraCloneArgs, templateParams) 204 if err != nil { 205 logger.Errorf("lxc container cloning failed: %v", err) 206 return nil, nil, err 207 } 208 } else { 209 // Note here that the lxcObjectFacotry only returns a valid container 210 // object, and doesn't actually construct the underlying lxc container on 211 // disk. 212 lxcContainer = LxcObjectFactory.New(name) 213 templateParams := []string{ 214 "--debug", // Debug errors in the cloud image 215 "--userdata", userDataFilename, // Our groovey cloud-init 216 "--hostid", name, // Use the container name as the hostid 217 "-r", series, 218 } 219 // Create the container. 220 logger.Tracef("create the container") 221 if err := lxcContainer.Create(configFile, defaultTemplate, nil, templateParams); err != nil { 222 logger.Errorf("lxc container creation failed: %v", err) 223 return nil, nil, err 224 } 225 logger.Tracef("lxc container created") 226 } 227 if err := autostartContainer(name); err != nil { 228 return nil, nil, err 229 } 230 if err := mountHostLogDir(name, manager.logdir); err != nil { 231 return nil, nil, err 232 } 233 // Start the lxc container with the appropriate settings for grabbing the 234 // console output and a log file. 235 consoleFile := filepath.Join(directory, "console.log") 236 lxcContainer.SetLogFile(filepath.Join(directory, "container.log"), golxc.LogDebug) 237 logger.Tracef("start the container") 238 // We explicitly don't pass through the config file to the container.Start 239 // method as we have passed it through at container creation time. This 240 // is necessary to get the appropriate rootfs reference without explicitly 241 // setting it ourselves. 242 if err = lxcContainer.Start("", consoleFile); err != nil { 243 logger.Errorf("container failed to start: %v", err) 244 return nil, nil, err 245 } 246 arch := version.Current.Arch 247 hardware := &instance.HardwareCharacteristics{ 248 Arch: &arch, 249 } 250 logger.Tracef("container %q started: %v", name, time.Now().Sub(start)) 251 return &lxcInstance{lxcContainer, name}, hardware, nil 252 } 253 254 func appendToContainerConfig(name, line string) error { 255 file, err := os.OpenFile( 256 containerConfigFilename(name), os.O_RDWR|os.O_APPEND, 0644) 257 if err != nil { 258 return err 259 } 260 defer file.Close() 261 _, err = file.WriteString(line) 262 return err 263 } 264 265 func autostartContainer(name string) error { 266 // Now symlink the config file into the restart directory, if it exists. 267 // This is for backwards compatiblity. From Trusty onwards, the auto start 268 // option should be set in the LXC config file, this is done in the networkConfigTemplate 269 // function below. 270 if useRestartDir() { 271 if err := os.Symlink( 272 containerConfigFilename(name), 273 restartSymlink(name), 274 ); err != nil { 275 return err 276 } 277 logger.Tracef("auto-restart link created") 278 } else { 279 logger.Tracef("Setting auto start to true in lxc config.") 280 return appendToContainerConfig(name, "lxc.start.auto = 1\n") 281 } 282 return nil 283 } 284 285 func mountHostLogDir(name, logDir string) error { 286 // Make sure that the mount dir has been created. 287 logger.Tracef("make the mount dir for the shared logs") 288 if err := os.MkdirAll(internalLogDir(name), 0755); err != nil { 289 logger.Errorf("failed to create internal /var/log/juju mount dir: %v", err) 290 return err 291 } 292 line := fmt.Sprintf( 293 "lxc.mount.entry=%s var/log/juju none defaults,bind 0 0\n", 294 logDir) 295 return appendToContainerConfig(name, line) 296 } 297 298 func (manager *containerManager) DestroyContainer(id instance.Id) error { 299 start := time.Now() 300 name := string(id) 301 lxcContainer := LxcObjectFactory.New(name) 302 if useRestartDir() { 303 // Remove the autostart link. 304 if err := os.Remove(restartSymlink(name)); err != nil { 305 logger.Errorf("failed to remove restart symlink: %v", err) 306 return err 307 } 308 } 309 if err := lxcContainer.Destroy(); err != nil { 310 logger.Errorf("failed to destroy lxc container: %v", err) 311 return err 312 } 313 314 err := container.RemoveDirectory(name) 315 logger.Tracef("container %q stopped: %v", name, time.Now().Sub(start)) 316 return err 317 } 318 319 func (manager *containerManager) ListContainers() (result []instance.Instance, err error) { 320 containers, err := LxcObjectFactory.List() 321 if err != nil { 322 logger.Errorf("failed getting all instances: %v", err) 323 return 324 } 325 managerPrefix := "" 326 if manager.name != "" { 327 managerPrefix = fmt.Sprintf("%s-", manager.name) 328 } 329 330 for _, container := range containers { 331 // Filter out those not starting with our name. 332 name := container.Name() 333 if !strings.HasPrefix(name, managerPrefix) { 334 continue 335 } 336 if container.IsRunning() { 337 result = append(result, &lxcInstance{container, name}) 338 } 339 } 340 return 341 } 342 343 const internalLogDirTemplate = "%s/%s/rootfs/var/log/juju" 344 345 func internalLogDir(containerName string) string { 346 return fmt.Sprintf(internalLogDirTemplate, LxcContainerDir, containerName) 347 } 348 349 func restartSymlink(name string) string { 350 return filepath.Join(LxcRestartDir, name+".conf") 351 } 352 353 func containerConfigFilename(name string) string { 354 return filepath.Join(LxcContainerDir, name, "config") 355 } 356 357 const networkTemplate = ` 358 lxc.network.type = %s 359 lxc.network.link = %s 360 lxc.network.flags = up 361 ` 362 363 func networkConfigTemplate(networkType, networkLink string) string { 364 return fmt.Sprintf(networkTemplate, networkType, networkLink) 365 } 366 367 func generateNetworkConfig(network *container.NetworkConfig) string { 368 var lxcConfig string 369 if network == nil { 370 logger.Warningf("network unspecified, using default networking config") 371 network = DefaultNetworkConfig() 372 } 373 switch network.NetworkType { 374 case container.PhysicalNetwork: 375 lxcConfig = networkConfigTemplate("phys", network.Device) 376 default: 377 logger.Warningf("Unknown network config type %q: using bridge", network.NetworkType) 378 fallthrough 379 case container.BridgeNetwork: 380 lxcConfig = networkConfigTemplate("veth", network.Device) 381 } 382 383 return lxcConfig 384 } 385 386 func writeLxcConfig(network *container.NetworkConfig, directory string) (string, error) { 387 networkConfig := generateNetworkConfig(network) 388 configFilename := filepath.Join(directory, "lxc.conf") 389 if err := ioutil.WriteFile(configFilename, []byte(networkConfig), 0644); err != nil { 390 return "", err 391 } 392 return configFilename, nil 393 } 394 395 // useRestartDir is used to determine whether or not to use a symlink to the 396 // container config as the restart mechanism. Older versions of LXC had the 397 // /etc/lxc/auto directory that would indicate that a container shoud auto- 398 // restart when the machine boots by having a symlink to the lxc.conf file. 399 // Newer versions don't do this, but instead have a config value inside the 400 // lxc.conf file. 401 func useRestartDir() bool { 402 if _, err := os.Stat(LxcRestartDir); err != nil { 403 if os.IsNotExist(err) { 404 return false 405 } 406 } 407 return true 408 }