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
=== modified file 'collect-logs'
--- collect-logs 2016-07-29 19:41:15 +0000
+++ collect-logs 2016-08-23 00:10:59 +0000
@@ -1,15 +1,18 @@
1#!/usr/bin/python1#!/usr/bin/python
22
3from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
4import errno
5from functools import partial
3import logging6import logging
4import multiprocessing7import multiprocessing
5import os8import os
6import shutil9import shutil
10from subprocess import (
11 CalledProcessError, check_call, check_output, call, STDOUT)
7import sys12import sys
8import tempfile13from tempfile import mkdtemp
14
9import yaml15import yaml
10from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
11from subprocess import (
12 CalledProcessError, check_call, check_output, call, STDOUT)
1316
1417
15log = logging.getLogger("collect-logs")18log = logging.getLogger("collect-logs")
@@ -21,17 +24,61 @@
21 "/var/lib/landscape/client"]24 "/var/lib/landscape/client"]
22EXCLUDED = ["/var/lib/landscape/client/package/hash-id",25EXCLUDED = ["/var/lib/landscape/client/package/hash-id",
23 "/var/lib/juju/containers/juju-*-lxc-template"]26 "/var/lib/juju/containers/juju-*-lxc-template"]
2427LANDSCAPE_JUJU_HOME = "/var/lib/landscape/juju-homes"
2528
26def juju_status():29JUJU = "juju"
30
31
32class Juju(object):
33 """A wrapper around a juju binary."""
34
35 def __init__(self, binary_path=JUJU, model=None, env=None, sudo=False):
36 self.binary_path = binary_path
37 self.model = model
38 self.sudo = sudo
39 self.env = env
40
41 def status_args(self):
42 """Return the subprocess.* args for a status command."""
43 args = self._resolve("status", "--format=yaml")
44 if self.model:
45 args.append("-e" + self.model)
46 return args
47
48 def ssh_args(self, unit, cmd):
49 """Return the subprocess.* args for an SSH command."""
50 return self._resolve("ssh", unit, cmd)
51
52 def pull_args(self, unit, source, target="."):
53 """Return the subprocess.* args for an SCP command."""
54 source = "{}:{}".format(unit, source)
55 return self._resolve("scp", source, target)
56
57 def push_args(self, unit, source, target):
58 """Return the subprocess.* args for an SCP command."""
59 target = "{}:{}".format(unit, target)
60 return self._resolve("scp", source, target)
61
62 def _resolve(self, sub, *subargs):
63 """Return the subprocess.* args for the juju subcommand."""
64 args = [self.binary_path, sub]
65 if self.sudo:
66 args.insert(0, "sudo")
67 args.extend(subargs)
68 return args
69
70
71def juju_status(juju):
27 """Return a juju status structure."""72 """Return a juju status structure."""
28 cmd = ["juju", "status", "--format=yaml"]73 output = check_output(juju.status_args(), env=juju.env)
29 output = check_output(cmd).decode("utf-8").strip()74 output = output.decode("utf-8").strip()
30 return yaml.load(output)75 return yaml.load(output)
3176
3277
33def get_units(status=juju_status()):78def get_units(juju, status=None):
34 """Return a list with all units."""79 """Return a list with all units."""
80 if status is None:
81 status = juju_status(juju)
35 units = []82 units = []
36 if "services" in status:83 if "services" in status:
37 applications = status["services"]84 applications = status["services"]
@@ -49,12 +96,13 @@
49 return units96 return units
5097
5198
52def _create_ps_output_file(unit):99def _create_ps_output_file(juju, unit):
53 """List running processes and redirect them to a file."""100 """List running processes and redirect them to a file."""
101 log.info("Collecting ps output on unit {}".format(unit))
54 ps_cmd = "ps fauxww | sudo tee /var/log/ps-fauxww.txt"102 ps_cmd = "ps fauxww | sudo tee /var/log/ps-fauxww.txt"
103 args = juju.ssh_args(unit, ps_cmd)
55 try:104 try:
56 log.info("Collecting ps output on unit {}".format(unit))105 check_output(args, stderr=STDOUT, env=juju.env)
57 check_output(["juju", "ssh", unit, ps_cmd], stderr=STDOUT)
58 except CalledProcessError as e:106 except CalledProcessError as e:
59 log.warning(107 log.warning(
60 "Failed to collect running processes on unit {}".format(unit))108 "Failed to collect running processes on unit {}".format(unit))
@@ -62,7 +110,8 @@
62 log.warning(e.returncode)110 log.warning(e.returncode)
63111
64112
65def _create_log_tarball(unit):113def _create_log_tarball(juju, unit):
114 log.info("Creating tarball on unit {}".format(unit))
66 exclude = " ".join(["--exclude=%s" % x for x in EXCLUDED])115 exclude = " ".join(["--exclude=%s" % x for x in EXCLUDED])
67 logs = "$(sudo sh -c \"ls -1d %s 2>/dev/null\")" % " ".join(LOGS)116 logs = "$(sudo sh -c \"ls -1d %s 2>/dev/null\")" % " ".join(LOGS)
68 # note, tar commands can fail since we are backing up log files117 # note, tar commands can fail since we are backing up log files
@@ -75,9 +124,9 @@
75 logsuffix = "bootstrap"124 logsuffix = "bootstrap"
76 cmd = "{} {} -czf /tmp/logs_{}.tar.gz {}".format(125 cmd = "{} {} -czf /tmp/logs_{}.tar.gz {}".format(
77 tar_cmd, exclude, logsuffix, logs)126 tar_cmd, exclude, logsuffix, logs)
127 args = juju.ssh_args(unit, cmd)
78 try:128 try:
79 log.info("Creating tarball on unit {}".format(unit))129 check_output(args, stderr=STDOUT, env=juju.env)
80 check_output(["juju", "ssh", unit, cmd], stderr=STDOUT)
81 except CalledProcessError as e:130 except CalledProcessError as e:
82 log.warning(131 log.warning(
83 "Failed to create remote log tarball on unit {}".format(unit))132 "Failed to create remote log tarball on unit {}".format(unit))
@@ -85,7 +134,26 @@
85 log.warning(e.returncode)134 log.warning(e.returncode)
86135
87136
88def collect_logs():137def download_log_from_unit(juju, unit):
138 log.info("Downloading tarball from unit %s" % unit)
139 unit_filename = unit.replace("/", "-")
140 if unit == "0":
141 unit_filename = "bootstrap"
142 remote_filename = "logs_%s.tar.gz" % unit_filename
143 try:
144 args = juju.pull_args(unit, "/tmp/" + remote_filename)
145 call(args, env=juju.env)
146 os.mkdir(unit_filename)
147 call(["tar", "-C", unit_filename, "-xzf", remote_filename])
148 os.unlink(remote_filename)
149 except:
150 log.warning("error collecting logs from %s, skipping" % unit)
151 finally:
152 if os.path.exists(remote_filename):
153 os.unlink(remote_filename)
154
155
156def collect_logs(juju):
89 """157 """
90 Remotely, on each unit, create a tarball with the requested log files158 Remotely, on each unit, create a tarball with the requested log files
91 or directories, if they exist. If a requested log does not exist on a159 or directories, if they exist. If a requested log does not exist on a
@@ -93,18 +161,24 @@
93 After each tarball is created, it's downloaded to the current directory161 After each tarball is created, it's downloaded to the current directory
94 and expanded, and the tarball is then deleted.162 and expanded, and the tarball is then deleted.
95 """163 """
96 units = get_units()164 units = get_units(juju)
97 # include bootstrap165 # include bootstrap
98 units.append("0")166 units.append("0")
167
99 log.info("Collecting running processes for all units including bootstrap")168 log.info("Collecting running processes for all units including bootstrap")
100 pool = multiprocessing.Pool(processes=4)169 map(partial(_create_ps_output_file, juju), units)
101 map(_create_ps_output_file, units)
102170
103 log.info("Creating remote tarball in parallel for units %s" % (171 log.info("Creating remote tarball in parallel for units %s" % (
104 ",".join(units)))172 ",".join(units)))
105 map(_create_log_tarball, units)173 map(partial(_create_log_tarball, juju), units)
106 log.info("Downloading logs from units")174 log.info("Downloading logs from units")
107 pool.map(download_log_from_unit, units)175
176 _mp_map(partial(download_log_from_unit, juju), units)
177
178
179def _mp_map(func, args):
180 pool = multiprocessing.Pool(processes=4)
181 pool.map(func, args)
108182
109183
110def get_landscape_unit(units):184def get_landscape_unit(units):
@@ -120,7 +194,7 @@
120 return units[0]194 return units[0]
121195
122196
123def disable_ssh_proxy(landscape_unit):197def disable_ssh_proxy(juju, landscape_unit):
124 """198 """
125 Workaround for #1607076: disable the proxy-ssh juju environment setting199 Workaround for #1607076: disable the proxy-ssh juju environment setting
126 for the inner cloud so we can juju ssh into it.200 for the inner cloud so we can juju ssh into it.
@@ -131,70 +205,75 @@
131 cmd = cmd_prefix.format(juju_home, juju_home, disable_cmd)205 cmd = cmd_prefix.format(juju_home, juju_home, disable_cmd)
132 log.info("Disabling proxy-ssh in the juju environment on "206 log.info("Disabling proxy-ssh in the juju environment on "
133 "{}".format(landscape_unit))207 "{}".format(landscape_unit))
208 args = juju.ssh_args(landscape_unit, cmd)
134 try:209 try:
135 check_output(["juju", "ssh", landscape_unit, cmd], stderr=STDOUT)210 check_output(args, stderr=STDOUT, env=juju.env)
136 except CalledProcessError as e:211 except CalledProcessError as e:
137 log.warning("Couldn't disable proxy-ssh in the inner environment,"212 log.warning("Couldn't disable proxy-ssh in the inner environment,"
138 " collecting inner logs might fail.")213 " collecting inner logs might fail.")
139 log.warning("Error was:\n{}".format(e.output))214 log.warning("Error was:\n{}".format(e.output))
140215
141def collect_inner_logs():216
217def collect_inner_logs(juju):
142 """Collect logs from an inner landscape[-server]/0 unit."""218 """Collect logs from an inner landscape[-server]/0 unit."""
143 collect_logs = "/tmp/collect-logs"219 units = get_units(juju)
144 landscape_juju_home = "/var/lib/landscape/juju-homes"
145 collect_run = (
146 "JUJU_HOME=%s/`sudo ls -rt %s/ | tail -1`"
147 " %s /tmp/inner-logs.tar.gz" % (
148 landscape_juju_home, landscape_juju_home, collect_logs))
149 poke_inner_env = (
150 "sudo JUJU_HOME=%s/`sudo ls -rt %s/ | tail -1` juju status" %
151 (landscape_juju_home, landscape_juju_home))
152 units = get_units()
153 landscape_unit = get_landscape_unit(units)220 landscape_unit = get_landscape_unit(units)
154 if not landscape_unit:221 if not landscape_unit:
155 log.info("No landscape[-server]/N found, skipping")222 log.info("No landscape[-server]/N found, skipping")
156 return223 return
157 log.info("Found landscape unit %s" % landscape_unit)224 log.info("Found landscape unit %s" % landscape_unit)
225
226 disable_ssh_proxy(juju, landscape_unit)
227
228 # Make sure that there *is* an inner model.
229 poke_inner_env = (
230 "sudo JUJU_HOME=%s/`sudo ls -rt %s/ | tail -1` juju status" %
231 (LANDSCAPE_JUJU_HOME, LANDSCAPE_JUJU_HOME))
232 args = juju.ssh_args(landscape_unit, poke_inner_env)
158 try:233 try:
159 check_output(["juju", "ssh", landscape_unit, poke_inner_env])234 check_output(args, env=juju.env)
160 except CalledProcessError:235 except CalledProcessError:
161 log.info("No active inner environment found on %s, skipping" %236 log.info("No active inner environment found on %s, skipping" %
162 landscape_unit)237 landscape_unit)
163 return238 return
164 disable_ssh_proxy(landscape_unit)239
165 call(["juju", "scp", PRG, "%s:%s" % (landscape_unit, collect_logs)])240 # Prepare to get the logs from the inner model.
166 call(["juju", "ssh", landscape_unit, "sudo rm -rf /tmp/inner-logs.tar.*"])241 collect_logs = "/tmp/collect-logs"
242 args = juju.push_args(landscape_unit, PRG, collect_logs)
243 call(args, env=juju.env)
244 filename = "inner-logs.tar.gz"
245 inner_filename = os.path.join("/tmp", filename)
246 args = juju.ssh_args(landscape_unit, "sudo rm -rf " + inner_filename)
247 call(args, env=juju.env)
248
249 # Collect the logs for the inner model.
167 log.info("Collecting Logs on inner environment")250 log.info("Collecting Logs on inner environment")
168 check_call(251 collect_run = ("JUJU_HOME=%s/`sudo ls -rt %s/ | tail -1` %s %s"
169 ["juju", "ssh", landscape_unit, "sudo -u landscape %s" % collect_run])252 ) % (LANDSCAPE_JUJU_HOME, LANDSCAPE_JUJU_HOME,
253 collect_logs, inner_filename)
254 args = juju.ssh_args(landscape_unit, "sudo -u landscape %s" % collect_run)
255 check_call(args, env=juju.env)
256
257 # Copy the inner logs into a local directory.
170 log.info("Copying inner environment back")258 log.info("Copying inner environment back")
171 check_call(["juju", "scp", "%s:/tmp/inner-logs.tar.gz" % landscape_unit,259 args = juju.pull_args(landscape_unit, inner_filename)
172 "."])260 check_call(args, env=juju.env)
173 cwd = os.getcwd()
174 inner_dir = "landscape-0-inner-logs"
175 os.mkdir(inner_dir)
176 os.chdir(inner_dir)
177 check_call(["tar", "-zxf", "%s/inner-logs.tar.gz" % cwd])
178 os.chdir(cwd)
179 check_call(["rm", "-rf", "inner-logs.tar.gz"])
180
181
182def download_log_from_unit(unit):
183 log.info("Downloading tarball from unit %s" % unit)
184 unit_filename = unit.replace("/", "-")
185 if unit == "0":
186 unit_filename = "bootstrap"
187 try:261 try:
188 remote_filename = "logs_%s.tar.gz" % unit_filename262 cwd = os.getcwd()
189 call(["juju", "scp", "%s:/tmp/%s" % (unit, remote_filename), "."])263 inner_dir = "landscape-0-inner-logs"
190 os.mkdir(unit_filename)264 os.mkdir(inner_dir)
191 call(["tar", "-C", unit_filename, "-xzf", remote_filename])265 os.chdir(inner_dir)
192 os.unlink(remote_filename)266 try:
193 except:267 check_call(["tar", "-zxf", os.path.join(cwd, filename)])
194 log.warning("error collecting logs from %s, skipping" % unit)268 finally:
269 os.chdir(cwd)
195 finally:270 finally:
196 if os.path.exists(remote_filename):271 try:
197 os.unlink(remote_filename)272 os.remove(filename)
273 except OSError as e:
274 if e.errno != errno.ENOENT:
275 log.warning(
276 "failed to remove inner logs tarball: {}".format(e))
198277
199278
200def bundle_logs(tmpdir, tarfile, extrafiles=[]):279def bundle_logs(tmpdir, tarfile, extrafiles=[]):
@@ -224,7 +303,7 @@
224 # get rid of the tmpdir prefix303 # get rid of the tmpdir prefix
225 args.extend(["--transform", "s,{}/,,".format(tmpdir[1:])])304 args.extend(["--transform", "s,{}/,,".format(tmpdir[1:])])
226 # need absolute paths since tmpdir isn't the cwd305 # need absolute paths since tmpdir isn't the cwd
227 args.extend(os.path.join(tmpdir, d) for d in os.listdir(tmpdir))306 args.extend(os.path.join(tmpdir, d) for d in sorted(os.listdir(tmpdir)))
228 if extrafiles:307 if extrafiles:
229 args.extend(extrafiles)308 args.extend(extrafiles)
230 call(args)309 call(args)
@@ -241,32 +320,36 @@
241 return parser320 return parser
242321
243322
244def main():323def main(tarfile, extrafiles, juju=None):
245 logging.basicConfig(324 if juju is None:
246 level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')325 juju = Juju("juju")
247 parser = get_option_parser()326
248 args = parser.parse_args()
249 # we need the absolute path because we will be changing327 # we need the absolute path because we will be changing
250 # the cwd328 # the cwd
251 tarfile = os.path.abspath(args.tarfile)329 tmpdir = mkdtemp()
252 tmpdir = tempfile.mkdtemp()
253 cwd = os.getcwd()330 cwd = os.getcwd()
254 # logs are collected inside a temporary directory331 # logs are collected inside a temporary directory
255 os.chdir(tmpdir)332 os.chdir(tmpdir)
256 try:333 try:
257 collect_logs()334 collect_logs(juju)
258 try:335 try:
259 collect_inner_logs()336 collect_inner_logs(juju)
260 except:337 except:
261 log.warning("Collecting inner logs failed, continuing")338 log.warning("Collecting inner logs failed, continuing")
262 # we create the final tarball outside of tmpdir to we can339 # we create the final tarball outside of tmpdir to we can
263 # add the extrafiles to the tarball root340 # add the extrafiles to the tarball root
264 os.chdir(cwd)341 os.chdir(cwd)
265 bundle_logs(tmpdir, tarfile, args.extrafiles)342 bundle_logs(tmpdir, tarfile, extrafiles)
266 log.info("created: %s" % tarfile)343 log.info("created: %s" % tarfile)
267 finally:344 finally:
268 call(["chmod", "-R", "u+w", tmpdir])345 call(["chmod", "-R", "u+w", tmpdir])
269 shutil.rmtree(tmpdir)346 shutil.rmtree(tmpdir)
270347
271348
272main()349if __name__ == "__main__":
350 logging.basicConfig(
351 level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
352 parser = get_option_parser()
353 args = parser.parse_args(sys.argv[1:])
354 tarfile = os.path.abspath(args.tarfile)
355 main(tarfile, args.extrafiles)
273356
=== added file 'test_collect-logs.py'
--- test_collect-logs.py 1970-01-01 00:00:00 +0000
+++ test_collect-logs.py 2016-08-23 00:10:59 +0000
@@ -0,0 +1,749 @@
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 MainTestCase(_BaseTestCase):
89
90 MOCKED = ("collect_logs", "collect_inner_logs", "bundle_logs")
91
92 def setUp(self):
93 super(MainTestCase, self).setUp()
94
95 self.orig_mkdtemp = script.mkdtemp
96 script.mkdtemp = lambda: self.tempdir
97
98 def tearDown(self):
99 script.mkdtemp = self.orig_mkdtemp
100
101 super(MainTestCase, self).tearDown()
102
103 def test_success(self):
104 """
105 main() calls collect_logs(), collect_inner_logs(), and bundle_logs().
106 """
107 tarfile = "/tmp/logs.tgz"
108 extrafiles = ["spam.py"]
109
110 script.main(tarfile, extrafiles, juju=self.juju)
111
112 script.collect_logs.assert_called_once_with(self.juju)
113 script.collect_inner_logs.assert_called_once_with(self.juju)
114 script.bundle_logs.assert_called_once_with(
115 self.tempdir, tarfile, extrafiles)
116 self.assertFalse(os.path.exists(self.tempdir))
117
118 def test_in_correct_directories(self):
119 """
120 main() calls its dependencies while in specific directories.
121 """
122 script.collect_logs.side_effect = (
123 lambda _: self.assert_cwd(self.tempdir))
124 script.collect_inner_logs.side_effect = (
125 lambda _: self.assert_cwd(self.tempdir))
126 script.bundle_logs.side_effect = lambda *a: self.assert_cwd(self.cwd)
127 tarfile = "/tmp/logs.tgz"
128 extrafiles = ["spam.py"]
129
130 script.main(tarfile, extrafiles, juju=self.juju)
131
132 def test_cleanup(self):
133 """
134 main() cleans up the temp dir it creates.
135 """
136 tarfile = "/tmp/logs.tgz"
137 extrafiles = ["spam.py"]
138
139 script.main(tarfile, extrafiles, juju=self.juju)
140
141 self.assertFalse(os.path.exists(self.tempdir))
142
143 def test_collect_logs_error(self):
144 """
145 main() doesn't handle the error when collect_logs() fails.
146
147 It still cleans up the temp dir.
148 """
149 tarfile = "/tmp/logs.tgz"
150 extrafiles = ["spam.py"]
151 script.collect_logs.side_effect = FakeError()
152
153 with self.assertRaises(FakeError):
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_not_called()
158 script.bundle_logs.assert_not_called()
159 self.assertFalse(os.path.exists(self.tempdir))
160
161 def test_collect_inner_logs_error(self):
162 """
163 main() ignores the error when collect_inner_logs() fails.
164
165 It still cleans up the temp dir.
166 """
167 tarfile = "/tmp/logs.tgz"
168 extrafiles = ["spam.py"]
169 script.collect_inner_logs.side_effect = FakeError()
170
171 script.main(tarfile, extrafiles, juju=self.juju)
172
173 script.collect_logs.assert_called_once_with(self.juju)
174 script.collect_inner_logs.assert_called_once_with(self.juju)
175 script.bundle_logs.assert_called_once_with(
176 self.tempdir, tarfile, extrafiles)
177 self.assertFalse(os.path.exists(self.tempdir))
178
179 def test_bundle_logs_error(self):
180 """
181 main() doesn't handle the error when bundle_logs() fails.
182
183 It still cleans up the temp dir.
184 """
185 tarfile = "/tmp/logs.tgz"
186 extrafiles = ["spam.py"]
187 script.bundle_logs.side_effect = FakeError()
188
189 with self.assertRaises(FakeError):
190 script.main(tarfile, extrafiles, juju=self.juju)
191
192 script.collect_logs.assert_called_once_with(self.juju)
193 script.collect_inner_logs.assert_called_once_with(self.juju)
194 script.bundle_logs.assert_called_once_with(
195 self.tempdir, tarfile, extrafiles)
196 self.assertFalse(os.path.exists(self.tempdir))
197
198
199class CollectLogsTestCase(_BaseTestCase):
200
201 MOCKED = ("get_units", "check_output", "call")
202
203 def setUp(self):
204 super(CollectLogsTestCase, self).setUp()
205
206 self.units = [
207 "landscape-server/0",
208 "postgresql/0",
209 "rabbitmq-server/0",
210 "haproxy/0",
211 ]
212 script.get_units.return_value = self.units[:]
213
214 self.mp_map_orig = script._mp_map
215 script._mp_map = lambda f, a: map(f, a)
216
217 os.chdir(self.tempdir)
218
219 def tearDown(self):
220 script._mp_map = self.mp_map_orig
221
222 super(CollectLogsTestCase, self).tearDown()
223
224 def _call_side_effect(self, cmd, env=None):
225 """Perform the side effect of calling the mocked-out call()."""
226 if cmd[0] == "tar":
227 self.assertTrue(os.path.exists(cmd[-1]))
228 return
229 self.assertEqual(env, self.juju.env)
230 self.assertEqual(cmd[0], "juju")
231 _create_file(os.path.basename(cmd[2]))
232
233 def test_success(self):
234 """
235 collect_logs() gathers "ps" output and logs from each unit.
236 """
237 script.call.side_effect = self._call_side_effect
238
239 script.collect_logs(self.juju)
240
241 script.get_units.assert_called_once_with(self.juju)
242 expected = []
243 units = self.units + ["0"]
244 # for _create_ps_output_file()
245 for unit in units:
246 cmd = "ps fauxww | sudo tee /var/log/ps-fauxww.txt"
247 expected.append(mock.call(["juju", "ssh", unit, cmd],
248 stderr=subprocess.STDOUT,
249 env=None,
250 ))
251 # for _create_log_tarball()
252 for unit in units:
253 cmd = ("sudo tar --ignore-failed-read"
254 " --exclude=/var/lib/landscape/client/package/hash-id"
255 " --exclude=/var/lib/juju/containers/juju-*-lxc-template"
256 " -czf /tmp/logs_{}.tar.gz"
257 " $(sudo sh -c \"ls -1d {} 2>/dev/null\")"
258 ).format(
259 unit.replace("/", "-") if unit != "0" else "bootstrap",
260 " ".join(["/var/log",
261 "/etc/nova",
262 "/etc/swift",
263 "/etc/neutron",
264 "/etc/apache2",
265 "/etc/haproxy",
266 "/etc/ceph",
267 "/etc/glance",
268 "/var/lib/lxc/*/rootfs/var/log",
269 "/var/lib/juju/containers",
270 "/var/lib/landscape/client",
271 ]),
272 )
273 expected.append(mock.call(["juju", "ssh", unit, cmd],
274 stderr=subprocess.STDOUT,
275 env=None,
276 ))
277 self.assertEqual(script.check_output.call_count, len(expected))
278 script.check_output.assert_has_calls(expected, any_order=True)
279 expected = []
280 # for download_log_from_unit()
281 for unit in units:
282 name = unit.replace("/", "-") if unit != "0" else "bootstrap"
283 filename = "logs_{}.tar.gz".format(name)
284 source = "{}:/tmp/{}".format(unit, filename)
285 expected.append(mock.call(["juju", "scp", source, "."], env=None))
286 expected.append(mock.call(["tar", "-C", name, "-xzf", filename]))
287 self.assertFalse(os.path.exists(filename))
288 self.assertEqual(script.call.call_count, len(expected))
289 script.call.assert_has_calls(expected, any_order=True)
290
291 def test_get_units_failure(self):
292 """
293 collect_logs() does not handle errors from get_units().
294 """
295 script.get_units.side_effect = FakeError()
296
297 with self.assertRaises(FakeError):
298 script.collect_logs(self.juju)
299
300 script.get_units.assert_called_once_with(self.juju)
301 script.check_output.assert_not_called()
302 script.call.assert_not_called()
303
304 def test_check_output_failure(self):
305 """
306 collect_logs() does not handle errors from check_output().
307 """
308 script.check_output.side_effect = [mock.DEFAULT,
309 FakeError(),
310 ]
311
312 with self.assertRaises(FakeError):
313 script.collect_logs(self.juju)
314
315 script.get_units.assert_called_once_with(self.juju)
316 self.assertEqual(script.check_output.call_count, 2)
317 script.call.assert_not_called()
318
319 def test_call_failure(self):
320 """
321 collect_logs() does not handle errors from call().
322 """
323 def call_side_effect(cmd, env=None):
324 # second use of call() for landscape-server/0
325 if script.call.call_count == 2:
326 raise FakeError()
327 # first use of call() for postgresql/0
328 if script.call.call_count == 3:
329 raise FakeError()
330 # all other uses of call() default to the normal side effect.
331 return self._call_side_effect(cmd, env=env)
332 script.call.side_effect = call_side_effect
333
334 script.collect_logs(self.juju)
335
336 script.get_units.assert_called_once_with(self.juju)
337 units = self.units + ["0"]
338 self.assertEqual(script.check_output.call_count, len(units) * 2)
339 self.assertEqual(script.call.call_count, len(units) * 2 - 1)
340 for unit in units:
341 name = unit.replace("/", "-") if unit != "0" else "bootstrap"
342 if unit == self.units[1]:
343 self.assertFalse(os.path.exists(name))
344 else:
345 self.assertTrue(os.path.exists(name))
346 filename = "logs_{}.tar.gz".format(name)
347 self.assertFalse(os.path.exists(filename))
348
349
350class CollectInnerLogsTestCase(_BaseTestCase):
351
352 MOCKED = ("get_units", "check_output", "call", "check_call")
353
354 def setUp(self):
355 super(CollectInnerLogsTestCase, self).setUp()
356
357 self.units = [
358 "landscape-server/0",
359 "postgresql/0",
360 "rabbitmq-server/0",
361 "haproxy/0",
362 ]
363 script.get_units.return_value = self.units[:]
364
365 os.chdir(self.tempdir)
366
367 def assert_clean(self):
368 """Ensure that collect_inner_logs cleaned up after itself."""
369 self.assert_cwd(self.tempdir)
370 self.assertFalse(os.path.exists("inner-logs.tar.gz"))
371
372 def test_success(self):
373 """
374 collect_inner_logs() finds the inner model and runs collect-logs
375 inside it. The resulting tarball is downloaded, extracted, and
376 deleted.
377 """
378 def check_call_side_effect(cmdenv, env=None):
379 self.assertEqual(env, self.juju.env)
380 if script.check_call.call_count == 2:
381 self.assert_cwd(self.tempdir)
382 self._create_tempfile("inner-logs.tar.gz")
383 elif script.check_call.call_count == 3:
384 cwd = os.path.join(self.tempdir, "landscape-0-inner-logs")
385 self.assert_cwd(cwd)
386 return None
387 script.check_call.side_effect = check_call_side_effect
388
389 script.collect_inner_logs(self.juju)
390
391 # Check get_units() calls.
392 script.get_units.assert_called_once_with(self.juju)
393 # Check check_output() calls.
394 expected = []
395 cmd = ("sudo JUJU_HOME=/var/lib/landscape/juju-homes/"
396 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
397 " juju set-env proxy-ssh=false")
398 expected.append(mock.call(["juju", "ssh", "landscape-server/0", cmd],
399 stderr=subprocess.STDOUT,
400 env=None))
401 cmd = ("sudo JUJU_HOME=/var/lib/landscape/juju-homes/"
402 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
403 " juju status")
404 expected.append(mock.call(["juju", "ssh", "landscape-server/0", cmd],
405 env=None))
406 self.assertEqual(script.check_output.call_count, len(expected))
407 script.check_output.assert_has_calls(expected, any_order=True)
408 # Check call() calls.
409 expected = [
410 mock.call(["juju", "scp",
411 os.path.join(os.path.dirname(__file__), "collect-logs"),
412 "landscape-server/0:/tmp/collect-logs",
413 ], env=None),
414 mock.call(["juju", "ssh",
415 "landscape-server/0",
416 "sudo rm -rf /tmp/inner-logs.tar.gz",
417 ], env=None),
418 ]
419 self.assertEqual(script.call.call_count, len(expected))
420 script.call.assert_has_calls(expected, any_order=True)
421 # Check check_call() calls.
422 cmd = ("sudo -u landscape"
423 " JUJU_HOME=/var/lib/landscape/juju-homes/"
424 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
425 " /tmp/collect-logs /tmp/inner-logs.tar.gz")
426 expected = [
427 mock.call(["juju", "ssh", "landscape-server/0", cmd], env=None),
428 mock.call(["juju", "scp",
429 "landscape-server/0:/tmp/inner-logs.tar.gz",
430 ".",
431 ], env=None),
432 mock.call(["tar", "-zxf", self.tempdir + "/inner-logs.tar.gz"]),
433 ]
434 self.assertEqual(script.check_call.call_count, len(expected))
435 script.check_call.assert_has_calls(expected, any_order=True)
436 self.assert_clean()
437
438 def test_with_legacy_landscape_unit(self):
439 """
440 collect_inner_logs() correctly supports legacy landscape installations.
441 """
442 self.units[0] = "landscape/0"
443 script.get_units.return_value = self.units[:]
444
445 script.collect_inner_logs(self.juju)
446
447 expected = []
448 cmd = ("sudo JUJU_HOME=/var/lib/landscape/juju-homes/"
449 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
450 " juju set-env proxy-ssh=false")
451 expected.append(mock.call(["juju", "ssh", "landscape/0", cmd],
452 stderr=subprocess.STDOUT,
453 env=None))
454 cmd = ("sudo JUJU_HOME=/var/lib/landscape/juju-homes/"
455 "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
456 " juju status")
457 expected.append(mock.call(["juju", "ssh", "landscape/0", cmd],
458 env=None))
459 self.assertEqual(script.check_output.call_count, len(expected))
460 script.check_output.assert_has_calls(expected, any_order=True)
461 self.assert_clean()
462
463 def test_no_units(self):
464 """
465 collect_inner_logs() is a noop if no units are found.
466 """
467 script.get_units.return_value = []
468
469 script.collect_inner_logs(self.juju)
470
471 script.get_units.assert_called_once_with(self.juju)
472 script.check_output.assert_not_called()
473 script.call.assert_not_called()
474 script.check_call.assert_not_called()
475 self.assert_clean()
476
477 def test_no_landscape_server_unit(self):
478 """
479 collect_inner_logs() is a noop if the landscape unit isn't found.
480 """
481 del self.units[0]
482 script.get_units.return_value = self.units[:]
483
484 script.collect_inner_logs(self.juju)
485
486 script.get_units.assert_called_once_with(self.juju)
487 script.check_output.assert_not_called()
488 script.call.assert_not_called()
489 script.check_call.assert_not_called()
490 self.assert_clean()
491
492 def test_get_units_failure(self):
493 """
494 collect_inner_logs() does not handle errors from get_units().
495 """
496 script.get_units.side_effect = FakeError()
497
498 with self.assertRaises(FakeError):
499 script.collect_inner_logs(self.juju)
500
501 self.assertEqual(script.get_units.call_count, 1)
502 script.check_output.assert_not_called()
503 script.call.assert_not_called()
504 script.check_call.assert_not_called()
505 self.assert_cwd(self.tempdir)
506 self.assert_clean()
507
508 def test_check_output_failure_1(self):
509 """
510 collect_inner_logs() does not handle non-CalledProcessError
511 errors when disabling the SSH proxy.
512 """
513 script.check_output.side_effect = FakeError()
514
515 with self.assertRaises(FakeError):
516 script.collect_inner_logs(self.juju)
517
518 self.assertEqual(script.get_units.call_count, 1)
519 self.assertEqual(script.check_output.call_count, 1)
520 script.call.assert_not_called()
521 script.check_call.assert_not_called()
522 self.assert_cwd(self.tempdir)
523 self.assert_clean()
524
525 def test_check_output_failure_2(self):
526 """
527 collect_inner_logs() does not handle non-CalledProcessError
528 errors when verifying the inner model is bootstrapped.
529 """
530 script.check_output.side_effect = [None,
531 FakeError(),
532 ]
533
534 with self.assertRaises(FakeError):
535 script.collect_inner_logs(self.juju)
536
537 self.assertEqual(script.get_units.call_count, 1)
538 self.assertEqual(script.check_output.call_count, 2)
539 script.call.assert_not_called()
540 script.check_call.assert_not_called()
541 self.assert_cwd(self.tempdir)
542 self.assert_clean()
543
544 def test_call_failure_1(self):
545 """
546 collect_inner_logs() does not handle errors from call().
547 """
548 script.call.side_effect = FakeError()
549
550 with self.assertRaises(FakeError):
551 script.collect_inner_logs(self.juju)
552
553 self.assertEqual(script.get_units.call_count, 1)
554 self.assertEqual(script.check_output.call_count, 2)
555 self.assertEqual(script.call.call_count, 1)
556 script.check_call.assert_not_called()
557 self.assert_cwd(self.tempdir)
558 self.assert_clean()
559
560 def test_call_failure_2(self):
561 """
562 collect_inner_logs() does not handle errors from call().
563 """
564 script.call.side_effect = [None,
565 FakeError(),
566 ]
567
568 with self.assertRaises(FakeError):
569 script.collect_inner_logs(self.juju)
570
571 self.assertEqual(script.get_units.call_count, 1)
572 self.assertEqual(script.check_output.call_count, 2)
573 self.assertEqual(script.call.call_count, 2)
574 script.check_call.assert_not_called()
575 self.assert_clean()
576
577 def test_check_call_failure_1(self):
578 """
579 collect_inner_logs() does not handle errors when running
580 collect-logs in the inner model.
581 """
582 script.check_call.side_effect = FakeError()
583
584 with self.assertRaises(FakeError):
585 script.collect_inner_logs(self.juju)
586
587 self.assertEqual(script.get_units.call_count, 1)
588 self.assertEqual(script.check_output.call_count, 2)
589 self.assertEqual(script.call.call_count, 2)
590 self.assertEqual(script.check_call.call_count, 1)
591 self.assert_clean()
592
593 def test_check_call_failure_2(self):
594 """
595 collect_inner_logs() does not handle errors downloading the
596 collected logs from the inner model.
597
598 It does clean up, however.
599 """
600 script.check_call.side_effect = [None,
601 FakeError(),
602 ]
603
604 with self.assertRaises(FakeError):
605 script.collect_inner_logs(self.juju)
606
607 self.assertEqual(script.get_units.call_count, 1)
608 self.assertEqual(script.check_output.call_count, 2)
609 self.assertEqual(script.call.call_count, 2)
610 self.assertEqual(script.check_call.call_count, 2)
611 self.assert_clean()
612
613 def test_check_call_failure_3(self):
614 """
615 collect_inner_logs() does not handle errors when unpacking
616 the inner model tarball.
617
618 It does clean up, however.
619 """
620 def check_call_side_effect(cmd, env=None):
621 self.assertEqual(env, self.juju.env)
622 if script.check_call.call_count == 1:
623 return None
624 if script.check_call.call_count == 2:
625 self._create_tempfile("inner-logs.tar.gz")
626 return None
627 raise FakeError()
628 script.check_call.side_effect = check_call_side_effect
629
630 with self.assertRaises(FakeError):
631 script.collect_inner_logs(self.juju)
632
633 self.assertEqual(script.get_units.call_count, 1)
634 self.assertEqual(script.check_output.call_count, 2)
635 self.assertEqual(script.call.call_count, 2)
636 self.assertEqual(script.check_call.call_count, 3)
637 self.assert_clean()
638
639
640class BundleLogsTestCase(_BaseTestCase):
641
642 MOCKED = ("call",)
643
644 def setUp(self):
645 """
646 bundle_logs() creates a tarball holding the files in the tempdir.
647 """
648 super(BundleLogsTestCase, self).setUp()
649
650 os.chdir(self.tempdir)
651
652 self._create_tempfile("bootstrap/var/log/syslog")
653 self._create_tempfile("bootstrap/var/log/juju/all-machines.log")
654 self._create_tempfile(
655 "bootstrap/var/lib/lxc/deadbeef/rootfs/var/log/syslog")
656 self._create_tempfile("bootstrap/var/lib/juju/containers")
657 self._create_tempfile("landscape-server-0/var/log/syslog")
658 self._create_tempfile("postgresql-0/var/log/syslog")
659 self._create_tempfile("rabbitmq-server-0/var/log/syslog")
660 self._create_tempfile("haproxy-0/var/log/syslog")
661 self._create_tempfile(
662 "landscape-0-inner-logs/bootstrap/var/log/syslog")
663
664 self.extrafile = os.path.join(self.cwd, "spam.txt")
665 _create_file(self.extrafile)
666
667 def test_success_with_extra(self):
668 """
669 bundle_logs() works if extra files are included.
670 """
671 tarfile = "/tmp/logs.tgz"
672 extrafiles = [self.extrafile]
673
674 script.bundle_logs(self.tempdir, tarfile, extrafiles)
675
676 script.call.assert_called_once_with(
677 ["tar",
678 "czf", tarfile,
679 "--transform", "s,{}/,,".format(self.tempdir[1:]),
680 os.path.join(self.tempdir, "bootstrap"),
681 os.path.join(self.tempdir, "haproxy-0"),
682 os.path.join(self.tempdir, "landscape-0-inner-logs"),
683 os.path.join(self.tempdir, "landscape-server-0"),
684 os.path.join(self.tempdir, "postgresql-0"),
685 os.path.join(self.tempdir, "rabbitmq-server-0"),
686 self.extrafile,
687 ],
688 )
689
690 def test_success_without_extra(self):
691 """
692 bundle_logs() works if there aren't any extra files.
693 """
694 tarfile = "/tmp/logs.tgz"
695
696 script.bundle_logs(self.tempdir, tarfile)
697
698 script.call.assert_called_once_with(
699 ["tar",
700 "czf", tarfile,
701 "--transform", "s,{}/,,".format(self.tempdir[1:]),
702 os.path.join(self.tempdir, "bootstrap"),
703 os.path.join(self.tempdir, "haproxy-0"),
704 os.path.join(self.tempdir, "landscape-0-inner-logs"),
705 os.path.join(self.tempdir, "landscape-server-0"),
706 os.path.join(self.tempdir, "postgresql-0"),
707 os.path.join(self.tempdir, "rabbitmq-server-0"),
708 ],
709 )
710
711 def test_success_no_files(self):
712 """
713 bundle_logs() works even when the temp dir is empty.
714 """
715 for filename in os.listdir(self.tempdir):
716 shutil.rmtree(os.path.join(self.tempdir, filename))
717 tarfile = "/tmp/logs.tgz"
718
719 script.bundle_logs(self.tempdir, tarfile)
720
721 script.call.assert_called_once_with(
722 ["tar",
723 "czf", tarfile,
724 "--transform", "s,{}/,,".format(self.tempdir[1:]),
725 ],
726 )
727
728 def test_call_failure(self):
729 """
730 bundle_logs() does not handle errors when creating the tarball.
731 """
732 script.call.side_effect = FakeError()
733 tarfile = "/tmp/logs.tgz"
734
735 with self.assertRaises(FakeError):
736 script.bundle_logs(self.tempdir, tarfile)
737
738 script.call.assert_called_once_with(
739 ["tar",
740 "czf", tarfile,
741 "--transform", "s,{}/,,".format(self.tempdir[1:]),
742 os.path.join(self.tempdir, "bootstrap"),
743 os.path.join(self.tempdir, "haproxy-0"),
744 os.path.join(self.tempdir, "landscape-0-inner-logs"),
745 os.path.join(self.tempdir, "landscape-server-0"),
746 os.path.join(self.tempdir, "postgresql-0"),
747 os.path.join(self.tempdir, "rabbitmq-server-0"),
748 ],
749 )

Subscribers

People subscribed via source and target branches

to all changes: