Merge lp:~ericsnowcurrently/landscape-charm/tools-unit-tests-for-script into lp:~landscape/landscape-charm/tools

Proposed by Eric Snow on 2016-08-16
Status: Merged
Approved by: Eric Snow on 2016-08-19
Approved revision: 28
Merged at revision: 23
Proposed branch: lp:~ericsnowcurrently/landscape-charm/tools-unit-tests-for-script
Merge into: lp:~landscape/landscape-charm/tools
Diff against target: 1130 lines (+910/-78)
2 files modified
collect-logs (+161/-78)
test_collect-logs.py (+749/-0)
To merge this branch: bzr merge lp:~ericsnowcurrently/landscape-charm/tools-unit-tests-for-script
Reviewer Review Type Date Requested Status
Chad Smith 2016-08-16 Approve on 2016-08-19
Benji York (community) 2016-08-16 Approve on 2016-08-18
Review via email: mp+303061@code.launchpad.net

Commit message

Add unit tests for the collect-logs script.

Description of the change

Add unit tests for the collect-logs script.

Testing instructions:

Run the tests with "python -m unittest test_collect-logs".

To post a comment you must log in.
21. By Eric Snow on 2016-08-17

Merge from tools branch.

Benji York (benji) wrote :

Assuming the inline comments are addressed, this branch looks good to me.

review: Approve
Chad Smith (chad.smith) wrote :

+1 plus the fix we talked about in irc to avoid hitting an external "real" juju when running execfile
 https://pastebin.canonical.com/163587/

review: Approve
22. By Eric Snow on 2016-08-19

Do not call juju_status() as a default keyword argument.

23. By Eric Snow on 2016-08-19

typo

24. By Eric Snow on 2016-08-19

_touch() -> _create_file()

25. By Eric Snow on 2016-08-19

Clarify some comments.

26. By Eric Snow on 2016-08-19

Add missing test docstrings.

27. By Eric Snow on 2016-08-19

Explicitly pass the juju command in to the various functions.

28. By Eric Snow on 2016-08-19

Add CommandWrapper.

29. By Eric Snow on 2016-08-23

Add the Juju class.

Preview Diff

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

Subscribers

People subscribed via source and target branches

to all changes: