Merge lp:~thumper/juju-core/fast-lxc-no-apt-upgrade into lp:~go-bot/juju-core/trunk
- fast-lxc-no-apt-upgrade
- Merge into trunk
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 |
Related bugs: |
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.
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.
Tim Penhey (thumper) wrote : | # |
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:/
File container/
https:/
container/
Is there a helper function for str -> bool?
https:/
File container/
https:/
container/
event mock.Event, expected mock.Action, id string) {
does this need to be exported?
https:/
container/
name)
Should we be checking (some key aspects of) the contents of the
template?
Sidnei da Silva (sidnei) wrote : | # |
https:/
File container/
https:/
container/
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.
Tim Penhey (thumper) wrote : | # |
Please take a look.
https:/
File container/
https:/
container/
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:/
File container/
https:/
container/
On 2014/03/12 05:42:25, wallyworld wrote:
> Is there a helper function for str -> bool?
Yes there is. Using it now.
https:/
File container/
https:/
container/
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:/
container/
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?
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.
Preview Diff
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 |
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: /code.launchpad .net/~thumper/ juju-core/ update- golxc-version/ +merge/ 210492
https:/
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/74370044/
Affected files (+485, -46 lines): lxc/clonetempla te.go lxc/lxc. go lxc/lxc_ test.go lxc/mock/ mock-lxc. go userdata. go local/environ. go provisioner/ lxc-broker_ test.go
A [revision details]
A container/
M container/
M container/
M container/
M container/
M provider/
M worker/