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

Proposed by Eric Snow
Status: Merged
Approved by: Eric Snow
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 Approve
Benji York (community) Approve
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

Merge from tools branch.

Revision history for this message
Benji York (benji) wrote :

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

review: Approve
Revision history for this message
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
Revision history for this message
Eric Snow (ericsnowcurrently) :
22. By Eric Snow

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

23. By Eric Snow

typo

24. By Eric Snow

_touch() -> _create_file()

25. By Eric Snow

Clarify some comments.

26. By Eric Snow

Add missing test docstrings.

27. By Eric Snow

Explicitly pass the juju command in to the various functions.

28. By Eric Snow

Add CommandWrapper.

29. By Eric Snow

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: