Merge lp:~fginther/landscape-charm/remove-collect-logs into lp:~landscape/landscape-charm/tools

Proposed by Francis Ginther
Status: Merged
Approved by: Francis Ginther
Approved revision: 39
Merged at revision: 39
Proposed branch: lp:~fginther/landscape-charm/remove-collect-logs
Merge into: lp:~landscape/landscape-charm/tools
Diff against target: 1697 lines (+0/-1683)
3 files modified
Makefile (+0/-7)
collect-logs (+0/-613)
test_collect-logs.py (+0/-1063)
To merge this branch: bzr merge lp:~fginther/landscape-charm/remove-collect-logs
Reviewer Review Type Date Requested Status
Alberto Donato (community) Approve
🤖 Landscape Builder test results Needs Fixing
Review via email: mp+315731@code.launchpad.net

Commit message

Remove collect-logs, it's now at https://github.com/juju/autopilot-log-collector.

Description of the change

Remove collect-logs, it's now at https://github.com/juju/autopilot-log-collector.

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :
review: Needs Fixing (test results)
Revision history for this message
Alberto Donato (ack) wrote :

+1

you'll need to update the latch config not to vote on this anymore, since there's no makefile anymore

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== removed file 'Makefile'
--- Makefile 2016-09-16 15:31:13 +0000
+++ Makefile 1970-01-01 00:00:00 +0000
@@ -1,7 +0,0 @@
1.PHONY: test
2test:
3 python -m unittest test_collect-logs
4
5
6.PHONY: ci-test
7ci-test: test
80
=== removed file 'collect-logs'
--- collect-logs 2017-01-05 17:33:16 +0000
+++ collect-logs 1970-01-01 00:00:00 +0000
@@ -1,613 +0,0 @@
1#!/usr/bin/python
2
3from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
4import errno
5from functools import partial
6import logging
7import multiprocessing
8import os
9import shutil
10from subprocess import (
11 CalledProcessError, check_call, check_output, call, STDOUT)
12import sys
13from tempfile import mkdtemp
14
15import yaml
16
17
18log = logging.getLogger("collect-logs")
19PRG = os.path.abspath(__file__)
20LOGS = [
21 # Basics
22 "/var/log",
23 "/etc/hosts",
24 "/etc/network",
25 # for the landscape client
26 "/var/lib/landscape/client",
27 # for the landscape server charm
28 "/etc/apache2",
29 "/etc/haproxy",
30 # XXX This should be handled by the per-unit logs.
31 "/var/lib/lxc/*/rootfs/var/log",
32 # logs and configs for juju 1 using LXC (not LXD)
33 "/var/lib/juju/containers",
34 # for openstack
35 "/etc/nova",
36 "/etc/swift",
37 "/etc/neutron",
38 "/etc/ceph",
39 "/etc/glance",
40 ]
41EXCLUDED = ["/var/lib/landscape/client/package/hash-id",
42 "/var/lib/juju/containers/juju-*-lxc-template"]
43LANDSCAPE_JUJU_HOME = "/var/lib/landscape/juju-homes"
44
45JUJU1 = "juju"
46# XXX This is going to break once juju-2.1 happens.
47# See https://bugs.launchpad.net/juju-core/+bug/1613864.
48JUJU2 = "juju-2.1"
49JUJU = JUJU1
50
51DEFAULT_MODEL = object()
52
53VERBOSE = False
54
55
56if VERBOSE:
57 def call(args, env=None, _call=call):
58 print(" running {!r}".format(" ".join(args)))
59 return _call(args, env=env)
60
61 def check_call(args, env=None, _check_call=check_call):
62 print(" running {!r}".format(" ".join(args)))
63 return _check_call(args, env=env)
64
65 def check_output(args, stderr=None, env=None, _check_output=check_output):
66 print(" running {!r}".format(" ".join(args)))
67 return _check_output(args, stderr=stderr, env=env)
68
69
70class Juju(object):
71 """A wrapper around a juju binary."""
72
73 def __init__(self, binary_path=None, model=None, cfgdir=None, sudo=None):
74 if binary_path is None:
75 binary_path = JUJU
76 if model is DEFAULT_MODEL:
77 model = None
78
79 self.binary_path = binary_path
80 self.model = model
81 self.cfgdir = cfgdir
82 self.sudo = sudo
83
84 if binary_path == JUJU1:
85 self.envvar = "JUJU_HOME"
86 else:
87 self.envvar = "JUJU_DATA"
88
89 self.env = None
90 if cfgdir is not None:
91 self.env = dict(os.environ, **{self.envvar: cfgdir})
92
93 def __repr__(self):
94 args = ", ".join("{}={!r}".format(name, getattr(self, name))
95 for name in ("binary_path", "model", "cfgdir"))
96 return "{}({})".format(self.__class__.__name__, args)
97
98 def __eq__(self, other):
99 if self.binary_path != other.binary_path:
100 return False
101 if self.model != other.model:
102 return False
103 if self.cfgdir != other.cfgdir:
104 return False
105 if self.sudo != other.sudo:
106 return False
107 return True
108
109 def __ne__(self, other):
110 return not(self == other)
111
112 @property
113 def envstr(self):
114 if not self.cfgdir:
115 return ""
116 else:
117 return "{}={}".format(self.envvar, self.cfgdir)
118
119 def status_args(self):
120 """Return the subprocess.* args for a status command."""
121 args = self._resolve("status", "--format=yaml")
122 return args
123
124 def format_status(self):
125 """Return the formatted juju status command."""
126 args = self.status_args()
127 return self._format(args)
128
129 def set_model_config_args(self, key, value):
130 item = "{}={}".format(key, value)
131 if self.binary_path == JUJU1:
132 args = self._resolve("set-env", item)
133 else:
134 args = self._resolve("model-config", item)
135 return args
136
137 def format_set_model_config(self, key, value):
138 """Return the formatted model config command."""
139 args = self.set_model_config_args(key, value)
140 return self._format(args)
141
142 def ssh_args(self, unit, cmd):
143 """Return the subprocess.* args for an SSH command."""
144 return self._resolve("ssh", unit, cmd)
145
146 def pull_args(self, unit, source, target="."):
147 """Return the subprocess.* args for an SCP command."""
148 source = "{}:{}".format(unit, source)
149 return self._resolve("scp", source, target)
150
151 def push_args(self, unit, source, target):
152 """Return the subprocess.* args for an SCP command."""
153 target = "{}:{}".format(unit, target)
154 return self._resolve("scp", source, target)
155
156 def format(self, cmd, *subargs):
157 """Return the formatted command.
158
159 "sudo" and the juju config dir env var are set if appropriate.
160 """
161 args = [cmd]
162 args.extend(subargs)
163 return self._format(args)
164
165 def _format(self, args):
166 """Return the formatted args.
167
168 "sudo" and the juju config dir env var are set if appropriate.
169 """
170 if self.cfgdir:
171 args.insert(0, self.envstr)
172 if self.sudo == "" or self.sudo is True:
173 args.insert(0, "sudo")
174 elif self.sudo:
175 args.insert(0, "sudo -u {}".format(self.sudo))
176 return " ".join(args)
177
178 def _resolve(self, sub, *subargs):
179 """Return the subprocess.* args for the juju subcommand."""
180 args = [self.binary_path, sub]
181 if self.model:
182 if self.binary_path == JUJU1:
183 args.append("-e")
184 else:
185 args.append("-m")
186 args.append(self.model)
187 args.extend(subargs)
188 return args
189
190
191def format_collect_logs(juju, script, target, inner=True):
192 """Return the formatted command for the collect_logs script."""
193 if inner:
194 args = [script, "--inner"]
195 else:
196 args = [script]
197 args.extend(["--juju", juju.binary_path])
198 if juju.model and juju.model is not DEFAULT_MODEL:
199 args.extend(["--model", juju.model])
200 if juju.cfgdir:
201 args.extend(["--cfgdir", juju.cfgdir])
202 args.append(target)
203 return juju._format(args)
204
205
206def juju_status(juju):
207 """Return a juju status structure."""
208 output = check_output(juju.status_args(), env=juju.env)
209 output = output.decode("utf-8").strip()
210 return yaml.load(output)
211
212
213def get_units(juju, status=None):
214 """Return a list with all units."""
215 if status is None:
216 status = juju_status(juju)
217 units = []
218 if "services" in status:
219 applications = status["services"]
220 else:
221 applications = status["applications"]
222 for application in applications:
223 # skip subordinate charms
224 if "subordinate-to" in applications[application].keys():
225 continue
226 if "units" in applications[application]:
227 units.extend(applications[application]["units"].keys())
228 if len(units) == 0:
229 sys.exit("ERROR, no units found. Make sure the right juju environment"
230 "is set.")
231 return units
232
233
234def _create_ps_output_file(juju, unit):
235 """List running processes and redirect them to a file."""
236 log.info("Collecting ps output on unit {}".format(unit))
237 ps_cmd = "ps fauxww | sudo tee /var/log/ps-fauxww.txt"
238 args = juju.ssh_args(unit, ps_cmd)
239 try:
240 check_output(args, stderr=STDOUT, env=juju.env)
241 except CalledProcessError as e:
242 log.warning(
243 "Failed to collect running processes on unit {}".format(unit))
244 log.warning(e.output)
245 log.warning(e.returncode)
246
247
248def _create_log_tarball(juju, unit):
249 log.info("Creating tarball on unit {}".format(unit))
250 exclude = " ".join(["--exclude=%s" % x for x in EXCLUDED])
251 logs = "$(sudo sh -c \"ls -1d %s 2>/dev/null\")" % " ".join(LOGS)
252 # --ignore-failed-read avoids failure for unreadable files (not for files
253 # being written)
254 tar_cmd = "sudo tar --ignore-failed-read"
255 logsuffix = unit.replace("/", "-")
256 if unit == "0":
257 logsuffix = "bootstrap"
258 cmd = "{} {} -cf /tmp/logs_{}.tar {}".format(
259 tar_cmd, exclude, logsuffix, logs)
260 args = juju.ssh_args(unit, cmd)
261 ATTEMPTS = 5
262 for i in range(ATTEMPTS):
263 log.info("...attempt {} of {}".format(i+1, ATTEMPTS))
264 try:
265 check_output(args, stderr=STDOUT, env=juju.env)
266 except CalledProcessError as e:
267 # Note: tar command returns 1 for everything it considers a
268 # warning, 2 for fatal errors. Since we are backing up
269 # log files that are actively being written, or part of a live
270 # system, logging and ignoring such warnings (return code 1) is
271 # what we can do now.
272 # Everything else we might retry as usual.
273 if e.returncode == 1:
274 log.warning(
275 "tar returned 1, proceeding anyway: {}".format(e.output))
276 break
277 log.warning(
278 "Failed to archive log files on unit {}".format(unit))
279 log.warning(e.output)
280 log.warning(e.returncode)
281 if i < 4:
282 log.warning("...retrying...")
283 cmd = "{} {} --update -f /tmp/logs_{}.tar {}".format(
284 tar_cmd, exclude, logsuffix, logs)
285 args = juju.ssh_args(unit, cmd)
286 else:
287 # The command succeeded so we stop the retry loop.
288 break
289 else:
290 # Don't bother compressing.
291 log.warning("...{} attempts failed; giving up".format(ATTEMPTS))
292 return
293 cmd = "sudo gzip -f /tmp/logs_{}.tar".format(logsuffix)
294 args = juju.ssh_args(unit, cmd)
295 try:
296 check_output(args, stderr=STDOUT, env=juju.env)
297 except CalledProcessError as e:
298 log.warning(
299 "Failed to create remote log tarball on unit {}".format(unit))
300 log.warning(e.output)
301 log.warning(e.returncode)
302
303
304def download_log_from_unit(juju, unit):
305 log.info("Downloading tarball from unit %s" % unit)
306 unit_filename = unit.replace("/", "-")
307 if unit == "0":
308 unit_filename = "bootstrap"
309 remote_filename = "logs_%s.tar.gz" % unit_filename
310 try:
311 args = juju.pull_args(unit, "/tmp/" + remote_filename)
312 call(args, env=juju.env)
313 os.mkdir(unit_filename)
314 args = ["tar", "-C", unit_filename, "-xzf", remote_filename]
315 call(args)
316 os.unlink(remote_filename)
317 except:
318 log.warning("error collecting logs from %s, skipping" % unit)
319 finally:
320 if os.path.exists(remote_filename):
321 os.unlink(remote_filename)
322
323
324def collect_logs(juju):
325 """
326 Remotely, on each unit, create a tarball with the requested log files
327 or directories, if they exist. If a requested log does not exist on a
328 particular unit, it's ignored.
329 After each tarball is created, it's downloaded to the current directory
330 and expanded, and the tarball is then deleted.
331 """
332 units = get_units(juju)
333 # include bootstrap
334 units.append("0")
335
336 log.info("Collecting running processes for all units including bootstrap")
337 map(partial(_create_ps_output_file, juju), units)
338
339 log.info("Creating remote tarball in parallel for units %s" % (
340 ",".join(units)))
341 map(partial(_create_log_tarball, juju), units)
342 log.info("Downloading logs from units")
343
344 _mp_map(partial(download_log_from_unit, juju), units)
345
346
347def _mp_map(func, args):
348 pool = multiprocessing.Pool(processes=4)
349 pool.map(func, args)
350
351
352def get_landscape_unit(units):
353 """Return the landscape unit among the units list."""
354 units = [
355 unit for unit in units if unit.startswith("landscape-server/") or
356 unit.startswith("landscape/")]
357 if len(units) == 0:
358 return None
359 else:
360 # XXX we don't yet support multiple landscape units. We would have to
361 # find out which one has the juju home
362 return units[0]
363
364
365def get_inner_model(version, inner_model=DEFAULT_MODEL):
366 """Return a best-effort guess at the inner model name."""
367 if inner_model is not DEFAULT_MODEL:
368 return inner_model
369 if version == JUJU1:
370 return None
371 # We assume that this is a Landscape-bootstrapped controller.
372 return "controller"
373
374
375def disable_inner_ssh_proxy(juju, landscape_unit, inner_model=DEFAULT_MODEL):
376 """
377 Workaround for #1607076: disable the proxy-ssh juju environment setting
378 for the inner cloud so we can juju ssh into it.
379 """
380 log.info("Disabling proxy-ssh in the juju environment on "
381 "{}".format(landscape_unit))
382
383 cfgdir = "{0}/`sudo ls -rt {0}/ | tail -1`".format(LANDSCAPE_JUJU_HOME)
384 key = "proxy-ssh"
385 value = "false"
386
387 # Try Juju 2.
388 model2 = get_inner_model(JUJU2, inner_model)
389 inner2 = Juju(JUJU2, model=model2, cfgdir=cfgdir, sudo=True)
390 cmd2 = inner2.format_set_model_config(key, value)
391 args2 = juju.ssh_args(landscape_unit, cmd2)
392 try:
393 check_output(args2, stderr=STDOUT, env=juju.env)
394 except CalledProcessError as e:
395 log.warning("Couldn't disable proxy-ssh in the inner environment "
396 "using Juju 2, attempting Juju 1.")
397 log.warning("Error was:\n{}".format(e.output))
398 else:
399 return
400
401 # Try Juju 1.
402 model1 = get_inner_model(JUJU1, inner_model)
403 inner1 = Juju(JUJU1, model=model1, cfgdir=cfgdir, sudo=True)
404 cmd1 = inner1.format_set_model_config(key, value)
405 args1 = juju.ssh_args(landscape_unit, cmd1)
406 try:
407 check_output(args1, stderr=STDOUT, env=juju.env)
408 except CalledProcessError as e:
409 log.warning("Couldn't disable proxy-ssh in the inner environment "
410 "using Juju 1, collecting inner logs might fail.")
411 log.warning("Error was:\n{}".format(e.output))
412
413
414
415def find_inner_juju(juju, landscape_unit, inner_model=DEFAULT_MODEL):
416 """Return the juju dir and binary path, if any."""
417 # Identify the most recent juju "home" that landscape is using.
418 cmd = "sudo ls -rt {}/".format(LANDSCAPE_JUJU_HOME)
419 args = juju.ssh_args(landscape_unit, cmd)
420 try:
421 output = check_output(args, env=juju.env).strip()
422 except CalledProcessError:
423 return None
424 if output.startswith("sudo: "):
425 _, _, output = output.partition("\r\n")
426 indices = [m for m in output.split() if m and m.isdigit()]
427 if not indices:
428 return None
429 index = indices[-1]
430 juju_dir = os.path.join(LANDSCAPE_JUJU_HOME, index)
431
432 # Try Juju 2.
433 model2 = get_inner_model(JUJU2, inner_model)
434 inner2 = Juju(JUJU2, model=model2, cfgdir=juju_dir, sudo="")
435 cmd2 = inner2.format_status()
436 args2 = juju.ssh_args(landscape_unit, cmd2)
437 if call(args2, env=juju.env) == 0:
438 log.info("using Juju 2 for inner model")
439 return inner2
440
441 # Try Juju 1.
442 model1 = get_inner_model(JUJU1, inner_model)
443 inner1 = Juju(JUJU1, model=model1, cfgdir=juju_dir, sudo="landscape")
444 cmd1 = inner1.format_status()
445 args1 = juju.ssh_args(landscape_unit, cmd1)
446 if call(args1, env=juju.env) == 0:
447 log.info("using Juju 1 for inner model")
448 return inner1
449
450 # We didn't find an inner model.
451 return None
452
453
454def collect_inner_logs(juju, inner_model=DEFAULT_MODEL):
455 """Collect logs from an inner landscape[-server]/0 unit."""
456 log.info("Collecting logs on inner environment")
457 units = get_units(juju)
458 landscape_unit = get_landscape_unit(units)
459 if not landscape_unit:
460 log.info("No landscape[-server]/N found, skipping")
461 return
462 log.info("Found landscape unit {}".format(landscape_unit))
463
464 disable_inner_ssh_proxy(juju, landscape_unit, inner_model)
465
466 # Look up the inner model.
467 inner_juju = find_inner_juju(juju, landscape_unit, inner_model)
468 if inner_juju is None:
469 log.info(("No active inner environment found on {}, skipping"
470 ).format(landscape_unit))
471 return
472
473 # Prepare to get the logs from the inner model.
474 collect_logs = "/tmp/collect-logs"
475 args = juju.push_args(landscape_unit, PRG, collect_logs)
476 call(args, env=juju.env)
477 filename = "inner-logs.tar.gz"
478 inner_filename = os.path.join("/tmp", filename)
479 args = juju.ssh_args(landscape_unit, "sudo rm -rf " + inner_filename)
480 call(args, env=juju.env)
481
482 # Collect the logs for the inner model.
483 cmd = format_collect_logs(inner_juju, collect_logs, inner_filename)
484 args = juju.ssh_args(landscape_unit, cmd)
485 check_call(args, env=juju.env)
486
487 # Copy the inner logs into a local directory.
488 log.info("Copying inner environment back")
489 cwd = os.getcwd()
490 target = os.path.join(cwd, filename)
491 args = juju.pull_args(landscape_unit, inner_filename, target)
492 check_call(args, env=juju.env)
493 try:
494 inner_dir = "landscape-0-inner-logs"
495 os.mkdir(inner_dir)
496 os.chdir(inner_dir)
497 try:
498 check_call(["tar", "-zxf", os.path.join(cwd, filename)])
499 finally:
500 os.chdir(cwd)
501 finally:
502 try:
503 os.remove(target)
504 except OSError as e:
505 if e.errno != errno.ENOENT:
506 log.warning(
507 "failed to remove inner logs tarball: {}".format(e))
508
509
510def bundle_logs(tmpdir, tarfile, extrafiles=[]):
511 """
512 Create a tarball with the directories under tmpdir and the
513 specified extra files. The tar command is executed outside
514 of tmpdir, so we need to a) use an absolute path that
515 includes tmpdir; and b) strip the tmpdir so it's not part of
516 the tarball.
517 We don't want absolute paths for extra files because otherwise
518 this path would be inside the tarball.
519
520 This allows you to have a tarball where this:
521 /tmp/tmpdir/foo
522 /tmp/tmpdir/bar
523 /home/ubuntu/data/log
524
525 Becomes this inside the tarball:
526 foo
527 bar
528 data/log
529
530 If collect-logs is run with CWD=/home/ubuntu and given data/log as
531 the extra file.
532 """
533 args = ["tar", "czf", tarfile]
534 # get rid of the tmpdir prefix
535 args.extend(["--transform", "s,{}/,,".format(tmpdir[1:])])
536 # need absolute paths since tmpdir isn't the cwd
537 args.extend(os.path.join(tmpdir, d) for d in sorted(os.listdir(tmpdir)))
538 if extrafiles:
539 args.extend(extrafiles)
540 call(args)
541
542
543def get_juju(binary_path, model=DEFAULT_MODEL, cfgdir=None, inner=False):
544 """Return a Juju for the provided info."""
545 if model is DEFAULT_MODEL and inner and binary_path != JUJU1:
546 # We assume that this is a Landscape-bootstrapped controller.
547 model = "controller"
548 return Juju(binary_path, model=model, cfgdir=cfgdir)
549
550
551def get_option_parser():
552 description = ("Collect logs from current juju environment and, if an "
553 "inner autopilot cloud is detected, include that.")
554 parser = ArgumentParser(description=description,
555 formatter_class=ArgumentDefaultsHelpFormatter)
556 parser.add_argument("--inner", action="store_true", default=False,
557 help="Collect logs for an inner model.")
558 parser.add_argument("--juju", default=JUJU2,
559 help="The Juju binary to use.")
560 parser.add_argument("--model", default=DEFAULT_MODEL,
561 help="The Juju model to use.")
562 parser.add_argument("--inner-model", default=DEFAULT_MODEL,
563 help="The Juju model to use for the inner juju.")
564 parser.add_argument("--cfgdir",
565 help="The Juju config dir to use.")
566 parser.add_argument("tarfile", help="Full path to tarfile to create.")
567 parser.add_argument("extrafiles", help="Optional full path to extra "
568 "logfiles to include, space separated", nargs="*")
569 return parser
570
571
572def main(tarfile, extrafiles, juju=None, inner_model=DEFAULT_MODEL,
573 inner=False):
574 if juju is None:
575 juju = Juju()
576
577 # we need the absolute path because we will be changing
578 # the cwd
579 tmpdir = mkdtemp()
580 cwd = os.getcwd()
581 # logs are collected inside a temporary directory
582 os.chdir(tmpdir)
583 try:
584 collect_logs(juju)
585 if not inner:
586 try:
587 collect_inner_logs(juju, inner_model)
588 except:
589 log.warning("Collecting inner logs failed, continuing")
590 # we create the final tarball outside of tmpdir to we can
591 # add the extrafiles to the tarball root
592 os.chdir(cwd)
593 bundle_logs(tmpdir, tarfile, extrafiles)
594 log.info("created: %s" % tarfile)
595 finally:
596 call(["chmod", "-R", "u+w", tmpdir])
597 shutil.rmtree(tmpdir)
598
599
600if __name__ == "__main__":
601 logging.basicConfig(
602 level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
603 parser = get_option_parser()
604 args = parser.parse_args(sys.argv[1:])
605 tarfile = os.path.abspath(args.tarfile)
606 juju = get_juju(args.juju, args.model, args.cfgdir, args.inner)
607 if args.inner:
608 log.info("# start inner ##############################")
609 try:
610 main(tarfile, args.extrafiles, juju, args.inner_model, args.inner)
611 finally:
612 if args.inner:
613 log.info("# end inner ################################")
6140
=== removed file 'test_collect-logs.py'
--- test_collect-logs.py 2017-01-05 17:33:16 +0000
+++ test_collect-logs.py 1970-01-01 00:00:00 +0000
@@ -1,1063 +0,0 @@
1# Copyright 2016 Canonical Limited. All rights reserved.
2
3# To run: "python -m unittest test_collect-logs"
4
5import errno
6import os
7import os.path
8import shutil
9import subprocess
10import sys
11import tempfile
12from unittest import TestCase
13
14import mock
15
16
17__file__ = os.path.abspath(__file__)
18
19script = type(sys)("collect-logs")
20script.__file__ = os.path.abspath("collect-logs")
21execfile("collect-logs", script.__dict__)
22
23
24class FakeError(Exception):
25 """A specific error for which to check."""
26
27
28def _create_file(filename, data=None):
29 """Create (or re-create) the identified file.
30
31 If data is provided, it is written to the file. Otherwise it
32 will be empty.
33
34 The file's directory is created if necessary.
35 """
36 dirname = os.path.dirname(os.path.abspath(filename))
37 try:
38 os.makedirs(dirname)
39 except OSError as e:
40 if e.errno != errno.EEXIST:
41 raise
42
43 with open(filename, "w") as file:
44 if data:
45 file.write()
46
47
48class _BaseTestCase(TestCase):
49
50 MOCKED = None
51
52 def setUp(self):
53 super(_BaseTestCase, self).setUp()
54
55 self.orig_cwd = os.getcwd()
56 self.cwd = tempfile.mkdtemp()
57 os.chdir(self.cwd)
58
59 self.tempdir = os.path.join(self.cwd, "tempdir")
60 os.mkdir(self.tempdir)
61
62 self.orig = {}
63 for attr in self.MOCKED or ():
64 self.orig[attr] = getattr(script, attr)
65 setattr(script, attr, mock.Mock())
66
67 self.juju = script.Juju()
68
69 def tearDown(self):
70 for attr in self.MOCKED or ():
71 setattr(script, attr, self.orig[attr])
72
73 shutil.rmtree(self.cwd)
74 os.chdir(self.orig_cwd)
75
76 super(_BaseTestCase, self).tearDown()
77
78 def _create_tempfile(self, filename, data=None):
79 """Create a file at the identified path, but rooted at the temp dir."""
80 _create_file(os.path.join(self.tempdir, filename), data)
81
82 def assert_cwd(self, dirname):
83 """Ensure that the CWD matches the given directory."""
84 cwd = os.getcwd()
85 self.assertEqual(cwd, dirname)
86
87
88class GetJujuTests(TestCase):
89
90 def test_juju1_outer(self):
91 """
92 get_juju() returns a Juju prepped for a Juju 1 outer model.
93 """
94 juju = script.get_juju(script.JUJU1, inner=False)
95
96 expected = script.Juju("juju", model=None)
97 self.assertEqual(juju, expected)
98
99 def test_juju1_inner(self):
100 """
101 get_juju() returns a Juju prepped for a Juju 1 inner model.
102 """
103 cfgdir = "/var/lib/landscape/juju-homes/0"
104
105 juju = script.get_juju(script.JUJU1, model=None, cfgdir=cfgdir,
106 inner=True)
107
108 expected = script.Juju("juju", cfgdir=cfgdir)
109 self.assertEqual(juju, expected)
110
111 def test_juju2_outer(self):
112 """
113 get_juju() returns a Juju prepped for a Juju 2 outer model.
114 """
115 juju = script.get_juju(script.JUJU2, inner=False)
116
117 expected = script.Juju("juju-2.1", model=None)
118 self.assertEqual(juju, expected)
119
120 def test_juju2_inner(self):
121 """
122 get_juju() returns a Juju prepped for a Juju 2 inner model.
123 """
124 cfgdir = "/var/lib/landscape/juju-homes/0"
125
126 juju = script.get_juju(script.JUJU2, cfgdir=cfgdir, inner=True)
127
128 expected = script.Juju("juju-2.1", model="controller", cfgdir=cfgdir)
129 self.assertEqual(juju, expected)
130
131
132class MainTestCase(_BaseTestCase):
133
134 MOCKED = ("collect_logs", "collect_inner_logs", "bundle_logs")
135
136 def setUp(self):
137 super(MainTestCase, self).setUp()
138
139 self.orig_mkdtemp = script.mkdtemp
140 script.mkdtemp = lambda: self.tempdir
141
142 def tearDown(self):
143 script.mkdtemp = self.orig_mkdtemp
144
145 super(MainTestCase, self).tearDown()
146
147 def test_success(self):
148 """
149 main() calls collect_logs(), collect_inner_logs(), and bundle_logs().
150 """
151 tarfile = "/tmp/logs.tgz"
152 extrafiles = ["spam.py"]
153
154 script.main(tarfile, extrafiles, juju=self.juju)
155
156 script.collect_logs.assert_called_once_with(self.juju)
157 script.collect_inner_logs.assert_called_once_with(
158 self.juju, script.DEFAULT_MODEL)
159 script.bundle_logs.assert_called_once_with(
160 self.tempdir, tarfile, extrafiles)
161 self.assertFalse(os.path.exists(self.tempdir))
162
163 def test_in_correct_directories(self):
164 """
165 main() calls its dependencies while in specific directories.
166 """
167 script.collect_logs.side_effect = (
168 lambda _: self.assert_cwd(self.tempdir))
169 script.collect_inner_logs.side_effect = (
170 lambda _: self.assert_cwd(self.tempdir))
171 script.bundle_logs.side_effect = lambda *a: self.assert_cwd(self.cwd)
172 tarfile = "/tmp/logs.tgz"
173 extrafiles = ["spam.py"]
174
175 script.main(tarfile, extrafiles, juju=self.juju)
176
177 def test_no_script_recursion_for_inner_model(self):
178 """
179 main() will not call collect_inner_logs() if --inner is True.
180 """
181 tarfile = "/tmp/logs.tgz"
182 extrafiles = ["spam.py"]
183 cfgdir = "/var/lib/landscape/juju-homes/0"
184 juju = script.get_juju(script.JUJU2, cfgdir)
185
186 script.main(tarfile, extrafiles, juju=juju, inner=True)
187
188 script.collect_logs.assert_called_once_with(juju)
189 script.collect_inner_logs.assert_not_called()
190 script.bundle_logs.assert_called_once_with(
191 self.tempdir, tarfile, extrafiles)
192 self.assertFalse(os.path.exists(self.tempdir))
193
194 def test_cleanup(self):
195 """
196 main() cleans up the temp dir it creates.
197 """
198 tarfile = "/tmp/logs.tgz"
199 extrafiles = ["spam.py"]
200
201 script.main(tarfile, extrafiles, juju=self.juju)
202
203 self.assertFalse(os.path.exists(self.tempdir))
204
205 def test_collect_logs_error(self):
206 """
207 main() doesn't handle the error when collect_logs() fails.
208
209 It still cleans up the temp dir.
210 """
211 tarfile = "/tmp/logs.tgz"
212 extrafiles = ["spam.py"]
213 script.collect_logs.side_effect = FakeError()
214
215 with self.assertRaises(FakeError):
216 script.main(tarfile, extrafiles, juju=self.juju)
217
218 script.collect_logs.assert_called_once_with(self.juju)
219 script.collect_inner_logs.assert_not_called()
220 script.bundle_logs.assert_not_called()
221 self.assertFalse(os.path.exists(self.tempdir))
222
223 def test_collect_inner_logs_error(self):
224 """
225 main() ignores the error when collect_inner_logs() fails.
226
227 It still cleans up the temp dir.
228 """
229 tarfile = "/tmp/logs.tgz"
230 extrafiles = ["spam.py"]
231 script.collect_inner_logs.side_effect = FakeError()
232
233 script.main(tarfile, extrafiles, juju=self.juju)
234
235 script.collect_logs.assert_called_once_with(self.juju)
236 script.collect_inner_logs.assert_called_once_with(
237 self.juju, script.DEFAULT_MODEL)
238 script.bundle_logs.assert_called_once_with(
239 self.tempdir, tarfile, extrafiles)
240 self.assertFalse(os.path.exists(self.tempdir))
241
242 def test_bundle_logs_error(self):
243 """
244 main() doesn't handle the error when bundle_logs() fails.
245
246 It still cleans up the temp dir.
247 """
248 tarfile = "/tmp/logs.tgz"
249 extrafiles = ["spam.py"]
250 script.bundle_logs.side_effect = FakeError()
251
252 with self.assertRaises(FakeError):
253 script.main(tarfile, extrafiles, juju=self.juju)
254
255 script.collect_logs.assert_called_once_with(self.juju)
256 script.collect_inner_logs.assert_called_once_with(
257 self.juju, script.DEFAULT_MODEL)
258 script.bundle_logs.assert_called_once_with(
259 self.tempdir, tarfile, extrafiles)
260 self.assertFalse(os.path.exists(self.tempdir))
261
262
263class CollectLogsTestCase(_BaseTestCase):
264
265 MOCKED = ("get_units", "check_output", "call")
266
267 def setUp(self):
268 super(CollectLogsTestCase, self).setUp()
269
270 self.units = [
271 "landscape-server/0",
272 "postgresql/0",
273 "rabbitmq-server/0",
274 "haproxy/0",
275 ]
276 script.get_units.return_value = self.units[:]
277
278 self.mp_map_orig = script._mp_map
279 script._mp_map = lambda f, a: map(f, a)
280
281 os.chdir(self.tempdir)
282
283 def tearDown(self):
284 script._mp_map = self.mp_map_orig
285
286 super(CollectLogsTestCase, self).tearDown()
287
288 def _call_side_effect(self, cmd, env=None):
289 """Perform the side effect of calling the mocked-out call()."""
290 if cmd[0] == "tar":
291 self.assertTrue(os.path.exists(cmd[-1]))
292 return
293 self.assertEqual(env, self.juju.env)
294 self.assertEqual(cmd[0], self.juju.binary_path)
295 _create_file(os.path.basename(cmd[2]))
296
297 def test_success(self):
298 """
299 collect_logs() gathers "ps" output and logs from each unit.
300 """
301 script.call.side_effect = self._call_side_effect
302
303 script.collect_logs(self.juju)
304
305 script.get_units.assert_called_once_with(self.juju)
306 expected = []
307 units = self.units + ["0"]
308 # for _create_ps_output_file()
309 for unit in units:
310 cmd = "ps fauxww | sudo tee /var/log/ps-fauxww.txt"
311 expected.append(mock.call(["juju", "ssh", unit, cmd],
312 stderr=subprocess.STDOUT,
313 env=None,
314 ))
315 # for _create_log_tarball()
316 for unit in units:
317 tarfile = "/tmp/logs_{}.tar".format(unit.replace("/", "-")
318 if unit != "0"
319 else "bootstrap")
320 cmd = ("sudo tar --ignore-failed-read"
321 " --exclude=/var/lib/landscape/client/package/hash-id"
322 " --exclude=/var/lib/juju/containers/juju-*-lxc-template"
323 " -cf {}"
324 " $(sudo sh -c \"ls -1d {} 2>/dev/null\")"
325 ).format(
326 tarfile,
327 " ".join(["/var/log",
328 "/etc/hosts",
329 "/etc/network",
330 "/var/lib/landscape/client",
331 "/etc/apache2",
332 "/etc/haproxy",
333 "/var/lib/lxc/*/rootfs/var/log",
334 "/var/lib/juju/containers",
335 "/etc/nova",
336 "/etc/swift",
337 "/etc/neutron",
338 "/etc/ceph",
339 "/etc/glance",
340 ]),
341 )
342 expected.append(mock.call(["juju", "ssh", unit, cmd],
343 stderr=subprocess.STDOUT,
344 env=None,
345 ))
346 expected.append(mock.call(["juju", "ssh", unit,
347 "sudo gzip -f {}".format(tarfile)],
348 stderr=subprocess.STDOUT,
349 env=None,
350 ))
351 self.assertEqual(script.check_output.call_count, len(expected))
352 script.check_output.assert_has_calls(expected, any_order=True)
353 # for download_log_from_unit()
354 expected = []
355 for unit in units:
356 name = unit.replace("/", "-") if unit != "0" else "bootstrap"
357 filename = "logs_{}.tar.gz".format(name)
358 source = "{}:/tmp/{}".format(unit, filename)
359 expected.append(mock.call(["juju", "scp", source, "."], env=None))
360 expected.append(mock.call(["tar", "-C", name, "-xzf", filename]))
361 self.assertFalse(os.path.exists(filename))
362 self.assertEqual(script.call.call_count, len(expected))
363 script.call.assert_has_calls(expected, any_order=True)
364
365 def test_inner(self):
366 """
367 collect_logs() gathers "ps" output and logs from each unit.
368 Running in the inner model produces different commands.
369 """
370 cfgdir = "/var/lib/landscape/juju-homes/0"
371 juju = script.Juju("juju-2.1", model="controller", cfgdir=cfgdir)
372 self.juju = juju
373 script.call.side_effect = self._call_side_effect
374
375 script.collect_logs(juju)
376
377 script.get_units.assert_called_once_with(juju)
378 expected = []
379 units = self.units + ["0"]
380 # for _create_ps_output_file()
381 for unit in units:
382 cmd = "ps fauxww | sudo tee /var/log/ps-fauxww.txt"
383 expected.append(mock.call(["juju-2.1", "ssh",
384 "-m", "controller", unit, cmd],
385 stderr=subprocess.STDOUT,
386 env=juju.env,
387 ))
388 # for _create_log_tarball()
389 for unit in units:
390 tarfile = "/tmp/logs_{}.tar".format(unit.replace("/", "-")
391 if unit != "0"
392 else "bootstrap")
393 cmd = ("sudo tar --ignore-failed-read"
394 " --exclude=/var/lib/landscape/client/package/hash-id"
395 " --exclude=/var/lib/juju/containers/juju-*-lxc-template"
396 " -cf {}"
397 " $(sudo sh -c \"ls -1d {} 2>/dev/null\")"
398 ).format(
399 tarfile,
400 " ".join(["/var/log",
401 "/etc/hosts",
402 "/etc/network",
403 "/var/lib/landscape/client",
404 "/etc/apache2",
405 "/etc/haproxy",
406 "/var/lib/lxc/*/rootfs/var/log",
407 "/var/lib/juju/containers",
408 "/etc/nova",
409 "/etc/swift",
410 "/etc/neutron",
411 "/etc/ceph",
412 "/etc/glance",
413 ]),
414 )
415 expected.append(mock.call(
416 ["juju-2.1", "ssh", "-m", "controller", unit, cmd],
417 stderr=subprocess.STDOUT,
418 env=juju.env,
419 ))
420 expected.append(mock.call(
421 ["juju-2.1", "ssh", "-m", "controller", unit,
422 "sudo gzip -f {}".format(tarfile)],
423 stderr=subprocess.STDOUT,
424 env=juju.env,
425 ))
426 self.assertEqual(script.check_output.call_count, len(expected))
427 script.check_output.assert_has_calls(expected, any_order=True)
428 # for download_log_from_unit()
429 expected = []
430 for unit in units:
431 name = unit.replace("/", "-") if unit != "0" else "bootstrap"
432 filename = "logs_{}.tar.gz".format(name)
433 source = "{}:/tmp/{}".format(unit, filename)
434 expected.append(mock.call(
435 ["juju-2.1", "scp", "-m", "controller", source, "."],
436 env=juju.env))
437 expected.append(mock.call(["tar", "-C", name, "-xzf", filename]))
438 self.assertFalse(os.path.exists(filename))
439 self.assertEqual(script.call.call_count, len(expected))
440 script.call.assert_has_calls(expected, any_order=True)
441
442 def test_get_units_failure(self):
443 """
444 collect_logs() does not handle errors from get_units().
445 """
446 script.get_units.side_effect = FakeError()
447
448 with self.assertRaises(FakeError):
449 script.collect_logs(self.juju)
450
451 script.get_units.assert_called_once_with(self.juju)
452 script.check_output.assert_not_called()
453 script.call.assert_not_called()
454
455 def test_check_output_failure(self):
456 """
457 collect_logs() does not handle errors from check_output().
458 """
459 script.check_output.side_effect = [mock.DEFAULT,
460 FakeError(),
461 ]
462
463 with self.assertRaises(FakeError):
464 script.collect_logs(self.juju)
465
466 script.get_units.assert_called_once_with(self.juju)
467 self.assertEqual(script.check_output.call_count, 2)
468 script.call.assert_not_called()
469
470 def test_call_failure(self):
471 """
472 collect_logs() does not handle errors from call().
473 """
474 def call_side_effect(cmd, env=None):
475 # second use of call() for landscape-server/0
476 if script.call.call_count == 2:
477 raise FakeError()
478 # first use of call() for postgresql/0
479 if script.call.call_count == 3:
480 raise FakeError()
481 # all other uses of call() default to the normal side effect.
482 return self._call_side_effect(cmd, env=env)
483 script.call.side_effect = call_side_effect
484
485 script.collect_logs(self.juju)
486
487 script.get_units.assert_called_once_with(self.juju)
488 units = self.units + ["0"]
489 self.assertEqual(script.check_output.call_count, len(units) * 3)
490 self.assertEqual(script.call.call_count, len(units) * 2 - 1)
491 for unit in units:
492 name = unit.replace("/", "-") if unit != "0" else "bootstrap"
493 if unit == self.units[1]:
494 self.assertFalse(os.path.exists(name))
495 else:
496 self.assertTrue(os.path.exists(name))
497 filename = "logs_{}.tar.gz".format(name)
498 self.assertFalse(os.path.exists(filename))
499
500
501class CollectInnerLogsTestCase(_BaseTestCase):
502
503 MOCKED = ("get_units", "check_output", "call", "check_call")
504
505 def setUp(self):
506 super(CollectInnerLogsTestCase, self).setUp()
507
508 self.units = [
509 "landscape-server/0",
510 "postgresql/0",
511 "rabbitmq-server/0",
512 "haproxy/0",
513 ]
514 script.get_units.return_value = self.units[:]
515 script.check_output.return_value = "0\n"
516 script.call.return_value = 0
517
518 os.chdir(self.tempdir)
519
520 def assert_clean(self):
521 """Ensure that collect_inner_logs cleaned up after itself."""
522 self.assert_cwd(self.tempdir)
523 self.assertFalse(os.path.exists("inner-logs.tar.gz"))
524
525 def test_juju_2(self):
526 """
527 collect_inner_logs() finds the inner model and runs collect-logs
528 inside it. The resulting tarball is downloaded, extracted, and
529 deleted.
530 """
531 def check_call_side_effect(cmd, env=None):
532 self.assertEqual(env, self.juju.env)
533 if script.check_call.call_count == 4:
534 self.assert_cwd(self.tempdir)
535 self._create_tempfile("inner-logs.tar.gz")
536 elif script.check_call.call_count == 5:
537 cwd = os.path.join(self.tempdir, "landscape-0-inner-logs")
538 self.assert_cwd(cwd)
539 return None
540 script.check_call.side_effect = check_call_side_effect
541
542 script.collect_inner_logs(self.juju)
543
544 # Check get_units() calls.
545 script.get_units.assert_called_once_with(self.juju)
546 # Check check_output() calls.
547 expected = []
548 cmd = ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/"
549 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
550 " juju-2.1 model-config -m controller proxy-ssh=false")
551 expected.append(mock.call(["juju", "ssh", "landscape-server/0", cmd],
552 stderr=subprocess.STDOUT,
553 env=self.juju.env))
554 expected.append(mock.call(
555 ["juju", "ssh", "landscape-server/0",
556 "sudo ls -rt /var/lib/landscape/juju-homes/"],
557 env=self.juju.env))
558 self.assertEqual(script.check_output.call_count, len(expected))
559 script.check_output.assert_has_calls(expected, any_order=True)
560 # Check call() calls.
561 expected = [
562 mock.call(["juju", "ssh", "landscape-server/0",
563 ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/0 "
564 "juju-2.1 status -m controller --format=yaml"),
565 ], env=self.juju.env),
566 mock.call(["juju", "scp",
567 os.path.join(os.path.dirname(__file__), "collect-logs"),
568 "landscape-server/0:/tmp/collect-logs",
569 ], env=self.juju.env),
570 mock.call(["juju", "ssh",
571 "landscape-server/0",
572 "sudo rm -rf /tmp/inner-logs.tar.gz",
573 ], env=self.juju.env),
574 ]
575 self.assertEqual(script.call.call_count, len(expected))
576 script.call.assert_has_calls(expected, any_order=True)
577 # Check check_call() calls.
578 cmd = ("sudo"
579 " JUJU_DATA=/var/lib/landscape/juju-homes/0"
580 " /tmp/collect-logs --inner --juju juju-2.1"
581 " --model controller"
582 " --cfgdir /var/lib/landscape/juju-homes/0"
583 " /tmp/inner-logs.tar.gz")
584 expected = [
585 mock.call(["juju", "ssh", "landscape-server/0", cmd],
586 env=self.juju.env),
587 mock.call(["juju", "scp",
588 "landscape-server/0:/tmp/inner-logs.tar.gz",
589 os.path.join(self.tempdir, "inner-logs.tar.gz"),
590 ], env=self.juju.env),
591 mock.call(["tar", "-zxf", self.tempdir + "/inner-logs.tar.gz"]),
592 ]
593 self.assertEqual(script.check_call.call_count, len(expected))
594 script.check_call.assert_has_calls(expected, any_order=True)
595 self.assert_clean()
596
597 def test_juju_1(self):
598 """
599 collect_inner_logs() finds the inner model and runs collect-logs
600 inside it. The resulting tarball is downloaded, extracted, and
601 deleted.
602 """
603 def check_call_side_effect(cmd, env=None):
604 self.assertEqual(env, self.juju.env)
605 if script.check_call.call_count == 4:
606 self.assert_cwd(self.tempdir)
607 self._create_tempfile("inner-logs.tar.gz")
608 elif script.check_call.call_count == 5:
609 cwd = os.path.join(self.tempdir, "landscape-0-inner-logs")
610 self.assert_cwd(cwd)
611 return None
612 script.check_call.side_effect = check_call_side_effect
613 script.call.side_effect = [1, 0, 0, 0]
614 err = subprocess.CalledProcessError(1, "...", "<output>")
615 script.check_output.side_effect = [err,
616 mock.DEFAULT,
617 mock.DEFAULT,
618 ]
619
620 script.collect_inner_logs(self.juju)
621
622 # Check get_units() calls.
623 script.get_units.assert_called_once_with(self.juju)
624 # Check check_output() calls.
625 expected = []
626 cmd = ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/"
627 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
628 " juju-2.1 model-config -m controller proxy-ssh=false")
629 expected.append(mock.call(["juju", "ssh", "landscape-server/0", cmd],
630 stderr=subprocess.STDOUT,
631 env=None))
632 cmd = ("sudo JUJU_HOME=/var/lib/landscape/juju-homes/"
633 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
634 " juju set-env proxy-ssh=false")
635 expected.append(mock.call(["juju", "ssh", "landscape-server/0", cmd],
636 stderr=subprocess.STDOUT,
637 env=None))
638 expected.append(mock.call(
639 ["juju", "ssh", "landscape-server/0",
640 "sudo ls -rt /var/lib/landscape/juju-homes/"],
641 env=None))
642 self.assertEqual(script.check_output.call_count, len(expected))
643 script.check_output.assert_has_calls(expected, any_order=True)
644 # Check call() calls.
645 expected = [
646 mock.call(["juju", "ssh", "landscape-server/0",
647 ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/0 "
648 "juju-2.1 status -m controller --format=yaml"),
649 ], env=None),
650 mock.call(["juju", "ssh", "landscape-server/0",
651 ("sudo -u landscape "
652 "JUJU_HOME=/var/lib/landscape/juju-homes/0 "
653 "juju status --format=yaml"),
654 ], env=None),
655 mock.call(["juju", "scp",
656 os.path.join(os.path.dirname(__file__), "collect-logs"),
657 "landscape-server/0:/tmp/collect-logs",
658 ], env=None),
659 mock.call(["juju", "ssh",
660 "landscape-server/0",
661 "sudo rm -rf /tmp/inner-logs.tar.gz",
662 ], env=None),
663 ]
664 self.assertEqual(script.call.call_count, len(expected))
665 script.call.assert_has_calls(expected, any_order=True)
666 # Check check_call() calls.
667 cmd = ("sudo -u landscape"
668 " JUJU_HOME=/var/lib/landscape/juju-homes/0"
669 " /tmp/collect-logs --inner --juju juju"
670 " --cfgdir /var/lib/landscape/juju-homes/0"
671 " /tmp/inner-logs.tar.gz")
672 expected = [
673 mock.call(["juju", "ssh", "landscape-server/0", cmd], env=None),
674 mock.call(["juju", "scp",
675 "landscape-server/0:/tmp/inner-logs.tar.gz",
676 os.path.join(self.tempdir, "inner-logs.tar.gz"),
677 ], env=None),
678 mock.call(["tar", "-zxf", self.tempdir + "/inner-logs.tar.gz"]),
679 ]
680 self.assertEqual(script.check_call.call_count, len(expected))
681 script.check_call.assert_has_calls(expected, any_order=True)
682 self.assert_clean()
683
684 def test_with_legacy_landscape_unit(self):
685 """
686 collect_inner_logs() correctly supports legacy landscape installations.
687 """
688 self.units[0] = "landscape/0"
689 script.get_units.return_value = self.units[:]
690 err = subprocess.CalledProcessError(1, "...", "<output>")
691 script.check_output.side_effect = [err,
692 mock.DEFAULT,
693 mock.DEFAULT,
694 ]
695
696 script.collect_inner_logs(self.juju)
697
698 expected = []
699 cmd = ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/"
700 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
701 " juju-2.1 model-config -m controller proxy-ssh=false")
702 expected.append(mock.call(["juju", "ssh", "landscape/0", cmd],
703 stderr=subprocess.STDOUT,
704 env=None))
705 cmd = ("sudo JUJU_HOME=/var/lib/landscape/juju-homes/"
706 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
707 " juju set-env proxy-ssh=false")
708 expected.append(mock.call(["juju", "ssh", "landscape/0", cmd],
709 stderr=subprocess.STDOUT,
710 env=None))
711 expected.append(mock.call(
712 ["juju", "ssh", "landscape/0",
713 "sudo ls -rt /var/lib/landscape/juju-homes/"],
714 env=None))
715 self.assertEqual(script.check_output.call_count, len(expected))
716 script.check_output.assert_has_calls(expected, any_order=True)
717 self.assert_clean()
718
719 def test_no_units(self):
720 """
721 collect_inner_logs() is a noop if no units are found.
722 """
723 script.get_units.return_value = []
724
725 script.collect_inner_logs(self.juju)
726
727 script.get_units.assert_called_once_with(self.juju)
728 script.check_output.assert_not_called()
729 script.call.assert_not_called()
730 script.check_call.assert_not_called()
731 self.assert_clean()
732
733 def test_no_landscape_server_unit(self):
734 """
735 collect_inner_logs() is a noop if the landscape unit isn't found.
736 """
737 del self.units[0]
738 script.get_units.return_value = self.units[:]
739
740 script.collect_inner_logs(self.juju)
741
742 script.get_units.assert_called_once_with(self.juju)
743 script.check_output.assert_not_called()
744 script.call.assert_not_called()
745 script.check_call.assert_not_called()
746 self.assert_clean()
747
748 def test_no_juju_homes(self):
749 script.get_units.return_value = []
750 script.check_output.return_value = ""
751
752 script.collect_inner_logs(self.juju)
753
754 script.get_units.assert_called_once_with(self.juju)
755
756 script.get_units.assert_called_once_with(self.juju)
757 script.check_output.assert_not_called()
758 script.call.assert_not_called()
759 script.check_call.assert_not_called()
760 self.assert_clean()
761
762 def test_get_units_failure(self):
763 """
764 collect_inner_logs() does not handle errors from get_units().
765 """
766 script.get_units.side_effect = FakeError()
767
768 with self.assertRaises(FakeError):
769 script.collect_inner_logs(self.juju)
770
771 self.assertEqual(script.get_units.call_count, 1)
772 script.check_output.assert_not_called()
773 script.call.assert_not_called()
774 script.check_call.assert_not_called()
775 self.assert_cwd(self.tempdir)
776 self.assert_clean()
777
778 def test_check_output_failure_1(self):
779 """
780 collect_inner_logs() does not handle non-CalledProcessError
781 errors when disabling the SSH proxy.
782 """
783 script.check_output.side_effect = FakeError()
784
785 with self.assertRaises(FakeError):
786 script.collect_inner_logs(self.juju)
787
788 self.assertEqual(script.get_units.call_count, 1)
789 self.assertEqual(script.check_output.call_count, 1)
790 script.call.assert_not_called()
791 script.check_call.assert_not_called()
792 self.assert_cwd(self.tempdir)
793 self.assert_clean()
794
795 def test_check_output_failure_2(self):
796 """
797 collect_inner_logs() does not handle non-CalledProcessError
798 errors when verifying the inner model is bootstrapped.
799 """
800 script.check_output.side_effect = [None,
801 FakeError(),
802 ]
803
804 with self.assertRaises(FakeError):
805 script.collect_inner_logs(self.juju)
806
807 self.assertEqual(script.get_units.call_count, 1)
808 self.assertEqual(script.check_output.call_count, 2)
809 script.call.assert_not_called()
810 script.check_call.assert_not_called()
811 self.assert_cwd(self.tempdir)
812 self.assert_clean()
813
814 def test_call_juju2_failure(self):
815 """
816 collect_inner_logs() does not handle errors from call().
817 """
818 script.call.side_effect = FakeError()
819
820 with self.assertRaises(FakeError):
821 script.collect_inner_logs(self.juju)
822
823 self.assertEqual(script.get_units.call_count, 1)
824 self.assertEqual(script.check_output.call_count, 2)
825 self.assertEqual(script.call.call_count, 1)
826 script.check_call.assert_not_called()
827 self.assert_cwd(self.tempdir)
828 self.assert_clean()
829
830 def test_call_juju1_failure(self):
831 """
832 collect_inner_logs() does not handle errors from call().
833 """
834 script.call.side_effect = [1,
835 FakeError(),
836 ]
837
838 with self.assertRaises(FakeError):
839 script.collect_inner_logs(self.juju)
840
841 self.assertEqual(script.get_units.call_count, 1)
842 self.assertEqual(script.check_output.call_count, 2)
843 self.assertEqual(script.call.call_count, 2)
844 script.check_call.assert_not_called()
845 self.assert_cwd(self.tempdir)
846 self.assert_clean()
847
848 def test_call_juju2_nonzero_return(self):
849 """
850 When no Juju 2 model is detected, Juju 1 is tried.
851 """
852 script.call.side_effect = [1,
853 mock.DEFAULT,
854 mock.DEFAULT,
855 mock.DEFAULT,
856 ]
857
858 script.collect_inner_logs(self.juju)
859
860 self.assertEqual(script.get_units.call_count, 1)
861 self.assertEqual(script.check_output.call_count, 2)
862 self.assertEqual(script.call.call_count, 4)
863 self.assertEqual(script.check_call.call_count, 3)
864 self.assert_clean()
865
866 def test_call_juju1_nonzero_return(self):
867 """
868 When no Juju 2 model is detected, Juju 1 is tried. When that
869 is not detected, no inner logs are collected.
870 """
871 script.call.side_effect = [1,
872 1,
873 ]
874
875 script.collect_inner_logs(self.juju)
876
877 self.assertEqual(script.get_units.call_count, 1)
878 self.assertEqual(script.check_output.call_count, 2)
879 self.assertEqual(script.call.call_count, 2)
880 script.check_call.assert_not_called()
881 self.assert_clean()
882
883 def test_call_all_nonzero_return(self):
884 """
885 When no Juju 2 model is detected, Juju 1 is tried. When that
886 is not detected, no inner logs are collected.
887 """
888 script.call.return_value = 1
889
890 script.collect_inner_logs(self.juju)
891
892 self.assertEqual(script.get_units.call_count, 1)
893 self.assertEqual(script.check_output.call_count, 2)
894 self.assertEqual(script.call.call_count, 2)
895 script.check_call.assert_not_called()
896 self.assert_clean()
897
898 def test_check_call_failure_1(self):
899 """
900 collect_inner_logs() does not handle errors when running
901 collect-logs in the inner model.
902 """
903 script.check_call.side_effect = FakeError()
904
905 with self.assertRaises(FakeError):
906 script.collect_inner_logs(self.juju)
907
908 self.assertEqual(script.get_units.call_count, 1)
909 self.assertEqual(script.check_output.call_count, 2)
910 self.assertEqual(script.call.call_count, 3)
911 self.assertEqual(script.check_call.call_count, 1)
912 self.assert_clean()
913
914 def test_check_call_failure_2(self):
915 """
916 collect_inner_logs() does not handle errors downloading the
917 collected logs from the inner model.
918
919 It does clean up, however.
920 """
921 script.check_call.side_effect = [None,
922 FakeError(),
923 ]
924
925 with self.assertRaises(FakeError):
926 script.collect_inner_logs(self.juju)
927
928 self.assertEqual(script.get_units.call_count, 1)
929 self.assertEqual(script.check_output.call_count, 2)
930 self.assertEqual(script.call.call_count, 3)
931 self.assertEqual(script.check_call.call_count, 2)
932 self.assert_clean()
933
934 def test_check_call_failure_3(self):
935 def check_call_side_effect(cmd, env=None):
936 self.assertEqual(env, self.juju.env)
937 if script.check_call.call_count == 3:
938 raise FakeError()
939 if script.check_call.call_count == 2:
940 self._create_tempfile("inner-logs.tar.gz")
941 return None
942 script.check_call.side_effect = check_call_side_effect
943
944 with self.assertRaises(FakeError):
945 script.collect_inner_logs(self.juju)
946
947 self.assertEqual(script.get_units.call_count, 1)
948 self.assertEqual(script.check_output.call_count, 2)
949 self.assertEqual(script.call.call_count, 3)
950 self.assertEqual(script.check_call.call_count, 3)
951 self.assert_clean()
952
953
954class BundleLogsTestCase(_BaseTestCase):
955
956 MOCKED = ("call",)
957
958 def setUp(self):
959 """
960 bundle_logs() creates a tarball holding the files in the tempdir.
961 """
962 super(BundleLogsTestCase, self).setUp()
963
964 os.chdir(self.tempdir)
965
966 self._create_tempfile("bootstrap/var/log/syslog")
967 self._create_tempfile("bootstrap/var/log/juju/all-machines.log")
968 self._create_tempfile(
969 "bootstrap/var/lib/lxc/deadbeef/rootfs/var/log/syslog")
970 self._create_tempfile("bootstrap/var/lib/juju/containers")
971 self._create_tempfile("landscape-server-0/var/log/syslog")
972 self._create_tempfile("postgresql-0/var/log/syslog")
973 self._create_tempfile("rabbitmq-server-0/var/log/syslog")
974 self._create_tempfile("haproxy-0/var/log/syslog")
975 self._create_tempfile(
976 "landscape-0-inner-logs/bootstrap/var/log/syslog")
977
978 self.extrafile = os.path.join(self.cwd, "spam.txt")
979 _create_file(self.extrafile)
980
981 def test_success_with_extra(self):
982 """
983 bundle_logs() works if extra files are included.
984 """
985 tarfile = "/tmp/logs.tgz"
986 extrafiles = [self.extrafile]
987
988 script.bundle_logs(self.tempdir, tarfile, extrafiles)
989
990 script.call.assert_called_once_with(
991 ["tar",
992 "czf", tarfile,
993 "--transform", "s,{}/,,".format(self.tempdir[1:]),
994 os.path.join(self.tempdir, "bootstrap"),
995 os.path.join(self.tempdir, "haproxy-0"),
996 os.path.join(self.tempdir, "landscape-0-inner-logs"),
997 os.path.join(self.tempdir, "landscape-server-0"),
998 os.path.join(self.tempdir, "postgresql-0"),
999 os.path.join(self.tempdir, "rabbitmq-server-0"),
1000 self.extrafile,
1001 ],
1002 )
1003
1004 def test_success_without_extra(self):
1005 """
1006 bundle_logs() works if there aren't any extra files.
1007 """
1008 tarfile = "/tmp/logs.tgz"
1009
1010 script.bundle_logs(self.tempdir, tarfile)
1011
1012 script.call.assert_called_once_with(
1013 ["tar",
1014 "czf", tarfile,
1015 "--transform", "s,{}/,,".format(self.tempdir[1:]),
1016 os.path.join(self.tempdir, "bootstrap"),
1017 os.path.join(self.tempdir, "haproxy-0"),
1018 os.path.join(self.tempdir, "landscape-0-inner-logs"),
1019 os.path.join(self.tempdir, "landscape-server-0"),
1020 os.path.join(self.tempdir, "postgresql-0"),
1021 os.path.join(self.tempdir, "rabbitmq-server-0"),
1022 ],
1023 )
1024
1025 def test_success_no_files(self):
1026 """
1027 bundle_logs() works even when the temp dir is empty.
1028 """
1029 for filename in os.listdir(self.tempdir):
1030 shutil.rmtree(os.path.join(self.tempdir, filename))
1031 tarfile = "/tmp/logs.tgz"
1032
1033 script.bundle_logs(self.tempdir, tarfile)
1034
1035 script.call.assert_called_once_with(
1036 ["tar",
1037 "czf", tarfile,
1038 "--transform", "s,{}/,,".format(self.tempdir[1:]),
1039 ],
1040 )
1041
1042 def test_call_failure(self):
1043 """
1044 bundle_logs() does not handle errors when creating the tarball.
1045 """
1046 script.call.side_effect = FakeError()
1047 tarfile = "/tmp/logs.tgz"
1048
1049 with self.assertRaises(FakeError):
1050 script.bundle_logs(self.tempdir, tarfile)
1051
1052 script.call.assert_called_once_with(
1053 ["tar",
1054 "czf", tarfile,
1055 "--transform", "s,{}/,,".format(self.tempdir[1:]),
1056 os.path.join(self.tempdir, "bootstrap"),
1057 os.path.join(self.tempdir, "haproxy-0"),
1058 os.path.join(self.tempdir, "landscape-0-inner-logs"),
1059 os.path.join(self.tempdir, "landscape-server-0"),
1060 os.path.join(self.tempdir, "postgresql-0"),
1061 os.path.join(self.tempdir, "rabbitmq-server-0"),
1062 ],
1063 )

Subscribers

People subscribed via source and target branches

to all changes: