Merge lp:~thumper/juju-core/fast-lxc-no-apt-upgrade into lp:~go-bot/juju-core/trunk

Proposed by Tim Penhey
Status: Merged
Approved by: Tim Penhey
Approved revision: no longer in the source branch.
Merged at revision: 2412
Proposed branch: lp:~thumper/juju-core/fast-lxc-no-apt-upgrade
Merge into: lp:~go-bot/juju-core/trunk
Prerequisite: lp:~thumper/juju-core/update-golxc-version
Diff against target: 983 lines (+526/-64)
10 files modified
container/interface.go (+16/-0)
container/kvm/kvm.go (+3/-8)
container/kvm/kvm_test.go (+1/-1)
container/lxc/clonetemplate.go (+219/-0)
container/lxc/lxc.go (+75/-30)
container/lxc/lxc_test.go (+110/-3)
container/lxc/mock/mock-lxc.go (+87/-19)
container/userdata.go (+9/-3)
provider/local/environ.go (+2/-0)
worker/provisioner/lxc-broker_test.go (+4/-0)
To merge this branch: bzr merge lp:~thumper/juju-core/fast-lxc-no-apt-upgrade
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+210536@code.launchpad.net

Commit message

Use lxc clone on trusty

This branch speeds up lxc containers for the local
provider only by creating a template as needed that
persists between environments, and this template is
then used as a basis for the machines created in the
environment using lxc-clone.

If /var/lib/lxc is a btrfs filesystem, the containers
use btrfs snapshots. If not, the containers are
created using aufs snapshots.

In addition to this, the apt-get update/upgrade step
is skipped during cloud-init for the cloned machines.

https://codereview.appspot.com/74370044/

Description of the change

Use lxc clone on trusty

This branch speeds up lxc containers for the local
provider only by creating a template as needed that
persists between environments, and this template is
then used as a basis for the machines created in the
environment using lxc-clone.

If /var/lib/lxc is a btrfs filesystem, the containers
use btrfs snapshots. If not, the containers are
created using aufs snapshots.

In addition to this, the apt-get update/upgrade step
is skipped during cloud-init for the cloned machines.

Since the testing required a go routine to shut down
the template container, the mock containers had to be
made go routine safe for the state checking.

Certain extra info was also added for the mock containers,
being the config file and console log.

https://codereview.appspot.com/74370044/

To post a comment you must log in.
Revision history for this message
Tim Penhey (thumper) wrote :

Reviewers: mp+210536_code.launchpad.net,

Message:
Please take a look.

Description:
Use lxc clone on trusty

This branch speeds up lxc containers for the local
provider only by creating a template as needed that
persists between environments, and this template is
then used as a basis for the machines created in the
environment using lxc-clone.

If /var/lib/lxc is a btrfs filesystem, the containers
use btrfs snapshots. If not, the containers are
created using aufs snapshots.

In addition to this, the apt-get update/upgrade step
is skipped during cloud-init for the cloned machines.

Since the testing required a go routine to shut down
the template container, the mock containers had to be
made go routine safe for the state checking.

Certain extra info was also added for the mock containers,
being the config file and console log.

https://code.launchpad.net/~thumper/juju-core/fast-lxc-no-apt-upgrade/+merge/210536

Requires:
https://code.launchpad.net/~thumper/juju-core/update-golxc-version/+merge/210492

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/74370044/

Affected files (+485, -46 lines):
   A [revision details]
   A container/lxc/clonetemplate.go
   M container/lxc/lxc.go
   M container/lxc/lxc_test.go
   M container/lxc/mock/mock-lxc.go
   M container/userdata.go
   M provider/local/environ.go
   M worker/provisioner/lxc-broker_test.go

Revision history for this message
Ian Booth (wallyworld) wrote :

I'm not fully across the finer details about lxc cloning. I think this
looks good. Perhaps a little more test coverage though. Specifically:
1. EnsureCloneTemplate behaves correctly if called when a template
already exists
2. The cloud init contents are devoid of apt stuff

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc.go
File container/lxc/lxc.go (right):

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc.go#newcode92
container/lxc/lxc.go:92: if conf["use-clone"] == "true" {
Is there a helper function for str -> bool?

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc_test.go
File container/lxc/lxc_test.go (right):

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc_test.go#newcode165
container/lxc/lxc_test.go:165: func (s *LxcSuite) AssertEvent(c *gc.C,
event mock.Event, expected mock.Action, id string) {
does this need to be exported?

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc_test.go#newcode203
container/lxc/lxc_test.go:203: c.Assert(template.Name(), gc.Equals,
name)
Should we be checking (some key aspects of) the contents of the
template?

https://codereview.appspot.com/74370044/

Revision history for this message
Sidnei da Silva (sidnei) wrote :

https://codereview.appspot.com/74370044/diff/1/container/lxc/clonetemplate.go
File container/lxc/clonetemplate.go (right):

https://codereview.appspot.com/74370044/diff/1/container/lxc/clonetemplate.go#newcode142
container/lxc/clonetemplate.go:142: if backingFilesystem == Btrfs {
I find it odd that backingFilesystem is special-cased for btrfs, since
the version of lxc in trusty has support for picking the right backing
filesystem if -Bbest is specified, which would work for either btrfs or
lvm thin provisioning.

https://codereview.appspot.com/74370044/

Revision history for this message
Tim Penhey (thumper) wrote :

Please take a look.

https://codereview.appspot.com/74370044/diff/1/container/lxc/clonetemplate.go
File container/lxc/clonetemplate.go (right):

https://codereview.appspot.com/74370044/diff/1/container/lxc/clonetemplate.go#newcode142
container/lxc/clonetemplate.go:142: if backingFilesystem == Btrfs {
On 2014/03/12 08:31:38, sidnei.da.silva wrote:
> I find it odd that backingFilesystem is special-cased for btrfs, since
the
> version of lxc in trusty has support for picking the right backing
filesystem if
> -Bbest is specified, which would work for either btrfs or lvm thin
provisioning.

man lxc-create doesn't show this.

Post chat: yes -B best does exist, but not documented. However, it
looks for the following in order: "btrfs", "zfs", "lvm", "dir". Since
we have no easy way to determine when to use "aufs" if we are allowing
"best", it is easier to be explicit in our support with btrfs now, and
have all others use aufs.
I'll talk with hallyn

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc.go
File container/lxc/lxc.go (right):

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc.go#newcode92
container/lxc/lxc.go:92: if conf["use-clone"] == "true" {
On 2014/03/12 05:42:25, wallyworld wrote:
> Is there a helper function for str -> bool?

Yes there is. Using it now.

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc_test.go
File container/lxc/lxc_test.go (right):

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc_test.go#newcode165
container/lxc/lxc_test.go:165: func (s *LxcSuite) AssertEvent(c *gc.C,
event mock.Event, expected mock.Action, id string) {
On 2014/03/12 05:42:25, wallyworld wrote:
> does this need to be exported?

No it doesn't, but it fits with the other Assert statements.

Since "exporting" a test function has no meaning, I went with
consistency.

https://codereview.appspot.com/74370044/diff/1/container/lxc/lxc_test.go#newcode203
container/lxc/lxc_test.go:203: c.Assert(template.Name(), gc.Equals,
name)
On 2014/03/12 05:42:25, wallyworld wrote:
> Should we be checking (some key aspects of) the contents of the
template?

We could make sure that it isn't autostarting nor mounting the logdir by
looking at the config file. Worth it?

https://codereview.appspot.com/74370044/

Revision history for this message
Ian Booth (wallyworld) wrote :

LGTM. Let's get this landed, but I'd still like to see tests that the
template doesn't start etc.

https://codereview.appspot.com/74370044/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'container/interface.go'
2--- container/interface.go 2014-03-03 02:02:25 +0000
3+++ container/interface.go 2014-03-13 02:11:05 +0000
4@@ -39,3 +39,19 @@
5 // that the host machine can run containers.
6 Initialise() error
7 }
8+
9+// PopValue returns the requested key from the config map. If the value
10+// doesn't exist, the function returns the empty string. If the value does
11+// exist, the value is returned, and the element removed from the map.
12+func (m ManagerConfig) PopValue(key string) string {
13+ value := m[key]
14+ delete(m, key)
15+ return value
16+}
17+
18+// WarnAboutUnused emits a warning about each value in the map.
19+func (m ManagerConfig) WarnAboutUnused() {
20+ for key, value := range m {
21+ logger.Warningf("unused config option: %q -> %q", key, value)
22+ }
23+}
24
25=== modified file 'container/kvm/kvm.go'
26--- container/kvm/kvm.go 2014-03-11 01:17:39 +0000
27+++ container/kvm/kvm.go 2014-03-13 02:11:05 +0000
28@@ -56,20 +56,15 @@
29 // containers. The containers that are created are namespaced by the name
30 // parameter.
31 func NewContainerManager(conf container.ManagerConfig) (container.Manager, error) {
32- name := conf[container.ConfigName]
33- delete(conf, container.ConfigName)
34+ name := conf.PopValue(container.ConfigName)
35 if name == "" {
36 return nil, fmt.Errorf("name is required")
37 }
38- logDir := conf[container.ConfigLogDir]
39- delete(conf, container.ConfigLogDir)
40+ logDir := conf.PopValue(container.ConfigLogDir)
41 if logDir == "" {
42 logDir = agent.DefaultLogDir
43 }
44- for k, v := range conf {
45- logger.Warningf(`Found unused config option with key: "%v" and value: "%v"`, k, v)
46- }
47-
48+ conf.WarnAboutUnused()
49 return &containerManager{name: name, logdir: logDir}, nil
50 }
51
52
53=== modified file 'container/kvm/kvm_test.go'
54--- container/kvm/kvm_test.go 2014-03-11 01:17:39 +0000
55+++ container/kvm/kvm_test.go 2014-03-13 02:11:05 +0000
56@@ -46,7 +46,7 @@
57 "shazam": "Captain Marvel",
58 })
59 c.Assert(err, gc.IsNil)
60- c.Assert(c.GetTestLog(), gc.Matches, `^.*WARNING juju.container.kvm Found unused config option with key: "shazam" and value: "Captain Marvel"\n*`)
61+ c.Assert(c.GetTestLog(), jc.Contains, `WARNING juju.container unused config option: "shazam" -> "Captain Marvel"`)
62 }
63
64 func (s *KVMSuite) TestListInitiallyEmpty(c *gc.C) {
65
66=== added file 'container/lxc/clonetemplate.go'
67--- container/lxc/clonetemplate.go 1970-01-01 00:00:00 +0000
68+++ container/lxc/clonetemplate.go 2014-03-13 02:11:05 +0000
69@@ -0,0 +1,219 @@
70+// Copyright 2014 Canonical Ltd.
71+// Licensed under the AGPLv3, see LICENCE file for details.
72+
73+package lxc
74+
75+import (
76+ "fmt"
77+ "io"
78+ "os"
79+ "path/filepath"
80+ "sync"
81+ "time"
82+
83+ "launchpad.net/golxc"
84+
85+ coreCloudinit "launchpad.net/juju-core/cloudinit"
86+ "launchpad.net/juju-core/container"
87+ "launchpad.net/juju-core/environs/cloudinit"
88+ "launchpad.net/juju-core/juju/osenv"
89+ "launchpad.net/juju-core/utils"
90+ "launchpad.net/juju-core/utils/fslock"
91+ "launchpad.net/juju-core/utils/tailer"
92+)
93+
94+const (
95+ templateShutdownUpstartFilename = "/etc/init/juju-template-restart.conf"
96+ templateShutdownUpstartScript = `
97+description "Juju lxc template shutdown job"
98+author "Juju Team <juju@lists.ubuntu.com>"
99+start on stopped cloud-final
100+
101+script
102+ shutdown -h now
103+end script
104+
105+post-stop script
106+ rm ` + templateShutdownUpstartFilename + `
107+end script
108+`
109+)
110+
111+var (
112+ TemplateLockDir = "/var/lib/juju/locks"
113+
114+ TemplateStopTimeout = 5 * time.Minute
115+)
116+
117+// templateUserData returns a minimal user data necessary for the template.
118+// This should have the authorized keys, base packages, the cloud archive if
119+// necessary, initial apt proxy config, and it should do the apt-get
120+// update/upgrade initially.
121+func templateUserData(
122+ series string,
123+ authorizedKeys string,
124+ aptProxy osenv.ProxySettings,
125+) ([]byte, error) {
126+ config := coreCloudinit.New()
127+ config.AddScripts(
128+ "set -xe", // ensure we run all the scripts or abort.
129+ )
130+ config.AddSSHAuthorizedKeys(authorizedKeys)
131+ cloudinit.MaybeAddCloudArchiveCloudTools(config, series)
132+ cloudinit.AddAptCommands(aptProxy, config)
133+ config.AddScripts(
134+ fmt.Sprintf(
135+ "printf '%%s\n' %s > %s",
136+ utils.ShQuote(templateShutdownUpstartScript),
137+ templateShutdownUpstartFilename,
138+ ))
139+ data, err := config.Render()
140+ if err != nil {
141+ return nil, err
142+ }
143+ return data, nil
144+}
145+
146+func AcquireTemplateLock(name, message string) (*fslock.Lock, error) {
147+ logger.Infof("wait for fslock on %v", name)
148+ lock, err := fslock.NewLock(TemplateLockDir, name)
149+ if err != nil {
150+ logger.Tracef("failed to create fslock for template: %v", err)
151+ return nil, err
152+ }
153+ err = lock.Lock(message)
154+ if err != nil {
155+ logger.Tracef("failed to acquire lock for template: %v", err)
156+ return nil, err
157+ }
158+ return lock, nil
159+}
160+
161+// Make sure a template exists that we can clone from.
162+func EnsureCloneTemplate(
163+ backingFilesystem string,
164+ series string,
165+ network *container.NetworkConfig,
166+ authorizedKeys string,
167+ aptProxy osenv.ProxySettings,
168+) (golxc.Container, error) {
169+ name := fmt.Sprintf("juju-%s-template", series)
170+ containerDirectory, err := container.NewDirectory(name)
171+ if err != nil {
172+ return nil, err
173+ }
174+
175+ lock, err := AcquireTemplateLock(name, "ensure clone exists")
176+ if err != nil {
177+ return nil, err
178+ }
179+ defer lock.Unlock()
180+
181+ lxcContainer := LxcObjectFactory.New(name)
182+ // Early exit if the container has been constructed before.
183+ if lxcContainer.IsConstructed() {
184+ logger.Infof("template exists, continuing")
185+ return lxcContainer, nil
186+ }
187+ logger.Infof("template does not exist, creating")
188+
189+ userData, err := templateUserData(series, authorizedKeys, aptProxy)
190+ if err != nil {
191+ logger.Tracef("filed to create tempalte user data for template: %v", err)
192+ return nil, err
193+ }
194+ userDataFilename, err := container.WriteCloudInitFile(containerDirectory, userData)
195+ if err != nil {
196+ return nil, err
197+ }
198+
199+ configFile, err := writeLxcConfig(network, containerDirectory)
200+ if err != nil {
201+ logger.Errorf("failed to write config file: %v", err)
202+ return nil, err
203+ }
204+ templateParams := []string{
205+ "--debug", // Debug errors in the cloud image
206+ "--userdata", userDataFilename, // Our groovey cloud-init
207+ "--hostid", name, // Use the container name as the hostid
208+ "-r", series,
209+ }
210+ var extraCreateArgs []string
211+ if backingFilesystem == Btrfs {
212+ extraCreateArgs = append(extraCreateArgs, "-B", Btrfs)
213+ }
214+ // Create the container.
215+ logger.Tracef("create the container")
216+ if err := lxcContainer.Create(configFile, defaultTemplate, extraCreateArgs, templateParams); err != nil {
217+ logger.Errorf("lxc container creation failed: %v", err)
218+ return nil, err
219+ }
220+ // Make sure that the mount dir has been created.
221+ logger.Tracef("make the mount dir for the shared logs")
222+ if err := os.MkdirAll(internalLogDir(name), 0755); err != nil {
223+ logger.Tracef("failed to create internal /var/log/juju mount dir: %v", err)
224+ return nil, err
225+ }
226+
227+ // Start the lxc container with the appropriate settings for grabbing the
228+ // console output and a log file.
229+ consoleFile := filepath.Join(containerDirectory, "console.log")
230+ lxcContainer.SetLogFile(filepath.Join(containerDirectory, "container.log"), golxc.LogDebug)
231+ logger.Tracef("start the container")
232+ // We explicitly don't pass through the config file to the container.Start
233+ // method as we have passed it through at container creation time. This
234+ // is necessary to get the appropriate rootfs reference without explicitly
235+ // setting it ourselves.
236+ if err = lxcContainer.Start("", consoleFile); err != nil {
237+ logger.Errorf("container failed to start: %v", err)
238+ return nil, err
239+ }
240+ logger.Infof("template container started, now wait for it to stop")
241+ // Perhaps we should wait for it to finish, and the question becomes "how
242+ // long do we wait for it to complete?"
243+
244+ console, err := os.Open(consoleFile)
245+ if err != nil {
246+ // can't listen
247+ return nil, err
248+ }
249+
250+ tailWriter := &logTail{tick: time.Now()}
251+ consoleTailer := tailer.NewTailer(console, tailWriter, 0, nil)
252+ defer consoleTailer.Stop()
253+
254+ // We should wait maybe 1 minute between output?
255+ // if no output check to see if stopped
256+ // If we have no output and still running, something has probably gone wrong
257+ for lxcContainer.IsRunning() {
258+ if tailWriter.lastTick().Before(time.Now().Add(-TemplateStopTimeout)) {
259+ logger.Infof("not heard anything from the template log for five minutes")
260+ return nil, fmt.Errorf("template container %q did not stop", name)
261+ }
262+ time.Sleep(time.Second)
263+ }
264+
265+ return lxcContainer, nil
266+}
267+
268+type logTail struct {
269+ tick time.Time
270+ mutex sync.Mutex
271+}
272+
273+var _ io.Writer = (*logTail)(nil)
274+
275+func (t *logTail) Write(data []byte) (int, error) {
276+ logger.Tracef(string(data))
277+ t.mutex.Lock()
278+ defer t.mutex.Unlock()
279+ t.tick = time.Now()
280+ return len(data), nil
281+}
282+
283+func (t *logTail) lastTick() time.Time {
284+ t.mutex.Lock()
285+ defer t.mutex.Unlock()
286+ tick := t.tick
287+ return tick
288+}
289
290=== modified file 'container/lxc/lxc.go'
291--- container/lxc/lxc.go 2014-03-12 02:13:38 +0000
292+++ container/lxc/lxc.go 2014-03-13 02:11:05 +0000
293@@ -9,7 +9,9 @@
294 "os"
295 "os/exec"
296 "path/filepath"
297+ "strconv"
298 "strings"
299+ "time"
300
301 "github.com/juju/loggo"
302 "launchpad.net/golxc"
303@@ -66,6 +68,7 @@
304 type containerManager struct {
305 name string
306 logdir string
307+ createWithClone bool
308 backingFilesystem string
309 }
310
311@@ -76,16 +79,18 @@
312 // containers. The containers that are created are namespaced by the name
313 // parameter.
314 func NewContainerManager(conf container.ManagerConfig) (container.Manager, error) {
315- name := conf[container.ConfigName]
316- delete(conf, container.ConfigName)
317+ name := conf.PopValue(container.ConfigName)
318 if name == "" {
319 return nil, fmt.Errorf("name is required")
320 }
321- logDir := conf[container.ConfigLogDir]
322- delete(conf, container.ConfigLogDir)
323+ logDir := conf.PopValue(container.ConfigLogDir)
324 if logDir == "" {
325 logDir = agent.DefaultLogDir
326 }
327+ // Explicitly ignore the error result from ParseBool.
328+ // If it fails to parse, the value is false, and this suits
329+ // us fine.
330+ useClone, _ := strconv.ParseBool(conf.PopValue("use-clone"))
331 backingFS, err := containerDirFilesystem()
332 if err != nil {
333 // Especially in tests, or a bot, the lxc dir may not exist
334@@ -95,12 +100,11 @@
335 backingFS = "unknown"
336 }
337 logger.Tracef("backing filesystem: %q", backingFS)
338- for k, v := range conf {
339- logger.Warningf(`Found unused config option with key: "%v" and value: "%v"`, k, v)
340- }
341+ conf.WarnAboutUnused()
342 return &containerManager{
343 name: name,
344 logdir: logDir,
345+ createWithClone: useClone,
346 backingFilesystem: backingFS,
347 }, nil
348 }
349@@ -108,23 +112,23 @@
350 func (manager *containerManager) StartContainer(
351 machineConfig *cloudinit.MachineConfig,
352 series string,
353- network *container.NetworkConfig) (instance.Instance, *instance.HardwareCharacteristics, error) {
354-
355+ network *container.NetworkConfig,
356+) (instance.Instance, *instance.HardwareCharacteristics, error) {
357+ start := time.Now()
358 name := names.MachineTag(machineConfig.MachineId)
359 if manager.name != "" {
360 name = fmt.Sprintf("%s-%s", manager.name, name)
361 }
362- // Note here that the lxcObjectFacotry only returns a valid container
363- // object, and doesn't actually construct the underlying lxc container on
364- // disk.
365- lxcContainer := LxcObjectFactory.New(name)
366-
367 // Create the cloud-init.
368 directory, err := container.NewDirectory(name)
369 if err != nil {
370 return nil, nil, err
371 }
372 logger.Tracef("write cloud-init")
373+ if manager.createWithClone {
374+ // If we are using clone, disable the apt-get steps
375+ machineConfig.DisablePackageCommands = true
376+ }
377 userDataFilename, err := container.WriteUserData(machineConfig, directory)
378 if err != nil {
379 logger.Errorf("failed to write user data: %v", err)
380@@ -136,20 +140,58 @@
381 logger.Errorf("failed to write config file: %v", err)
382 return nil, nil, err
383 }
384- templateParams := []string{
385- "--debug", // Debug errors in the cloud image
386- "--userdata", userDataFilename, // Our groovey cloud-init
387- "--hostid", name, // Use the container name as the hostid
388- "-r", series,
389- }
390- // Create the container.
391- logger.Tracef("create the container")
392- if err := lxcContainer.Create(configFile, defaultTemplate, nil, templateParams); err != nil {
393- logger.Errorf("lxc container creation failed: %v", err)
394- return nil, nil, err
395- }
396- logger.Tracef("lxc container created")
397-
398+
399+ var lxcContainer golxc.Container
400+ if manager.createWithClone {
401+ templateContainer, err := EnsureCloneTemplate(
402+ manager.backingFilesystem,
403+ series,
404+ network,
405+ machineConfig.AuthorizedKeys,
406+ machineConfig.AptProxySettings,
407+ )
408+ if err != nil {
409+ return nil, nil, err
410+ }
411+ templateParams := []string{
412+ "--debug", // Debug errors in the cloud image
413+ "--userdata", userDataFilename, // Our groovey cloud-init
414+ "--hostid", name, // Use the container name as the hostid
415+ }
416+ extraCloneArgs := []string{"--snapshot"}
417+ if manager.backingFilesystem != Btrfs {
418+ extraCloneArgs = append(extraCloneArgs, "--backingstore", "aufs")
419+ }
420+
421+ lock, err := AcquireTemplateLock(templateContainer.Name(), "clone")
422+ if err != nil {
423+ return nil, nil, fmt.Errorf("failed to acquire lock on template: %v", err)
424+ }
425+ defer lock.Unlock()
426+ lxcContainer, err = templateContainer.Clone(name, extraCloneArgs, templateParams)
427+ if err != nil {
428+ logger.Errorf("lxc container cloning failed: %v", err)
429+ return nil, nil, err
430+ }
431+ } else {
432+ // Note here that the lxcObjectFacotry only returns a valid container
433+ // object, and doesn't actually construct the underlying lxc container on
434+ // disk.
435+ lxcContainer = LxcObjectFactory.New(name)
436+ templateParams := []string{
437+ "--debug", // Debug errors in the cloud image
438+ "--userdata", userDataFilename, // Our groovey cloud-init
439+ "--hostid", name, // Use the container name as the hostid
440+ "-r", series,
441+ }
442+ // Create the container.
443+ logger.Tracef("create the container")
444+ if err := lxcContainer.Create(configFile, defaultTemplate, nil, templateParams); err != nil {
445+ logger.Errorf("lxc container creation failed: %v", err)
446+ return nil, nil, err
447+ }
448+ logger.Tracef("lxc container created")
449+ }
450 if err := autostartContainer(name); err != nil {
451 return nil, nil, err
452 }
453@@ -173,7 +215,7 @@
454 hardware := &instance.HardwareCharacteristics{
455 Arch: &arch,
456 }
457- logger.Tracef("container started")
458+ logger.Tracef("container %q started: %v", name, time.Now().Sub(start))
459 return &lxcInstance{lxcContainer, name}, hardware, nil
460 }
461
462@@ -222,6 +264,7 @@
463 }
464
465 func (manager *containerManager) StopContainer(instance instance.Instance) error {
466+ start := time.Now()
467 name := string(instance.Id())
468 lxcContainer := LxcObjectFactory.New(name)
469 if useRestartDir() {
470@@ -236,7 +279,9 @@
471 return err
472 }
473
474- return container.RemoveDirectory(name)
475+ err := container.RemoveDirectory(name)
476+ logger.Tracef("container %q stopped: %v", name, time.Now().Sub(start))
477+ return err
478 }
479
480 func (manager *containerManager) ListContainers() (result []instance.Instance, err error) {
481
482=== modified file 'container/lxc/lxc_test.go'
483--- container/lxc/lxc_test.go 2014-03-12 01:27:57 +0000
484+++ container/lxc/lxc_test.go 2014-03-13 02:11:05 +0000
485@@ -10,6 +10,7 @@
486 "path/filepath"
487 "strings"
488 stdtesting "testing"
489+ "time"
490
491 "github.com/juju/loggo"
492 gc "launchpad.net/gocheck"
493@@ -19,9 +20,11 @@
494 "launchpad.net/juju-core/agent"
495 "launchpad.net/juju-core/container"
496 "launchpad.net/juju-core/container/lxc"
497+ "launchpad.net/juju-core/container/lxc/mock"
498 lxctesting "launchpad.net/juju-core/container/lxc/testing"
499 containertesting "launchpad.net/juju-core/container/testing"
500 instancetest "launchpad.net/juju-core/instance/testing"
501+ "launchpad.net/juju-core/juju/osenv"
502 jc "launchpad.net/juju-core/testing/checkers"
503 "launchpad.net/juju-core/testing/testbase"
504 )
505@@ -32,6 +35,9 @@
506
507 type LxcSuite struct {
508 lxctesting.TestSuite
509+
510+ events chan mock.Event
511+ useClone bool
512 }
513
514 var _ = gc.Suite(&LxcSuite{})
515@@ -39,6 +45,16 @@
516 func (s *LxcSuite) SetUpTest(c *gc.C) {
517 s.TestSuite.SetUpTest(c)
518 loggo.GetLogger("juju.container.lxc").SetLogLevel(loggo.TRACE)
519+ s.events = make(chan mock.Event, 25)
520+ s.TestSuite.Factory.AddListener(s.events)
521+ s.PatchValue(&lxc.TemplateLockDir, c.MkDir())
522+ s.PatchValue(&lxc.TemplateStopTimeout, 500*time.Millisecond)
523+}
524+
525+func (s *LxcSuite) TearDownTest(c *gc.C) {
526+ s.TestSuite.Factory.RemoveListener(s.events)
527+ close(s.events)
528+ s.TestSuite.TearDownTest(c)
529 }
530
531 func (s *LxcSuite) TestContainerDirFilesystem(c *gc.C) {
532@@ -73,9 +89,13 @@
533 }
534
535 func (s *LxcSuite) makeManager(c *gc.C, name string) container.Manager {
536- manager, err := lxc.NewContainerManager(container.ManagerConfig{
537+ params := container.ManagerConfig{
538 container.ConfigName: name,
539- })
540+ }
541+ if s.useClone {
542+ params["use-clone"] = "true"
543+ }
544+ manager, err := lxc.NewContainerManager(params)
545 c.Assert(err, gc.IsNil)
546 return manager
547 }
548@@ -86,7 +106,7 @@
549 "shazam": "Captain Marvel",
550 })
551 c.Assert(err, gc.IsNil)
552- c.Assert(c.GetTestLog(), jc.Contains, `WARNING juju.container.lxc Found unused config option with key: "shazam" and value: "Captain Marvel"`)
553+ c.Assert(c.GetTestLog(), jc.Contains, `WARNING juju.container unused config option: "shazam" -> "Captain Marvel"`)
554 }
555
556 func (s *LxcSuite) TestStartContainer(c *gc.C) {
557@@ -130,6 +150,93 @@
558 c.Assert(location, gc.Equals, expectedTarget)
559 }
560
561+func (s *LxcSuite) ensureTemplateStopped(name string) {
562+ go func() {
563+ for {
564+ template := s.Factory.New(name)
565+ if template.IsRunning() {
566+ template.Stop()
567+ }
568+ time.Sleep(50 * time.Millisecond)
569+ }
570+ }()
571+}
572+
573+func (s *LxcSuite) AssertEvent(c *gc.C, event mock.Event, expected mock.Action, id string) {
574+ c.Assert(event.Action, gc.Equals, expected)
575+ c.Assert(event.InstanceId, gc.Equals, id)
576+}
577+
578+func (s *LxcSuite) TestStartContainerEvents(c *gc.C) {
579+ manager := s.makeManager(c, "test")
580+ instance := containertesting.StartContainer(c, manager, "1")
581+ id := string(instance.Id())
582+ s.AssertEvent(c, <-s.events, mock.Created, id)
583+ s.AssertEvent(c, <-s.events, mock.Started, id)
584+}
585+
586+func (s *LxcSuite) TestStartContainerEventsWithClone(c *gc.C) {
587+ s.PatchValue(&s.useClone, true)
588+ // The template containers are created with an upstart job that
589+ // stops them once cloud init has finished. We emulate that here.
590+ template := "juju-series-template"
591+ s.ensureTemplateStopped(template)
592+ manager := s.makeManager(c, "test")
593+ instance := containertesting.StartContainer(c, manager, "1")
594+ id := string(instance.Id())
595+ s.AssertEvent(c, <-s.events, mock.Created, template)
596+ s.AssertEvent(c, <-s.events, mock.Started, template)
597+ s.AssertEvent(c, <-s.events, mock.Stopped, template)
598+ s.AssertEvent(c, <-s.events, mock.Cloned, template)
599+ s.AssertEvent(c, <-s.events, mock.Started, id)
600+}
601+
602+func (s *LxcSuite) createTemplate(c *gc.C) golxc.Container {
603+ name := "juju-series-template"
604+ s.ensureTemplateStopped(name)
605+ network := container.BridgeNetworkConfig("nic42")
606+ authorizedKeys := "authorized keys list"
607+ aptProxy := osenv.ProxySettings{}
608+ template, err := lxc.EnsureCloneTemplate(
609+ "ext4", "series", network, authorizedKeys, aptProxy)
610+ c.Assert(err, gc.IsNil)
611+ c.Assert(template.Name(), gc.Equals, name)
612+ s.AssertEvent(c, <-s.events, mock.Created, name)
613+ s.AssertEvent(c, <-s.events, mock.Started, name)
614+ s.AssertEvent(c, <-s.events, mock.Stopped, name)
615+
616+ autostartLink := lxc.RestartSymlink(name)
617+ config, err := ioutil.ReadFile(lxc.ContainerConfigFilename(name))
618+ c.Assert(err, gc.IsNil)
619+ expected := `
620+lxc.network.type = veth
621+lxc.network.link = nic42
622+lxc.network.flags = up
623+`
624+ // NOTE: no autostart, no mounting the log dir
625+ c.Assert(string(config), gc.Equals, expected)
626+ c.Assert(autostartLink, jc.DoesNotExist)
627+
628+ return template
629+}
630+
631+func (s *LxcSuite) TestStartContainerEventsWithCloneExistingTemplate(c *gc.C) {
632+ s.createTemplate(c)
633+ s.PatchValue(&s.useClone, true)
634+ manager := s.makeManager(c, "test")
635+ instance := containertesting.StartContainer(c, manager, "1")
636+ name := string(instance.Id())
637+ s.AssertEvent(c, <-s.events, mock.Cloned, "juju-series-template")
638+ s.AssertEvent(c, <-s.events, mock.Started, name)
639+
640+ autostartLink := lxc.RestartSymlink(name)
641+ config, err := ioutil.ReadFile(lxc.ContainerConfigFilename(name))
642+ c.Assert(err, gc.IsNil)
643+ mountLine := "lxc.mount.entry=/var/log/juju var/log/juju none defaults,bind 0 0"
644+ c.Assert(string(config), jc.Contains, mountLine)
645+ c.Assert(autostartLink, jc.IsSymlink)
646+}
647+
648 func (s *LxcSuite) TestContainerState(c *gc.C) {
649 manager := s.makeManager(c, "test")
650 c.Logf("%#v", manager)
651
652=== modified file 'container/lxc/mock/mock-lxc.go'
653--- container/lxc/mock/mock-lxc.go 2014-03-11 03:49:20 +0000
654+++ container/lxc/mock/mock-lxc.go 2014-03-13 02:11:05 +0000
655@@ -5,17 +5,23 @@
656
657 import (
658 "fmt"
659+ "io/ioutil"
660 "os"
661 "path/filepath"
662+ "sync"
663
664+ "github.com/juju/loggo"
665 "launchpad.net/golxc"
666
667+ "launchpad.net/juju-core/container"
668 "launchpad.net/juju-core/utils"
669 )
670
671 // This file provides a mock implementation of the golxc interfaces
672 // ContainerFactory and Container.
673
674+var logger = loggo.GetLogger("juju.container.lxc.mock")
675+
676 type Action int
677
678 const (
679@@ -23,6 +29,12 @@
680 Started Action = iota
681 // A container has been stopped.
682 Stopped
683+ // A container has been created.
684+ Created
685+ // A container has been destroyed.
686+ Destroyed
687+ // A container has been cloned.
688+ Cloned
689 )
690
691 func (action Action) String() string {
692@@ -31,6 +43,12 @@
693 return "Started"
694 case Stopped:
695 return "Stopped"
696+ case Created:
697+ return "Created"
698+ case Destroyed:
699+ return "Destroyed"
700+ case Cloned:
701+ return "Cloned"
702 }
703 return "unknown"
704 }
705@@ -51,6 +69,7 @@
706 containerDir string
707 instances map[string]golxc.Container
708 listeners []chan<- Event
709+ mutex sync.Mutex
710 }
711
712 func MockFactory(containerDir string) ContainerFactory {
713@@ -68,53 +87,85 @@
714 logLevel golxc.LogLevel
715 }
716
717+func (mock *mockContainer) getState() golxc.State {
718+ mock.factory.mutex.Lock()
719+ defer mock.factory.mutex.Unlock()
720+ return mock.state
721+}
722+
723+func (mock *mockContainer) setState(newState golxc.State) {
724+ mock.factory.mutex.Lock()
725+ defer mock.factory.mutex.Unlock()
726+ mock.state = newState
727+ logger.Debugf("container %q state change to %s", mock.name, string(newState))
728+}
729+
730 // Name returns the name of the container.
731 func (mock *mockContainer) Name() string {
732 return mock.name
733 }
734
735+func (mock *mockContainer) configFilename() string {
736+ return filepath.Join(mock.factory.containerDir, mock.name, "config")
737+}
738+
739 // Create creates a new container based on the given template.
740 func (mock *mockContainer) Create(configFile, template string, extraArgs []string, templateArgs []string) error {
741- if mock.state != golxc.StateUnknown {
742+ if mock.getState() != golxc.StateUnknown {
743 return fmt.Errorf("container is already created")
744 }
745- mock.state = golxc.StateStopped
746 mock.factory.instances[mock.name] = mock
747 // Create the container directory.
748 containerDir := filepath.Join(mock.factory.containerDir, mock.name)
749 if err := os.MkdirAll(containerDir, 0755); err != nil {
750 return err
751 }
752- containerConfig := filepath.Join(containerDir, "config")
753- return utils.CopyFile(containerConfig, configFile)
754+ if err := utils.CopyFile(mock.configFilename(), configFile); err != nil {
755+ return err
756+ }
757+ mock.setState(golxc.StateStopped)
758+ mock.factory.notify(Created, mock.name)
759+ return nil
760 }
761
762 // Start runs the container as a daemon.
763 func (mock *mockContainer) Start(configFile, consoleFile string) error {
764- if mock.state == golxc.StateUnknown {
765+ state := mock.getState()
766+ if state == golxc.StateUnknown {
767 return fmt.Errorf("container has not been created")
768- } else if mock.state == golxc.StateRunning {
769+ } else if state == golxc.StateRunning {
770 return fmt.Errorf("container is already running")
771 }
772- mock.state = golxc.StateRunning
773+ ioutil.WriteFile(
774+ filepath.Join(container.ContainerDir, mock.name, "console.log"),
775+ []byte("fake console.log"), 0644)
776+ mock.setState(golxc.StateRunning)
777 mock.factory.notify(Started, mock.name)
778 return nil
779 }
780
781 // Stop terminates the running container.
782 func (mock *mockContainer) Stop() error {
783- if mock.state == golxc.StateUnknown {
784+ state := mock.getState()
785+ if state == golxc.StateUnknown {
786 return fmt.Errorf("container has not been created")
787- } else if mock.state == golxc.StateStopped {
788+ } else if state == golxc.StateStopped {
789 return fmt.Errorf("container is already stopped")
790 }
791- mock.state = golxc.StateStopped
792+ mock.setState(golxc.StateStopped)
793 mock.factory.notify(Stopped, mock.name)
794 return nil
795 }
796
797 // Clone creates a copy of the container, giving the copy the specified name.
798 func (mock *mockContainer) Clone(name string, extraArgs []string, templateArgs []string) (golxc.Container, error) {
799+ state := mock.getState()
800+ if state == golxc.StateUnknown {
801+ return nil, fmt.Errorf("container has not been created")
802+ } else if state == golxc.StateRunning {
803+ return nil, fmt.Errorf("container is running, clone not possible")
804+ }
805+
806 container := &mockContainer{
807 factory: mock.factory,
808 name: name,
809@@ -122,6 +173,17 @@
810 logLevel: golxc.LogWarning,
811 }
812 mock.factory.instances[name] = container
813+
814+ // Create the container directory.
815+ containerDir := filepath.Join(mock.factory.containerDir, name)
816+ if err := os.MkdirAll(containerDir, 0755); err != nil {
817+ return nil, err
818+ }
819+ if err := utils.CopyFile(container.configFilename(), mock.configFilename()); err != nil {
820+ return nil, err
821+ }
822+
823+ mock.factory.notify(Cloned, mock.name)
824 return container, nil
825 }
826
827@@ -137,15 +199,17 @@
828
829 // Destroy stops and removes the container.
830 func (mock *mockContainer) Destroy() error {
831+ state := mock.getState()
832 // golxc destroy will stop the machine if it is running.
833- if mock.state == golxc.StateRunning {
834+ if state == golxc.StateRunning {
835 mock.Stop()
836 }
837- if mock.state == golxc.StateUnknown {
838+ if state == golxc.StateUnknown {
839 return fmt.Errorf("container has not been created")
840 }
841- mock.state = golxc.StateUnknown
842 delete(mock.factory.instances, mock.name)
843+ mock.setState(golxc.StateUnknown)
844+ mock.factory.notify(Destroyed, mock.name)
845 return nil
846 }
847
848@@ -157,27 +221,28 @@
849 // Info returns the status and the process id of the container.
850 func (mock *mockContainer) Info() (golxc.State, int, error) {
851 pid := -1
852- if mock.state == golxc.StateRunning {
853+ state := mock.getState()
854+ if state == golxc.StateRunning {
855 pid = 42
856 }
857- return mock.state, pid, nil
858+ return state, pid, nil
859 }
860
861 // IsConstructed checks if the container image exists.
862 func (mock *mockContainer) IsConstructed() bool {
863- return mock.state != golxc.StateUnknown
864+ return mock.getState() != golxc.StateUnknown
865 }
866
867 // IsRunning checks if the state of the container is 'RUNNING'.
868 func (mock *mockContainer) IsRunning() bool {
869- return mock.state == golxc.StateRunning
870+ return mock.getState() == golxc.StateRunning
871 }
872
873 // String returns information about the container, like the name, state,
874 // and process id.
875 func (mock *mockContainer) String() string {
876- _, pid, _ := mock.Info()
877- return fmt.Sprintf("<MockContainer %q, state: %s, pid %d>", mock.name, string(mock.state), pid)
878+ state, pid, _ := mock.Info()
879+ return fmt.Sprintf("<MockContainer %q, state: %s, pid %d>", mock.name, string(state), pid)
880 }
881
882 // LogFile returns the current filename used for the LogFile.
883@@ -202,6 +267,8 @@
884 }
885
886 func (mock *mockFactory) New(name string) golxc.Container {
887+ mock.mutex.Lock()
888+ defer mock.mutex.Unlock()
889 container, ok := mock.instances[name]
890 if ok {
891 return container
892@@ -212,6 +279,7 @@
893 state: golxc.StateUnknown,
894 logLevel: golxc.LogWarning,
895 }
896+ mock.instances[name] = container
897 return container
898 }
899
900
901=== modified file 'container/userdata.go'
902--- container/userdata.go 2014-03-12 02:28:30 +0000
903+++ container/userdata.go 2014-03-13 02:11:05 +0000
904@@ -9,7 +9,6 @@
905
906 "github.com/juju/loggo"
907
908- "launchpad.net/juju-core/agent"
909 coreCloudinit "launchpad.net/juju-core/cloudinit"
910 "launchpad.net/juju-core/environs/cloudinit"
911 )
912@@ -18,12 +17,21 @@
913 logger = loggo.GetLogger("juju.container")
914 )
915
916+// WriteUserData generates the cloud init for the specified machine config,
917+// and writes the serialized form out to a cloud-init file in the directory
918+// specified.
919 func WriteUserData(machineConfig *cloudinit.MachineConfig, directory string) (string, error) {
920 userData, err := cloudInitUserData(machineConfig)
921 if err != nil {
922 logger.Errorf("failed to create user data: %v", err)
923 return "", err
924 }
925+ return WriteCloudInitFile(directory, userData)
926+}
927+
928+// WriteCloudInitFile writes the data out to a cloud-init file in the
929+// directory specified, and returns the filename.
930+func WriteCloudInitFile(directory string, userData []byte) (string, error) {
931 userDataFilename := filepath.Join(directory, "cloud-init")
932 if err := ioutil.WriteFile(userDataFilename, userData, 0644); err != nil {
933 logger.Errorf("failed to write user data: %v", err)
934@@ -33,8 +41,6 @@
935 }
936
937 func cloudInitUserData(machineConfig *cloudinit.MachineConfig) ([]byte, error) {
938- // consider not having this line hardcoded...
939- machineConfig.DataDir = agent.DefaultDataDir
940 cloudConfig := coreCloudinit.New()
941 err := cloudinit.Configure(machineConfig, cloudConfig)
942 if err != nil {
943
944=== modified file 'provider/local/environ.go'
945--- provider/local/environ.go 2014-03-09 20:53:17 +0000
946+++ provider/local/environ.go 2014-03-13 02:11:05 +0000
947@@ -10,6 +10,7 @@
948 "os/exec"
949 "path/filepath"
950 "regexp"
951+ "strconv"
952 "strings"
953 "sync"
954 "syscall"
955@@ -219,6 +220,7 @@
956 container.ManagerConfig{
957 container.ConfigName: env.config.namespace(),
958 container.ConfigLogDir: env.config.logDir(),
959+ "use-clone": strconv.FormatBool(env.fastLXC),
960 })
961 if err != nil {
962 return err
963
964=== modified file 'worker/provisioner/lxc-broker_test.go'
965--- worker/provisioner/lxc-broker_test.go 2014-03-07 03:00:24 +0000
966+++ worker/provisioner/lxc-broker_test.go 2014-03-13 02:11:05 +0000
967@@ -204,6 +204,8 @@
968 func (s *lxcProvisionerSuite) expectStarted(c *gc.C, machine *state.Machine) string {
969 s.State.StartSync()
970 event := <-s.events
971+ c.Assert(event.Action, gc.Equals, mock.Created)
972+ event = <-s.events
973 c.Assert(event.Action, gc.Equals, mock.Started)
974 err := machine.Refresh()
975 c.Assert(err, gc.IsNil)
976@@ -215,6 +217,8 @@
977 s.State.StartSync()
978 event := <-s.events
979 c.Assert(event.Action, gc.Equals, mock.Stopped)
980+ event = <-s.events
981+ c.Assert(event.Action, gc.Equals, mock.Destroyed)
982 c.Assert(event.InstanceId, gc.Equals, instId)
983 }
984

Subscribers

People subscribed via source and target branches

to status/vote changes: