github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/container/lxc/clonetemplate.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lxc
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"path/filepath"
    11  	"sync"
    12  	"time"
    13  
    14  	"github.com/juju/utils"
    15  	"github.com/juju/utils/fslock"
    16  	"github.com/juju/utils/proxy"
    17  	"github.com/juju/utils/tailer"
    18  	"launchpad.net/golxc"
    19  
    20  	coreCloudinit "github.com/juju/juju/cloudinit"
    21  	"github.com/juju/juju/container"
    22  	"github.com/juju/juju/environs/cloudinit"
    23  )
    24  
    25  const (
    26  	templateShutdownUpstartFilename = "/etc/init/juju-template-restart.conf"
    27  	templateShutdownUpstartScript   = `
    28  description "Juju lxc template shutdown job"
    29  author "Juju Team <juju@lists.ubuntu.com>"
    30  start on stopped cloud-final
    31  
    32  script
    33    shutdown -h now
    34  end script
    35  
    36  post-stop script
    37    rm ` + templateShutdownUpstartFilename + `
    38  end script
    39  `
    40  )
    41  
    42  var (
    43  	TemplateLockDir = "/var/lib/juju/locks"
    44  
    45  	TemplateStopTimeout = 5 * time.Minute
    46  )
    47  
    48  // templateUserData returns a minimal user data necessary for the template.
    49  // This should have the authorized keys, base packages, the cloud archive if
    50  // necessary,  initial apt proxy config, and it should do the apt-get
    51  // update/upgrade initially.
    52  func templateUserData(
    53  	series string,
    54  	authorizedKeys string,
    55  	aptProxy proxy.Settings,
    56  ) ([]byte, error) {
    57  	config := coreCloudinit.New()
    58  	config.AddScripts(
    59  		"set -xe", // ensure we run all the scripts or abort.
    60  	)
    61  	config.AddSSHAuthorizedKeys(authorizedKeys)
    62  	cloudinit.MaybeAddCloudArchiveCloudTools(config, series)
    63  	cloudinit.AddAptCommands(aptProxy, config)
    64  	config.AddScripts(
    65  		fmt.Sprintf(
    66  			"printf '%%s\n' %s > %s",
    67  			utils.ShQuote(templateShutdownUpstartScript),
    68  			templateShutdownUpstartFilename,
    69  		))
    70  	data, err := config.Render()
    71  	if err != nil {
    72  		return nil, err
    73  	}
    74  	return data, nil
    75  }
    76  
    77  func AcquireTemplateLock(name, message string) (*fslock.Lock, error) {
    78  	logger.Infof("wait for fslock on %v", name)
    79  	lock, err := fslock.NewLock(TemplateLockDir, name)
    80  	if err != nil {
    81  		logger.Tracef("failed to create fslock for template: %v", err)
    82  		return nil, err
    83  	}
    84  	err = lock.Lock(message)
    85  	if err != nil {
    86  		logger.Tracef("failed to acquire lock for template: %v", err)
    87  		return nil, err
    88  	}
    89  	return lock, nil
    90  }
    91  
    92  // Make sure a template exists that we can clone from.
    93  func EnsureCloneTemplate(
    94  	backingFilesystem string,
    95  	series string,
    96  	network *container.NetworkConfig,
    97  	authorizedKeys string,
    98  	aptProxy proxy.Settings,
    99  ) (golxc.Container, error) {
   100  	name := fmt.Sprintf("juju-%s-template", series)
   101  	containerDirectory, err := container.NewDirectory(name)
   102  	if err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	lock, err := AcquireTemplateLock(name, "ensure clone exists")
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	defer lock.Unlock()
   111  
   112  	lxcContainer := LxcObjectFactory.New(name)
   113  	// Early exit if the container has been constructed before.
   114  	if lxcContainer.IsConstructed() {
   115  		logger.Infof("template exists, continuing")
   116  		return lxcContainer, nil
   117  	}
   118  	logger.Infof("template does not exist, creating")
   119  
   120  	userData, err := templateUserData(series, authorizedKeys, aptProxy)
   121  	if err != nil {
   122  		logger.Tracef("failed to create template user data for template: %v", err)
   123  		return nil, err
   124  	}
   125  	userDataFilename, err := container.WriteCloudInitFile(containerDirectory, userData)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	configFile, err := writeLxcConfig(network, containerDirectory)
   131  	if err != nil {
   132  		logger.Errorf("failed to write config file: %v", err)
   133  		return nil, err
   134  	}
   135  	templateParams := []string{
   136  		"--debug",                      // Debug errors in the cloud image
   137  		"--userdata", userDataFilename, // Our groovey cloud-init
   138  		"--hostid", name, // Use the container name as the hostid
   139  		"-r", series,
   140  	}
   141  	var extraCreateArgs []string
   142  	if backingFilesystem == Btrfs {
   143  		extraCreateArgs = append(extraCreateArgs, "-B", Btrfs)
   144  	}
   145  	// Create the container.
   146  	logger.Tracef("create the container")
   147  	if err := lxcContainer.Create(configFile, defaultTemplate, extraCreateArgs, templateParams); err != nil {
   148  		logger.Errorf("lxc container creation failed: %v", err)
   149  		return nil, err
   150  	}
   151  	// Make sure that the mount dir has been created.
   152  	logger.Tracef("make the mount dir for the shared logs")
   153  	if err := os.MkdirAll(internalLogDir(name), 0755); err != nil {
   154  		logger.Tracef("failed to create internal /var/log/juju mount dir: %v", err)
   155  		return nil, err
   156  	}
   157  
   158  	// Start the lxc container with the appropriate settings for grabbing the
   159  	// console output and a log file.
   160  	consoleFile := filepath.Join(containerDirectory, "console.log")
   161  	lxcContainer.SetLogFile(filepath.Join(containerDirectory, "container.log"), golxc.LogDebug)
   162  	logger.Tracef("start the container")
   163  	// We explicitly don't pass through the config file to the container.Start
   164  	// method as we have passed it through at container creation time.  This
   165  	// is necessary to get the appropriate rootfs reference without explicitly
   166  	// setting it ourselves.
   167  	if err = lxcContainer.Start("", consoleFile); err != nil {
   168  		logger.Errorf("container failed to start: %v", err)
   169  		return nil, err
   170  	}
   171  	logger.Infof("template container started, now wait for it to stop")
   172  	// Perhaps we should wait for it to finish, and the question becomes "how
   173  	// long do we wait for it to complete?"
   174  
   175  	console, err := os.Open(consoleFile)
   176  	if err != nil {
   177  		// can't listen
   178  		return nil, err
   179  	}
   180  
   181  	tailWriter := &logTail{tick: time.Now()}
   182  	consoleTailer := tailer.NewTailer(console, tailWriter, nil)
   183  	defer consoleTailer.Stop()
   184  
   185  	// We should wait maybe 1 minute between output?
   186  	// if no output check to see if stopped
   187  	// If we have no output and still running, something has probably gone wrong
   188  	for lxcContainer.IsRunning() {
   189  		if tailWriter.lastTick().Before(time.Now().Add(-TemplateStopTimeout)) {
   190  			logger.Infof("not heard anything from the template log for five minutes")
   191  			return nil, fmt.Errorf("template container %q did not stop", name)
   192  		}
   193  		time.Sleep(time.Second)
   194  	}
   195  
   196  	return lxcContainer, nil
   197  }
   198  
   199  type logTail struct {
   200  	tick  time.Time
   201  	mutex sync.Mutex
   202  }
   203  
   204  var _ io.Writer = (*logTail)(nil)
   205  
   206  func (t *logTail) Write(data []byte) (int, error) {
   207  	logger.Tracef(string(data))
   208  	t.mutex.Lock()
   209  	defer t.mutex.Unlock()
   210  	t.tick = time.Now()
   211  	return len(data), nil
   212  }
   213  
   214  func (t *logTail) lastTick() time.Time {
   215  	t.mutex.Lock()
   216  	defer t.mutex.Unlock()
   217  	tick := t.tick
   218  	return tick
   219  }