Merge lp:~themue/juju-core/021-deployer-lxc-context into lp:~juju/juju-core/trunk
- 021-deployer-lxc-context
- Merge into trunk
Status: | Work in progress |
---|---|
Proposed branch: | lp:~themue/juju-core/021-deployer-lxc-context |
Merge into: | lp:~juju/juju-core/trunk |
Diff against target: |
890 lines (+856/-0) 5 files modified
worker/deployer/export_test.go (+20/-0) worker/deployer/lxc/context.go (+355/-0) worker/deployer/lxc/export_test.go (+27/-0) worker/deployer/lxc/lxc.go (+213/-0) worker/deployer/lxc/lxc_test.go (+241/-0) |
To merge this branch: | bzr merge lp:~themue/juju-core/021-deployer-lxc-context |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+163237@code.launchpad.net |
Commit message
Description of the change
lxc: adding lxc context for local provider
The LxcContext is the second context beside the currently used
SimpleContext to be used by the Deployer to deploy and recall
units. It uses a small subset of the LXC commands by calling
the binaries/scripts. Currently the new context isn't used. This
has to be done by cmd/jujud/agent.go when creating a new Deployer
instance.
William Reade (fwereade) wrote : | # |
Frank Mueller (themue) wrote : | # |
Closed.
https:/
File worker/
https:/
worker/
On 2013/05/17 06:46:27, fwereade wrote:
> An environment does not have a series, other than default-series,
which is not
> relevant.
Renamed in defaultSeries, it's needed for the lxc container creation.
PyJuju gets this value by reading $JUJU_ENV, which we don't have so far.
But could also be a solution.
https:/
worker/
used by upstart on the local system.
On 2013/05/17 06:46:27, fwereade wrote:
> If LXC autoconf works, we don't need upstart on the local system. We
*do* need
> it relative to a container's root though.
Wrong comment, but removed it due to wrong concept.
https:/
worker/
On 2013/05/17 06:46:27, fwereade wrote:
> Why not const? We're completely in control here.
Done.
https:/
worker/
On 2013/05/17 06:46:27, fwereade wrote:
> Similarly, let's just make that const. No need to ever swap it out
that I can
> see.
Done.
https:/
worker/
On 2013/05/17 06:46:27, fwereade wrote:
> Hmm, I think I saw serge explaining that we can use whatever dir we
want. If we
> can, I'd really like us to just put them directly inside the deploying
agent's
> data dir... possible?
Seems to be possible by generating the directory in the container
configuration. Still one question left, will check.
https:/
worker/
On 2013/05/17 06:46:27, fwereade wrote:
> I don't think we should default this. We should always have the data
dir
> available, right?
Done.
https:/
worker/
ctx.createConta
On 2013/05/17 06:46:27, fwereade wrote:
> Shouldn't we trash it and try again? Failure partway through the first
attempt
> should not make a container undeployable on subsequent attempts...
Done.
https:/
worker/
On 2013/05/17 06:46:27, fwereade wrote:
> If we fail here, the unit'll keep running forever.
Done.
https:/
worker/
On 2013/05/17 06:46:27, fwereade wrote:
> Why are we using the host upstart dir? ISTM that this will cause us to
start
> containers that don't run age...
Unmerged revisions
- 972. By Frank Mueller
-
lxc: merged trunk
- 971. By Frank Mueller
-
lxc: refactored into own package; first network code
- 970. By Frank Mueller
-
lxc: improved context
- 969. By Frank Mueller
-
deployer: merged trunk and done review changes
- 968. By Frank Mueller
-
deployer/lxc: merged after review changes
- 967. By Frank Mueller
-
deployer/lxc: changes after reviews
- 966. By Frank Mueller
-
deployer: merged trunk and renamed manager to context
- 965. By Frank Mueller
-
deployer: added LXC manager
- 964. By Francesco Banconi
-
Add a CharmInfo API command.
Add a CharmInfo API command to get information
about a charm, given its URL. It is needed by the
Juju GUI, modeled upon the similar one in pyJuju.R=dimitern, rog
CC=
https://codereview. appspot. com/7444052 - 963. By Martin Packman
-
Correct url for openstack info on metadata service
The location on the metadata service we were using to retrieve the openstack
specific json file was misnamed and lacked the 'openstack' path component,
which was causing the legacy id to be used in all cases. That results in a
suicidal state server, which also hints we need to worry more about the
other cases when we may run into non-canonical server ids.R=dfc, jameinel
CC=
https://codereview. appspot. com/7527043
Preview Diff
1 | === modified file 'worker/deployer/export_test.go' |
2 | --- worker/deployer/export_test.go 2013-03-29 14:54:20 +0000 |
3 | +++ worker/deployer/export_test.go 2013-05-09 23:22:32 +0000 |
4 | @@ -1,5 +1,9 @@ |
5 | package deployer |
6 | |
7 | +import ( |
8 | + "launchpad.net/juju-core/version" |
9 | +) |
10 | + |
11 | type fakeAddresser struct{} |
12 | |
13 | func (*fakeAddresser) Addresses() []string { |
14 | @@ -17,3 +21,19 @@ |
15 | syslogConfigDir: syslogConfigDir, |
16 | } |
17 | } |
18 | + |
19 | +func NewTestLxcContext(deployerTag, initDir, hostDataDir, lxcContainerDir, lxcAutoDir string) *LxcContext { |
20 | + return &LxcContext{ |
21 | + environName: "lxc-test", |
22 | + environSeries: version.Current.Series, |
23 | + dataDir: "/var/lib/juju", |
24 | + hostDataDir: hostDataDir, |
25 | + addresser: &fakeAddresser{}, |
26 | + caCert: []byte("test-cert"), |
27 | + deployerTag: deployerTag, |
28 | + initDir: initDir, |
29 | + logDir: "/var/log/juju", |
30 | + lxcContainerDir: lxcContainerDir, |
31 | + lxcAutoDir: lxcAutoDir, |
32 | + } |
33 | +} |
34 | |
35 | === added directory 'worker/deployer/lxc' |
36 | === added file 'worker/deployer/lxc/context.go' |
37 | --- worker/deployer/lxc/context.go 1970-01-01 00:00:00 +0000 |
38 | +++ worker/deployer/lxc/context.go 2013-05-09 23:22:32 +0000 |
39 | @@ -0,0 +1,355 @@ |
40 | +package lxc |
41 | + |
42 | +import ( |
43 | + "fmt" |
44 | + "io" |
45 | + "io/ioutil" |
46 | + "os" |
47 | + "path/filepath" |
48 | + "regexp" |
49 | + "strings" |
50 | + |
51 | + "launchpad.net/juju-core/environs/agent" |
52 | + "launchpad.net/juju-core/log" |
53 | + "launchpad.net/juju-core/state" |
54 | + "launchpad.net/juju-core/upstart" |
55 | + "launchpad.net/juju-core/version" |
56 | + "launchpad.net/juju-core/worker/deployer" |
57 | +) |
58 | + |
59 | +// LxcContext stores details of the LXC environment on the local system. |
60 | +type LxcContext struct { |
61 | + // environName identifies the environment the context is running in. |
62 | + environName string |
63 | + |
64 | + // environSeries identifies the series of the environment. |
65 | + environSeries string |
66 | + |
67 | + // addresser is used to get the current state server addresses at the time |
68 | + // the given unit is deployed. |
69 | + addresser deployer.Addresser |
70 | + |
71 | + // caCert holds the CA certificate that will be used |
72 | + // to validate the state server's certificate, in PEM format. |
73 | + caCert []byte |
74 | + |
75 | + // deployerTag identifies the agent on whose behalf this context is running. |
76 | + deployerTag string |
77 | + |
78 | + // initDir specifies the directory used by upstart on the local system. |
79 | + // It is typically set to "/etc/init". |
80 | + initDir string |
81 | + |
82 | + // dataDir specifies the directory used by juju to store its state. It |
83 | + // is typically set to "/var/lib/juju". |
84 | + dataDir string |
85 | + |
86 | + // hostDataDir specifies the directory used by juju on the host system to |
87 | + // store its tools. It is typically set to "/var/lib/juju". |
88 | + hostDataDir string |
89 | + |
90 | + // logDir specifies the directory to which installed units will write |
91 | + // their log files. It is typically set to "/var/log/juju". |
92 | + logDir string |
93 | + |
94 | + // lxcContainerDir specifies the base directory for all containers. It |
95 | + // is typically set to "/var/lib/lxc". |
96 | + lxcContainerDir string |
97 | + |
98 | + // lxcAutoDir specifies the directory for the links to confguration files |
99 | + // needed for autostarting containers. It is typically set to "/etc/lxc/auto". |
100 | + lxcAutoDir string |
101 | +} |
102 | + |
103 | +var _ deployer.Context = (*LxcContext)(nil) |
104 | + |
105 | +// NewLxcContext returns a new LXC context, acting on behalf of the |
106 | +// entity specified in info, that deploys unit agents as upstart jobs in |
107 | +// "/etc/init" logging to "/var/log/juju". Paths to which agents and tools |
108 | +// are installed are relative to dataDir; if dataDir or hostDataDir are empty, |
109 | +// they will be set to "/var/lib/juju". |
110 | +func NewLxcContext(environName, environSeries, dataDir, hostDataDir string, caCert []byte, |
111 | + deployerTag string, addresser deployer.Addresser) *LxcContext { |
112 | + if dataDir == "" { |
113 | + dataDir = "/var/lib/juju" |
114 | + } |
115 | + if hostDataDir == "" { |
116 | + hostDataDir = "/var/lib/juju" |
117 | + } |
118 | + return &LxcContext{ |
119 | + environName: environName, |
120 | + environSeries: environSeries, |
121 | + dataDir: dataDir, |
122 | + hostDataDir: hostDataDir, |
123 | + addresser: addresser, |
124 | + caCert: caCert, |
125 | + deployerTag: deployerTag, |
126 | + initDir: "/etc/init", |
127 | + logDir: "/var/log/juju", |
128 | + lxcContainerDir: "/var/lib/lxc", |
129 | + lxcAutoDir: "/etc/lxc/auto", |
130 | + } |
131 | +} |
132 | + |
133 | +// DeployUnit creates a container for a unit and installs the required |
134 | +// upstart job in it. |
135 | +func (ctx *LxcContext) DeployUnit(unitName, initialPassword string) error { |
136 | + if ctx.configLinkExists(unitName) { |
137 | + return fmt.Errorf("unit %q is already deployed", unitName) |
138 | + } |
139 | + |
140 | + // Create container and initialize it. |
141 | + containerName := ctx.containerName(unitName) |
142 | + unitTag := state.UnitTag(unitName) |
143 | + if err := ctx.createContainer(containerName); err != nil { |
144 | + return err |
145 | + } |
146 | + if err := ctx.initContainer(containerName, unitName, unitTag, initialPassword); err != nil { |
147 | + if e := ctx.destroyContainer(containerName); e != nil { |
148 | + return fmt.Errorf("cannot destroy container after init error '%v': %v", err, e) |
149 | + } |
150 | + return err |
151 | + } |
152 | + if err := ctx.createConfigLink(unitName); err != nil { |
153 | + return fmt.Errorf("cannot create auto start link: %v", err) |
154 | + } |
155 | + return startContainer(containerName) |
156 | +} |
157 | + |
158 | +// RecallUnit destroys the container and removes the link in the |
159 | +// /etc/lxc/auto folder. |
160 | +func (ctx *LxcContext) RecallUnit(unitName string) error { |
161 | + if !ctx.configLinkExists(unitName) { |
162 | + return fmt.Errorf("unit %q is not deployed", unitName) |
163 | + } |
164 | + if err := ctx.destroyConfigLink(unitName); err != nil { |
165 | + return err |
166 | + } |
167 | + |
168 | + // Destroy container. |
169 | + containerName := ctx.containerName(unitName) |
170 | + return ctx.destroyContainer(containerName) |
171 | +} |
172 | + |
173 | +var autoRe = regexp.MustCompile("^juju-([a-z0-9-]+):([a-z0-9-]+):unit-([a-z0-9-]+)-([0-9]+)\\.conf$") |
174 | + |
175 | +// DeployedUnits returns which units are deployed right now. |
176 | +func (ctx *LxcContext) DeployedUnits() ([]string, error) { |
177 | + fis, err := ioutil.ReadDir(ctx.lxcAutoDir) |
178 | + if err != nil { |
179 | + return nil, err |
180 | + } |
181 | + var installed []string |
182 | + for _, fi := range fis { |
183 | + groups := autoRe.FindStringSubmatch(fi.Name()) |
184 | + if len(groups) == 5 { |
185 | + if groups[2] != ctx.deployerTag { |
186 | + continue |
187 | + } |
188 | + unitName := groups[3] + "/" + groups[4] |
189 | + if !state.IsUnitName(unitName) { |
190 | + continue |
191 | + } |
192 | + installed = append(installed, unitName) |
193 | + } |
194 | + } |
195 | + return installed, nil |
196 | +} |
197 | + |
198 | +// upstartService returns an upstart.Service corresponding to the specified |
199 | +// unit. Its name is badged according to the entity responsible for the |
200 | +// context, so as to distinguish its own jobs from those installed by other |
201 | +// means. |
202 | +func (ctx *LxcContext) upstartService(unitName string) *upstart.Service { |
203 | + unitTag := state.UnitTag(unitName) |
204 | + svcName := "jujud-" + ctx.deployerTag + ":" + unitTag |
205 | + svc := upstart.NewService(svcName) |
206 | + svc.InitDir = ctx.initDir |
207 | + return svc |
208 | +} |
209 | + |
210 | +// createContainer creates a new LXC container with the given name. |
211 | +func (ctx *LxcContext) createContainer(containerName string) error { |
212 | + if ctx.containerExists(containerName) { |
213 | + return fmt.Errorf("container %q is already created", containerName) |
214 | + } |
215 | + return createContainer(containerName, ctx.environSeries) |
216 | +} |
217 | + |
218 | +// initContainer creates the needed directories inside the container |
219 | +// and copies the tools. |
220 | +func (ctx *LxcContext) initContainer(containerName, unitName, unitTag, initialPassword string) error { |
221 | + hostToolsDir := agent.SharedToolsDir(ctx.hostDataDir, version.Current) |
222 | + toolsDir := ctx.containerDir(containerName, agent.ToolsDir(ctx.dataDir, unitTag)) |
223 | + err := os.MkdirAll(toolsDir, 0777) |
224 | + if err != nil { |
225 | + return err |
226 | + } |
227 | + defer removeOnErr(&err, toolsDir) |
228 | + |
229 | + agentsDir := ctx.containerDir(containerName, ctx.dataDir, "agents", unitTag) |
230 | + err = os.MkdirAll(agentsDir, 0777) |
231 | + if err != nil { |
232 | + return err |
233 | + } |
234 | + defer removeOnErr(&err, agentsDir) |
235 | + |
236 | + logDir := ctx.containerDir(containerName, ctx.logDir) |
237 | + err = os.MkdirAll(ctx.containerDir(containerName, ctx.logDir), 0777) |
238 | + if err != nil { |
239 | + return err |
240 | + } |
241 | + defer removeOnErr(&err, logDir) |
242 | + |
243 | + if err = copyTools(hostToolsDir, toolsDir); err != nil { |
244 | + return err |
245 | + } |
246 | + if _, err = agent.ChangeAgentTools(ctx.hostDataDir, unitTag, version.Current); err != nil { |
247 | + return err |
248 | + } |
249 | + |
250 | + // Prepare the agent's configuration data. |
251 | + info := state.Info{ |
252 | + Addrs: ctx.addresser.Addresses(), |
253 | + Tag: unitTag, |
254 | + CACert: ctx.caCert, |
255 | + } |
256 | + confDataDir := ctx.containerDir(containerName, ctx.dataDir) |
257 | + conf := &agent.Conf{ |
258 | + DataDir: confDataDir, |
259 | + OldPassword: initialPassword, |
260 | + StateInfo: &info, |
261 | + } |
262 | + conf.StateInfo.Tag = unitTag |
263 | + conf.StateInfo.Password = "" |
264 | + if err = conf.Write(); err != nil { |
265 | + return err |
266 | + } |
267 | + defer removeOnErr(&err, conf.Dir()) |
268 | + |
269 | + // Install an upstart job that runs the unit agent. |
270 | + jujudPath := filepath.Join(toolsDir, "jujud") |
271 | + logPath := filepath.Join(ctx.logDir, unitTag+".log") |
272 | + cmd := strings.Join([]string{ |
273 | + jujudPath, "unit", |
274 | + "--data-dir", ctx.dataDir, |
275 | + "--unit-name", unitName, |
276 | + "--debug", // TODO: propagate debug state sensibly |
277 | + }, " ") |
278 | + svc := ctx.upstartService(unitName) |
279 | + uconf := &upstart.Conf{ |
280 | + Service: *svc, |
281 | + Desc: "juju unit agent for " + unitName, |
282 | + Cmd: cmd, |
283 | + Out: logPath, |
284 | + } |
285 | + return uconf.Install() |
286 | +} |
287 | + |
288 | +// destroyContainer stops and removes the container. |
289 | +func (ctx *LxcContext) destroyContainer(containerName string) error { |
290 | + if !ctx.containerExists(containerName) { |
291 | + return fmt.Errorf("container %q does not exist", containerName) |
292 | + } |
293 | + if err := stopContainer(containerName); err != nil { |
294 | + return err |
295 | + } |
296 | + if err := waitContainerStopped(containerName); err != nil { |
297 | + return err |
298 | + } |
299 | + return destroyContainer(containerName) |
300 | +} |
301 | + |
302 | +// containerName returns a unique name for the container and its upstart |
303 | +// script based on the unit name. |
304 | +func (ctx *LxcContext) containerName(unitName string) string { |
305 | + unitTag := state.UnitTag(unitName) |
306 | + return "juju-" + ctx.environName + ":" + ctx.deployerTag + ":" + unitTag |
307 | +} |
308 | + |
309 | +// configLinkExists checks if the configuration link for an automatic start exists. |
310 | +func (ctx *LxcContext) configLinkExists(unitName string) bool { |
311 | + containerName := ctx.containerName(unitName) |
312 | + autoConfigPath := filepath.Join(ctx.lxcAutoDir, containerName+".conf") |
313 | + containerConfigPath := filepath.Join(ctx.lxcContainerDir, containerName, "config") |
314 | + targetPath, err := os.Readlink(autoConfigPath) |
315 | + return err == nil && targetPath == containerConfigPath |
316 | +} |
317 | + |
318 | +// createConfigLink links from a container config to the LXC auto-start folder. |
319 | +func (ctx *LxcContext) createConfigLink(unitName string) error { |
320 | + containerName := ctx.containerName(unitName) |
321 | + autoConfigPath := filepath.Join(ctx.lxcAutoDir, containerName+".conf") |
322 | + containerConfigPath := filepath.Join(ctx.lxcContainerDir, containerName, "config") |
323 | + return os.Symlink(containerConfigPath, autoConfigPath) |
324 | +} |
325 | + |
326 | +// destroyConfigLink removes the link from a container config to the LXC auto-start folder. |
327 | +func (ctx *LxcContext) destroyConfigLink(unitName string) error { |
328 | + containerName := ctx.containerName(unitName) |
329 | + autoConfigPath := filepath.Join(ctx.lxcAutoDir, containerName+".conf") |
330 | + return os.Remove(autoConfigPath) |
331 | +} |
332 | + |
333 | +// containerDir returns the full qualified name of a directory |
334 | +// inside the container. |
335 | +func (ctx *LxcContext) containerDir(containerName string, dirs ...string) string { |
336 | + rootfs := []string{ctx.lxcContainerDir, containerName, "rootfs"} |
337 | + return filepath.Join(append(rootfs, dirs...)...) |
338 | +} |
339 | + |
340 | +// containerExists checks if the container is created. |
341 | +func (ctx *LxcContext) containerExists(containerName string) bool { |
342 | + fi, err := os.Stat(ctx.containerDir(containerName)) |
343 | + if err != nil { |
344 | + if os.IsNotExist(err) { |
345 | + return false |
346 | + } |
347 | + panic(err) |
348 | + } |
349 | + return fi.IsDir() |
350 | +} |
351 | + |
352 | +// copyTools copies the hosts juju tools into the container. |
353 | +func copyTools(hostToolsDir, containerToolsDir string) error { |
354 | + hostLen := len(hostToolsDir) |
355 | + copyTool := func(hostPath string, info os.FileInfo, err error) error { |
356 | + if err != nil { |
357 | + return err |
358 | + } |
359 | + containerPath := filepath.Join(containerToolsDir, hostPath[hostLen:]) |
360 | + if info.IsDir() { |
361 | + // Create directory. |
362 | + if err := os.MkdirAll(containerPath, info.Mode().Perm()); err != nil { |
363 | + return err |
364 | + } |
365 | + } else { |
366 | + // Copy file. |
367 | + sourceFile, err := os.Open(hostPath) |
368 | + if err != nil { |
369 | + return err |
370 | + } |
371 | + defer sourceFile.Close() |
372 | + destFile, err := os.Create(containerPath) |
373 | + if err != nil { |
374 | + return err |
375 | + } |
376 | + defer destFile.Close() |
377 | + if _, err := io.Copy(destFile, sourceFile); err != nil { |
378 | + return err |
379 | + } |
380 | + return destFile.Chmod(info.Mode()) |
381 | + } |
382 | + return nil |
383 | + } |
384 | + return filepath.Walk(hostToolsDir, copyTool) |
385 | +} |
386 | + |
387 | +// removeOnErr removes a path in case of an error. |
388 | +func removeOnErr(err *error, path string) { |
389 | + if *err != nil { |
390 | + if e := os.Remove(path); e != nil { |
391 | + log.Warningf("installer: cannot remove %q: %v", path, e) |
392 | + } |
393 | + } |
394 | +} |
395 | |
396 | === added file 'worker/deployer/lxc/export_test.go' |
397 | --- worker/deployer/lxc/export_test.go 1970-01-01 00:00:00 +0000 |
398 | +++ worker/deployer/lxc/export_test.go 2013-05-09 23:22:32 +0000 |
399 | @@ -0,0 +1,27 @@ |
400 | +package lxc |
401 | + |
402 | +import ( |
403 | + "launchpad.net/juju-core/version" |
404 | +) |
405 | + |
406 | +type fakeAddresser struct{} |
407 | + |
408 | +func (*fakeAddresser) Addresses() []string { |
409 | + return []string{"s1:123", "s2:123"} |
410 | +} |
411 | + |
412 | +func NewTestLxcContext(deployerTag, initDir, hostDataDir, lxcContainerDir, lxcAutoDir string) *LxcContext { |
413 | + return &LxcContext{ |
414 | + environName: "lxc-test", |
415 | + environSeries: version.Current.Series, |
416 | + dataDir: "/var/lib/juju", |
417 | + hostDataDir: hostDataDir, |
418 | + addresser: &fakeAddresser{}, |
419 | + caCert: []byte("test-cert"), |
420 | + deployerTag: deployerTag, |
421 | + initDir: initDir, |
422 | + logDir: "/var/log/juju", |
423 | + lxcContainerDir: lxcContainerDir, |
424 | + lxcAutoDir: lxcAutoDir, |
425 | + } |
426 | +} |
427 | |
428 | === added file 'worker/deployer/lxc/lxc.go' |
429 | --- worker/deployer/lxc/lxc.go 1970-01-01 00:00:00 +0000 |
430 | +++ worker/deployer/lxc/lxc.go 2013-05-09 23:22:32 +0000 |
431 | @@ -0,0 +1,213 @@ |
432 | +package lxc |
433 | + |
434 | +import ( |
435 | + "fmt" |
436 | + "io/ioutil" |
437 | + "os/exec" |
438 | + "regexp" |
439 | + "strings" |
440 | +) |
441 | + |
442 | +const ( |
443 | + defaultAddr = "10.0.3.1" |
444 | + defaultBridge = "lxcbr0" |
445 | +) |
446 | + |
447 | +var ( |
448 | + confPath string = "/etc/default/lxc" |
449 | + addrRE *regexp.Regexp = regexp.MustCompile(`\n\s*LXC_ADDR="(\d+.\d+.\d+.\d+)"`) |
450 | + bridgeRE *regexp.Regexp = regexp.MustCompile(`\n\s*LXC_BRIDGE="(\w+)"`) |
451 | +) |
452 | + |
453 | +// Error reports the failure of a LXC command. |
454 | +type Error struct { |
455 | + Name string |
456 | + Err error |
457 | + Output []string |
458 | +} |
459 | + |
460 | +func (e Error) Error() string { |
461 | + if e.Output == nil { |
462 | + return fmt.Sprintf("error executing %q: %v", e.Name, e.Err) |
463 | + } |
464 | + if len(e.Output) == 1 { |
465 | + return fmt.Sprintf("error executing %q: %v", e.Name, e.Output[0]) |
466 | + } |
467 | + return fmt.Sprintf("error executing %q: %s", e.Name, strings.Join(e.Output, "; ")) |
468 | +} |
469 | + |
470 | +// createContainer creates a new ubuntu cloud container. |
471 | +func createContainer(containerName, series string) error { |
472 | + hostId := strings.Replace(containerName, ":", "-", -1) |
473 | + _, err := run("lxc-create", |
474 | + "-n", containerName, |
475 | + "-t", "ubuntu-cloud", |
476 | + "--", |
477 | + "-d", |
478 | + "-i", hostId, |
479 | + "-r", series) |
480 | + return err |
481 | +} |
482 | + |
483 | +// startContainer starts a container. |
484 | +func startContainer(containerName string) error { |
485 | + _, err := run("lxc-start", "-n", containerName) |
486 | + return err |
487 | +} |
488 | + |
489 | +// stopContainer stops a container. |
490 | +func stopContainer(containerName string) error { |
491 | + _, err := run("lxc-stop", "-n", containerName) |
492 | + return err |
493 | +} |
494 | + |
495 | +// waitContainerStopped waits until a container stopped. |
496 | +func waitContainerStopped(containerName string) error { |
497 | + _, err := run("lxc-wait", "-n", containerName, "-s", "STOPPED") |
498 | + return err |
499 | +} |
500 | + |
501 | +// destroyContainer destroys a container. |
502 | +func destroyContainer(containerName string) error { |
503 | + _, err := run("lxc-destroy", "-n", containerName) |
504 | + return err |
505 | +} |
506 | + |
507 | +// startNetwork starts the lxc network subsystem. |
508 | +func startNetwork() error { |
509 | + goal, _, err := networkStatus() |
510 | + if err != nil { |
511 | + return err |
512 | + } |
513 | + if goal != "start" { |
514 | + _, err := run("start", "lxc-net") |
515 | + if err != nil { |
516 | + return err |
517 | + } |
518 | + } |
519 | + return nil |
520 | +} |
521 | + |
522 | +// stopNetwork stops the lxc network subsystem. |
523 | +func stopNetwork() error { |
524 | + goal, _, err := networkStatus() |
525 | + if err != nil { |
526 | + return err |
527 | + } |
528 | + if goal != "stop" { |
529 | + _, err := run("stop", "lxc-net") |
530 | + if err != nil { |
531 | + return err |
532 | + } |
533 | + } |
534 | + return nil |
535 | +} |
536 | + |
537 | +// isNotworkRunning checks if the lxc network subsystem |
538 | +// is running. |
539 | +func isNetworkRunning() (bool, error) { |
540 | + _, status, err := networkStatus() |
541 | + if err != nil { |
542 | + return false, err |
543 | + } |
544 | + return status == "running", nil |
545 | +} |
546 | + |
547 | +// networkAttributes returns the lxc network attributes: |
548 | +// starting IP address and bridge name. |
549 | +func networkAttributes() (addr, bridge string, err error) { |
550 | + config, err := readConf() |
551 | + if err != nil { |
552 | + return "", "", err |
553 | + } |
554 | + addr = config["address"] |
555 | + if addr == "" { |
556 | + addr = defaultAddr |
557 | + } |
558 | + bridge = config["bridge"] |
559 | + if bridge == "" { |
560 | + bridge = defaultBridge |
561 | + } |
562 | + return addr, bridge, nil |
563 | +} |
564 | + |
565 | +// networkStatus returns the status of the lxc network subsystem. |
566 | +func networkStatus() (goal, status string, err error) { |
567 | + output, err := run("status", "lxc-net") |
568 | + if err != nil { |
569 | + return "", "", err |
570 | + } |
571 | + fields := strings.Fields(output) |
572 | + if len(fields) != 2 { |
573 | + return "", "", fmt.Errorf("unexpected status output: %q", output) |
574 | + } |
575 | + fields = strings.Split(fields[1], "/") |
576 | + if len(fields) != 2 { |
577 | + return "", "", fmt.Errorf("unexpected status output: %q", output) |
578 | + } |
579 | + return fields[0], fields[1], nil |
580 | +} |
581 | + |
582 | +// run executes the passed command and returns the out. |
583 | +func run(name string, args ...string) (string, error) { |
584 | + cmd := exec.Command(name, args...) |
585 | + // LXC tools do not use stdout and stderr in a predictable |
586 | + // way; based on experimentation, the most convenient |
587 | + // solution is to combine them and leave the client to |
588 | + // determine sanity as best it can. |
589 | + out, err := cmd.CombinedOutput() |
590 | + if err != nil { |
591 | + return "", runError(name, err, out) |
592 | + } |
593 | + return string(out), nil |
594 | +} |
595 | + |
596 | +// runError creates an error if run fails. |
597 | +func runError(name string, err error, out []byte) error { |
598 | + e := &Error{name, err, nil} |
599 | + for _, l := range strings.Split(string(out), "\n") { |
600 | + if strings.HasPrefix(l, name+": ") { |
601 | + // LXC tools do not always print their output with |
602 | + // the command name as prefix. The name is part of |
603 | + // the error struct, so stip it from the output if |
604 | + // printed. |
605 | + l = l[len(name)+2:] |
606 | + } |
607 | + if l != "" { |
608 | + e.Output = append(e.Output, l) |
609 | + } |
610 | + } |
611 | + return e |
612 | +} |
613 | + |
614 | +// keyValues retrieves key/value pairs out of a command out. |
615 | +func keyValues(raw string, sep string) map[string]string { |
616 | + kv := map[string]string{} |
617 | + lines := strings.Split(raw, "\n") |
618 | + for _, line := range lines { |
619 | + parts := strings.SplitN(line, sep, 2) |
620 | + if len(parts) == 2 { |
621 | + kv[parts[0]] = strings.TrimSpace(parts[1]) |
622 | + } |
623 | + } |
624 | + return kv |
625 | +} |
626 | + |
627 | +// readConf reads the LXC network address and bridge interface |
628 | +// out of the configuration file /etc/default/lxc. |
629 | +func readConf() (map[string]string, error) { |
630 | + conf := make(map[string]string) |
631 | + confData, err := ioutil.ReadFile(confPath) |
632 | + if err != nil { |
633 | + return nil, err |
634 | + } |
635 | + fetchValue := func(field string, re *regexp.Regexp) { |
636 | + groups := re.FindStringSubmatch(string(confData)) |
637 | + if len(groups) == 2 { |
638 | + conf[field] = groups[1] |
639 | + } |
640 | + } |
641 | + fetchValue("address", addrRE) |
642 | + fetchValue("bridge", bridgeRE) |
643 | + return conf, nil |
644 | +} |
645 | |
646 | === added file 'worker/deployer/lxc/lxc_test.go' |
647 | --- worker/deployer/lxc/lxc_test.go 1970-01-01 00:00:00 +0000 |
648 | +++ worker/deployer/lxc/lxc_test.go 2013-05-09 23:22:32 +0000 |
649 | @@ -0,0 +1,241 @@ |
650 | +package lxc_test |
651 | + |
652 | +import ( |
653 | + "bytes" |
654 | + "fmt" |
655 | + "io/ioutil" |
656 | + "os" |
657 | + "path/filepath" |
658 | + stdtesting "testing" |
659 | + "text/template" |
660 | + |
661 | + . "launchpad.net/gocheck" |
662 | + "launchpad.net/juju-core/environs/agent" |
663 | + "launchpad.net/juju-core/state" |
664 | + coretesting "launchpad.net/juju-core/testing" |
665 | + "launchpad.net/juju-core/version" |
666 | + "launchpad.net/juju-core/worker/deployer/lxc" |
667 | +) |
668 | + |
669 | +func TestPackage(t *stdtesting.T) { |
670 | + coretesting.MgoTestPackage(t) |
671 | +} |
672 | + |
673 | +type LxcContextSuite struct { |
674 | + LxcToolsFixture |
675 | +} |
676 | + |
677 | +var _ = Suite(&LxcContextSuite{}) |
678 | + |
679 | +func (s *LxcContextSuite) SetUpTest(c *C) { |
680 | + s.LxcToolsFixture.SetUp(c) |
681 | +} |
682 | + |
683 | +func (s *LxcContextSuite) TearDownTest(c *C) { |
684 | + s.LxcToolsFixture.TearDown(c) |
685 | +} |
686 | + |
687 | +func (s *LxcContextSuite) TestDeployRecall(c *C) { |
688 | + ctx0 := s.getContext(c, "test-entity-0") |
689 | + units, err := ctx0.DeployedUnits() |
690 | + c.Assert(err, IsNil) |
691 | + c.Assert(units, HasLen, 0) |
692 | + s.assertUpstartCount(c, 0) |
693 | + |
694 | + err = ctx0.DeployUnit("foo/123", "some-password") |
695 | + c.Assert(err, IsNil) |
696 | + units, err = ctx0.DeployedUnits() |
697 | + c.Assert(err, IsNil) |
698 | + c.Assert(units, DeepEquals, []string{"foo/123"}) |
699 | + s.assertUpstartCount(c, 1) |
700 | + s.checkUnitInstalled(c, "test-entity-0", "foo/123", "some-password") |
701 | + |
702 | + err = ctx0.RecallUnit("foo/123") |
703 | + c.Assert(err, IsNil) |
704 | + units, err = ctx0.DeployedUnits() |
705 | + c.Assert(err, IsNil) |
706 | + c.Assert(units, HasLen, 0) |
707 | + s.assertUpstartCount(c, 0) |
708 | + s.checkUnitRemoved(c, "test-entity-0", "foo/123") |
709 | +} |
710 | + |
711 | +func (s *LxcContextSuite) TestIndependentManagers(c *C) { |
712 | + ctx0 := s.getContext(c, "test-entity-0") |
713 | + err := ctx0.DeployUnit("foo/123", "some-password") |
714 | + c.Assert(err, IsNil) |
715 | + |
716 | + ctx1 := s.getContext(c, "test-entity-1") |
717 | + units, err := ctx1.DeployedUnits() |
718 | + c.Assert(err, IsNil) |
719 | + c.Assert(units, HasLen, 0) |
720 | + |
721 | + err = ctx1.RecallUnit("foo/123") |
722 | + c.Assert(err, ErrorMatches, `unit "foo/123" is not deployed`) |
723 | + s.checkUnitInstalled(c, "test-entity-0", "foo/123", "some-password") |
724 | + |
725 | + units, err = ctx0.DeployedUnits() |
726 | + c.Assert(err, IsNil) |
727 | + c.Assert(units, DeepEquals, []string{"foo/123"}) |
728 | + s.assertUpstartCount(c, 1) |
729 | + s.checkUnitInstalled(c, "test-entity-0", "foo/123", "some-password") |
730 | +} |
731 | + |
732 | +type LxcToolsFixture struct { |
733 | + origPath string |
734 | + hostDataDir string |
735 | + initDir string |
736 | + logDir string |
737 | + lxcContainerDir string |
738 | + lxcAutoDir string |
739 | + binDir string |
740 | +} |
741 | + |
742 | +var fakeJujud = "#!/bin/bash\n# fake-jujud\nexit 0\n" |
743 | + |
744 | +func (fix *LxcToolsFixture) SetUp(c *C) { |
745 | + // Create some needed temporary directory and manipulate the path |
746 | + // for the fake binaries. |
747 | + fix.initDir = c.MkDir() |
748 | + fix.logDir = c.MkDir() |
749 | + fix.hostDataDir = c.MkDir() |
750 | + fix.lxcContainerDir = c.MkDir() |
751 | + fix.lxcAutoDir = c.MkDir() |
752 | + fix.binDir = c.MkDir() |
753 | + fix.origPath = os.Getenv("PATH") |
754 | + os.Setenv("PATH", fix.binDir+":"+fix.origPath) |
755 | + // Create the faked tools and binaries. |
756 | + toolsDir := agent.SharedToolsDir(fix.hostDataDir, version.Current) |
757 | + err := os.MkdirAll(toolsDir, 0755) |
758 | + c.Assert(err, IsNil) |
759 | + jujudPath := filepath.Join(toolsDir, "jujud") |
760 | + err = ioutil.WriteFile(jujudPath, []byte(fakeJujud), 0755) |
761 | + c.Assert(err, IsNil) |
762 | + urlPath := filepath.Join(toolsDir, "downloaded-url.txt") |
763 | + err = ioutil.WriteFile(urlPath, []byte("http://example.com/tools"), 0644) |
764 | + c.Assert(err, IsNil) |
765 | + fix.makeLxcBins(c) |
766 | + fix.makeBin(c, "status", `echo "blah stop/waiting"`) |
767 | + fix.makeBin(c, "stopped-status", `echo "blah stop/waiting"`) |
768 | + fix.makeBin(c, "started-status", `echo "blah start/running, process 666"`) |
769 | + fix.makeBin(c, "start", "cp $(which started-status) $(which status)") |
770 | + fix.makeBin(c, "stop", "cp $(which stopped-status) $(which status)") |
771 | +} |
772 | + |
773 | +func (fix *LxcToolsFixture) TearDown(c *C) { |
774 | + os.Setenv("PATH", fix.origPath) |
775 | +} |
776 | + |
777 | +var LxcBins = []struct { |
778 | + Name string |
779 | + Code string |
780 | +}{ |
781 | + { |
782 | + Name: "lxc-create", |
783 | + Code: `#!/bin/bash |
784 | +# lxc-create |
785 | +mkdir -p {{.LxcContainerDir}}/$2/rootfs |
786 | +touch {{.LxcContainerDir}}/$2/config |
787 | +exit 0`, |
788 | + }, { |
789 | + Name: "lxc-start", |
790 | + Code: `#!/bin/bash |
791 | +# lxc-start |
792 | +exit 0`, |
793 | + }, { |
794 | + Name: "lxc-stop", |
795 | + Code: `#!/bin/bash |
796 | +# lxc-stop |
797 | +exit 0`, |
798 | + }, { |
799 | + Name: "lxc-wait", |
800 | + Code: `#!/bin/bash |
801 | +# lxc-wait |
802 | +exit 0`, |
803 | + }, { |
804 | + Name: "lxc-destroy", |
805 | + Code: `#!/bin/bash |
806 | +# lxc-destroy |
807 | +rm -R {{.LxcContainerDir}}/$2 |
808 | +exit 0`, |
809 | + }, |
810 | +} |
811 | + |
812 | +func (fix *LxcToolsFixture) makeLxcBins(c *C) { |
813 | + conf := struct { |
814 | + LxcContainerDir string |
815 | + LxcAutoDir string |
816 | + }{ |
817 | + LxcContainerDir: fix.lxcContainerDir, |
818 | + LxcAutoDir: fix.lxcAutoDir, |
819 | + } |
820 | + for _, lxcBin := range LxcBins { |
821 | + var buf bytes.Buffer |
822 | + binT := template.Must(template.New("").Parse(lxcBin.Code)) |
823 | + err := binT.Execute(&buf, conf) |
824 | + c.Assert(err, IsNil) |
825 | + binPath := filepath.Join(fix.binDir, lxcBin.Name) |
826 | + err = ioutil.WriteFile(binPath, buf.Bytes(), 0755) |
827 | + c.Assert(err, IsNil) |
828 | + } |
829 | +} |
830 | + |
831 | +func (fix *LxcToolsFixture) makeBin(c *C, name, script string) { |
832 | + path := filepath.Join(fix.binDir, name) |
833 | + err := ioutil.WriteFile(path, []byte("#!/bin/bash\n"+script), 0755) |
834 | + c.Assert(err, IsNil) |
835 | +} |
836 | + |
837 | +func (fix *LxcToolsFixture) assertUpstartCount(c *C, count int) { |
838 | + fis, err := ioutil.ReadDir(fix.lxcAutoDir) |
839 | + c.Assert(err, IsNil) |
840 | + c.Assert(fis, HasLen, count) |
841 | +} |
842 | + |
843 | +func (fix *LxcToolsFixture) getContext(c *C, deployerTag string) *lxc.LxcContext { |
844 | + return lxc.NewTestLxcContext(deployerTag, fix.initDir, fix.hostDataDir, fix.lxcContainerDir, fix.lxcAutoDir) |
845 | +} |
846 | + |
847 | +func (fix *LxcToolsFixture) checkUnitInstalled(c *C, dname, uname, password string) { |
848 | + unitTag := state.UnitTag(uname) |
849 | + lxcContainerName := "juju-lxc-test:" + dname + ":" + unitTag |
850 | + lxcContainerPath := filepath.Join(fix.lxcContainerDir, lxcContainerName) |
851 | + lxcConfName := fmt.Sprintf("%s.conf", lxcContainerName) |
852 | + lxcConfPath := filepath.Join(fix.lxcAutoDir, lxcConfName) |
853 | + |
854 | + _, err := os.Stat(lxcContainerPath) |
855 | + c.Assert(err, IsNil) |
856 | + _, err = os.Stat(lxcConfPath) |
857 | + c.Assert(err, IsNil) |
858 | + |
859 | + dataDir := filepath.Join(fix.lxcContainerDir, lxcContainerName, "rootfs/var/lib/juju") |
860 | + conf, err := agent.ReadConf(dataDir, unitTag) |
861 | + c.Assert(err, IsNil) |
862 | + c.Assert(conf, DeepEquals, &agent.Conf{ |
863 | + DataDir: dataDir, |
864 | + OldPassword: password, |
865 | + StateInfo: &state.Info{ |
866 | + Addrs: []string{"s1:123", "s2:123"}, |
867 | + CACert: []byte("test-cert"), |
868 | + Tag: unitTag, |
869 | + }, |
870 | + }) |
871 | + |
872 | + toolsDir := agent.ToolsDir(dataDir, unitTag) |
873 | + jujudPath := filepath.Join(toolsDir, "jujud") |
874 | + jujudData, err := ioutil.ReadFile(jujudPath) |
875 | + c.Assert(err, IsNil) |
876 | + c.Assert(string(jujudData), Equals, fakeJujud) |
877 | +} |
878 | + |
879 | +func (fix *LxcToolsFixture) checkUnitRemoved(c *C, ename, uname string) { |
880 | + unitTag := state.UnitTag(uname) |
881 | + lxcContainerName := "juju-lxc-test:" + ename + ":" + unitTag |
882 | + lxcContainerPath := filepath.Join(fix.lxcContainerDir, lxcContainerName) |
883 | + lxcConfName := fmt.Sprintf("%s.conf", lxcContainerName) |
884 | + lxcConfPath := filepath.Join(fix.lxcAutoDir, lxcConfName) |
885 | + |
886 | + _, err := os.Stat(lxcContainerPath) |
887 | + c.Assert(os.IsNotExist(err), Equals, true) |
888 | + _, err = os.Stat(lxcConfPath) |
889 | + c.Assert(os.IsNotExist(err), Equals, true) |
890 | +} |
NOT LGTM -- I would be prepared to let the unit-specific stuff slide,
but there's quite a lot to address here.
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go deployer/ lxc/context. go (right):
File worker/
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode26 deployer/ lxc/context. go:26: environSeries string
worker/
An environment does not have a series, other than default-series, which
is not relevant.
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode39 deployer/ lxc/context. go:39: // initDir specifies the directory
worker/
used by upstart on the local system.
If LXC autoconf works, we don't need upstart on the local system. We
*do* need it relative to a container's root though.
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode45 deployer/ lxc/context. go:45: dataDir string
worker/
Why not const? We're completely in control here.
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode53 deployer/ lxc/context. go:53: logDir string
worker/
Similarly, let's just make that const. No need to ever swap it out that
I can see.
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode57 deployer/ lxc/context. go:57: lxcContainerDir string
worker/
Hmm, I think I saw serge explaining that we can use whatever dir we
want. If we can, I'd really like us to just put them directly inside the
deploying agent's data dir... possible?
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode67 deployer/ lxc/context. go:67: // entity specified in info, that
worker/
deploys unit agents as upstart jobs in
FWIW, this can't be unit-specific any more. All containers are
"machines" and get their own machine agents.
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode77 deployer/ lxc/context. go:77: hostDataDir = "/var/lib/juju"
worker/
I don't think we should default this. We should always have the data dir
available, right?
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode104 deployer/ lxc/context. go:104: if err := iner(containerN ame); err != nil {
worker/
ctx.createConta
Shouldn't we trash it and try again? Failure partway through the first
attempt should not make a container undeployable on subsequent
attempts...
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode127 deployer/ lxc/context. go:127: }
worker/
If we fail here, the unit'll keep running forever.
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode146 deployer/ lxc/context. go:146: if groups[2] != ctx.deployerTag {
worker/
This is broken unless we also take into account environ name. But I'm
not sure we should care about environ names...
https:/ /codereview. appspot. com/9336045/ diff/1/ worker/ deployer/ lxc/context. go#newcode167 deployer/ lxc/context. go:167: svc.InitDir = ctx.initDir
worker/
Why are we using the host upstart dir? ISTM that this will cause us to
start containers that don't run agents, and start agents outside
containers.
https:/ /codereview. appspot. com/9336045/ diff/1/ work...