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  }