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

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

Commit message

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

Description of the change

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

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

+1

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

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== removed file 'Makefile'
2--- Makefile 2016-09-16 15:31:13 +0000
3+++ Makefile 1970-01-01 00:00:00 +0000
4@@ -1,7 +0,0 @@
5-.PHONY: test
6-test:
7- python -m unittest test_collect-logs
8-
9-
10-.PHONY: ci-test
11-ci-test: test
12
13=== removed file 'collect-logs'
14--- collect-logs 2017-01-05 17:33:16 +0000
15+++ collect-logs 1970-01-01 00:00:00 +0000
16@@ -1,613 +0,0 @@
17-#!/usr/bin/python
18-
19-from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
20-import errno
21-from functools import partial
22-import logging
23-import multiprocessing
24-import os
25-import shutil
26-from subprocess import (
27- CalledProcessError, check_call, check_output, call, STDOUT)
28-import sys
29-from tempfile import mkdtemp
30-
31-import yaml
32-
33-
34-log = logging.getLogger("collect-logs")
35-PRG = os.path.abspath(__file__)
36-LOGS = [
37- # Basics
38- "/var/log",
39- "/etc/hosts",
40- "/etc/network",
41- # for the landscape client
42- "/var/lib/landscape/client",
43- # for the landscape server charm
44- "/etc/apache2",
45- "/etc/haproxy",
46- # XXX This should be handled by the per-unit logs.
47- "/var/lib/lxc/*/rootfs/var/log",
48- # logs and configs for juju 1 using LXC (not LXD)
49- "/var/lib/juju/containers",
50- # for openstack
51- "/etc/nova",
52- "/etc/swift",
53- "/etc/neutron",
54- "/etc/ceph",
55- "/etc/glance",
56- ]
57-EXCLUDED = ["/var/lib/landscape/client/package/hash-id",
58- "/var/lib/juju/containers/juju-*-lxc-template"]
59-LANDSCAPE_JUJU_HOME = "/var/lib/landscape/juju-homes"
60-
61-JUJU1 = "juju"
62-# XXX This is going to break once juju-2.1 happens.
63-# See https://bugs.launchpad.net/juju-core/+bug/1613864.
64-JUJU2 = "juju-2.1"
65-JUJU = JUJU1
66-
67-DEFAULT_MODEL = object()
68-
69-VERBOSE = False
70-
71-
72-if VERBOSE:
73- def call(args, env=None, _call=call):
74- print(" running {!r}".format(" ".join(args)))
75- return _call(args, env=env)
76-
77- def check_call(args, env=None, _check_call=check_call):
78- print(" running {!r}".format(" ".join(args)))
79- return _check_call(args, env=env)
80-
81- def check_output(args, stderr=None, env=None, _check_output=check_output):
82- print(" running {!r}".format(" ".join(args)))
83- return _check_output(args, stderr=stderr, env=env)
84-
85-
86-class Juju(object):
87- """A wrapper around a juju binary."""
88-
89- def __init__(self, binary_path=None, model=None, cfgdir=None, sudo=None):
90- if binary_path is None:
91- binary_path = JUJU
92- if model is DEFAULT_MODEL:
93- model = None
94-
95- self.binary_path = binary_path
96- self.model = model
97- self.cfgdir = cfgdir
98- self.sudo = sudo
99-
100- if binary_path == JUJU1:
101- self.envvar = "JUJU_HOME"
102- else:
103- self.envvar = "JUJU_DATA"
104-
105- self.env = None
106- if cfgdir is not None:
107- self.env = dict(os.environ, **{self.envvar: cfgdir})
108-
109- def __repr__(self):
110- args = ", ".join("{}={!r}".format(name, getattr(self, name))
111- for name in ("binary_path", "model", "cfgdir"))
112- return "{}({})".format(self.__class__.__name__, args)
113-
114- def __eq__(self, other):
115- if self.binary_path != other.binary_path:
116- return False
117- if self.model != other.model:
118- return False
119- if self.cfgdir != other.cfgdir:
120- return False
121- if self.sudo != other.sudo:
122- return False
123- return True
124-
125- def __ne__(self, other):
126- return not(self == other)
127-
128- @property
129- def envstr(self):
130- if not self.cfgdir:
131- return ""
132- else:
133- return "{}={}".format(self.envvar, self.cfgdir)
134-
135- def status_args(self):
136- """Return the subprocess.* args for a status command."""
137- args = self._resolve("status", "--format=yaml")
138- return args
139-
140- def format_status(self):
141- """Return the formatted juju status command."""
142- args = self.status_args()
143- return self._format(args)
144-
145- def set_model_config_args(self, key, value):
146- item = "{}={}".format(key, value)
147- if self.binary_path == JUJU1:
148- args = self._resolve("set-env", item)
149- else:
150- args = self._resolve("model-config", item)
151- return args
152-
153- def format_set_model_config(self, key, value):
154- """Return the formatted model config command."""
155- args = self.set_model_config_args(key, value)
156- return self._format(args)
157-
158- def ssh_args(self, unit, cmd):
159- """Return the subprocess.* args for an SSH command."""
160- return self._resolve("ssh", unit, cmd)
161-
162- def pull_args(self, unit, source, target="."):
163- """Return the subprocess.* args for an SCP command."""
164- source = "{}:{}".format(unit, source)
165- return self._resolve("scp", source, target)
166-
167- def push_args(self, unit, source, target):
168- """Return the subprocess.* args for an SCP command."""
169- target = "{}:{}".format(unit, target)
170- return self._resolve("scp", source, target)
171-
172- def format(self, cmd, *subargs):
173- """Return the formatted command.
174-
175- "sudo" and the juju config dir env var are set if appropriate.
176- """
177- args = [cmd]
178- args.extend(subargs)
179- return self._format(args)
180-
181- def _format(self, args):
182- """Return the formatted args.
183-
184- "sudo" and the juju config dir env var are set if appropriate.
185- """
186- if self.cfgdir:
187- args.insert(0, self.envstr)
188- if self.sudo == "" or self.sudo is True:
189- args.insert(0, "sudo")
190- elif self.sudo:
191- args.insert(0, "sudo -u {}".format(self.sudo))
192- return " ".join(args)
193-
194- def _resolve(self, sub, *subargs):
195- """Return the subprocess.* args for the juju subcommand."""
196- args = [self.binary_path, sub]
197- if self.model:
198- if self.binary_path == JUJU1:
199- args.append("-e")
200- else:
201- args.append("-m")
202- args.append(self.model)
203- args.extend(subargs)
204- return args
205-
206-
207-def format_collect_logs(juju, script, target, inner=True):
208- """Return the formatted command for the collect_logs script."""
209- if inner:
210- args = [script, "--inner"]
211- else:
212- args = [script]
213- args.extend(["--juju", juju.binary_path])
214- if juju.model and juju.model is not DEFAULT_MODEL:
215- args.extend(["--model", juju.model])
216- if juju.cfgdir:
217- args.extend(["--cfgdir", juju.cfgdir])
218- args.append(target)
219- return juju._format(args)
220-
221-
222-def juju_status(juju):
223- """Return a juju status structure."""
224- output = check_output(juju.status_args(), env=juju.env)
225- output = output.decode("utf-8").strip()
226- return yaml.load(output)
227-
228-
229-def get_units(juju, status=None):
230- """Return a list with all units."""
231- if status is None:
232- status = juju_status(juju)
233- units = []
234- if "services" in status:
235- applications = status["services"]
236- else:
237- applications = status["applications"]
238- for application in applications:
239- # skip subordinate charms
240- if "subordinate-to" in applications[application].keys():
241- continue
242- if "units" in applications[application]:
243- units.extend(applications[application]["units"].keys())
244- if len(units) == 0:
245- sys.exit("ERROR, no units found. Make sure the right juju environment"
246- "is set.")
247- return units
248-
249-
250-def _create_ps_output_file(juju, unit):
251- """List running processes and redirect them to a file."""
252- log.info("Collecting ps output on unit {}".format(unit))
253- ps_cmd = "ps fauxww | sudo tee /var/log/ps-fauxww.txt"
254- args = juju.ssh_args(unit, ps_cmd)
255- try:
256- check_output(args, stderr=STDOUT, env=juju.env)
257- except CalledProcessError as e:
258- log.warning(
259- "Failed to collect running processes on unit {}".format(unit))
260- log.warning(e.output)
261- log.warning(e.returncode)
262-
263-
264-def _create_log_tarball(juju, unit):
265- log.info("Creating tarball on unit {}".format(unit))
266- exclude = " ".join(["--exclude=%s" % x for x in EXCLUDED])
267- logs = "$(sudo sh -c \"ls -1d %s 2>/dev/null\")" % " ".join(LOGS)
268- # --ignore-failed-read avoids failure for unreadable files (not for files
269- # being written)
270- tar_cmd = "sudo tar --ignore-failed-read"
271- logsuffix = unit.replace("/", "-")
272- if unit == "0":
273- logsuffix = "bootstrap"
274- cmd = "{} {} -cf /tmp/logs_{}.tar {}".format(
275- tar_cmd, exclude, logsuffix, logs)
276- args = juju.ssh_args(unit, cmd)
277- ATTEMPTS = 5
278- for i in range(ATTEMPTS):
279- log.info("...attempt {} of {}".format(i+1, ATTEMPTS))
280- try:
281- check_output(args, stderr=STDOUT, env=juju.env)
282- except CalledProcessError as e:
283- # Note: tar command returns 1 for everything it considers a
284- # warning, 2 for fatal errors. Since we are backing up
285- # log files that are actively being written, or part of a live
286- # system, logging and ignoring such warnings (return code 1) is
287- # what we can do now.
288- # Everything else we might retry as usual.
289- if e.returncode == 1:
290- log.warning(
291- "tar returned 1, proceeding anyway: {}".format(e.output))
292- break
293- log.warning(
294- "Failed to archive log files on unit {}".format(unit))
295- log.warning(e.output)
296- log.warning(e.returncode)
297- if i < 4:
298- log.warning("...retrying...")
299- cmd = "{} {} --update -f /tmp/logs_{}.tar {}".format(
300- tar_cmd, exclude, logsuffix, logs)
301- args = juju.ssh_args(unit, cmd)
302- else:
303- # The command succeeded so we stop the retry loop.
304- break
305- else:
306- # Don't bother compressing.
307- log.warning("...{} attempts failed; giving up".format(ATTEMPTS))
308- return
309- cmd = "sudo gzip -f /tmp/logs_{}.tar".format(logsuffix)
310- args = juju.ssh_args(unit, cmd)
311- try:
312- check_output(args, stderr=STDOUT, env=juju.env)
313- except CalledProcessError as e:
314- log.warning(
315- "Failed to create remote log tarball on unit {}".format(unit))
316- log.warning(e.output)
317- log.warning(e.returncode)
318-
319-
320-def download_log_from_unit(juju, unit):
321- log.info("Downloading tarball from unit %s" % unit)
322- unit_filename = unit.replace("/", "-")
323- if unit == "0":
324- unit_filename = "bootstrap"
325- remote_filename = "logs_%s.tar.gz" % unit_filename
326- try:
327- args = juju.pull_args(unit, "/tmp/" + remote_filename)
328- call(args, env=juju.env)
329- os.mkdir(unit_filename)
330- args = ["tar", "-C", unit_filename, "-xzf", remote_filename]
331- call(args)
332- os.unlink(remote_filename)
333- except:
334- log.warning("error collecting logs from %s, skipping" % unit)
335- finally:
336- if os.path.exists(remote_filename):
337- os.unlink(remote_filename)
338-
339-
340-def collect_logs(juju):
341- """
342- Remotely, on each unit, create a tarball with the requested log files
343- or directories, if they exist. If a requested log does not exist on a
344- particular unit, it's ignored.
345- After each tarball is created, it's downloaded to the current directory
346- and expanded, and the tarball is then deleted.
347- """
348- units = get_units(juju)
349- # include bootstrap
350- units.append("0")
351-
352- log.info("Collecting running processes for all units including bootstrap")
353- map(partial(_create_ps_output_file, juju), units)
354-
355- log.info("Creating remote tarball in parallel for units %s" % (
356- ",".join(units)))
357- map(partial(_create_log_tarball, juju), units)
358- log.info("Downloading logs from units")
359-
360- _mp_map(partial(download_log_from_unit, juju), units)
361-
362-
363-def _mp_map(func, args):
364- pool = multiprocessing.Pool(processes=4)
365- pool.map(func, args)
366-
367-
368-def get_landscape_unit(units):
369- """Return the landscape unit among the units list."""
370- units = [
371- unit for unit in units if unit.startswith("landscape-server/") or
372- unit.startswith("landscape/")]
373- if len(units) == 0:
374- return None
375- else:
376- # XXX we don't yet support multiple landscape units. We would have to
377- # find out which one has the juju home
378- return units[0]
379-
380-
381-def get_inner_model(version, inner_model=DEFAULT_MODEL):
382- """Return a best-effort guess at the inner model name."""
383- if inner_model is not DEFAULT_MODEL:
384- return inner_model
385- if version == JUJU1:
386- return None
387- # We assume that this is a Landscape-bootstrapped controller.
388- return "controller"
389-
390-
391-def disable_inner_ssh_proxy(juju, landscape_unit, inner_model=DEFAULT_MODEL):
392- """
393- Workaround for #1607076: disable the proxy-ssh juju environment setting
394- for the inner cloud so we can juju ssh into it.
395- """
396- log.info("Disabling proxy-ssh in the juju environment on "
397- "{}".format(landscape_unit))
398-
399- cfgdir = "{0}/`sudo ls -rt {0}/ | tail -1`".format(LANDSCAPE_JUJU_HOME)
400- key = "proxy-ssh"
401- value = "false"
402-
403- # Try Juju 2.
404- model2 = get_inner_model(JUJU2, inner_model)
405- inner2 = Juju(JUJU2, model=model2, cfgdir=cfgdir, sudo=True)
406- cmd2 = inner2.format_set_model_config(key, value)
407- args2 = juju.ssh_args(landscape_unit, cmd2)
408- try:
409- check_output(args2, stderr=STDOUT, env=juju.env)
410- except CalledProcessError as e:
411- log.warning("Couldn't disable proxy-ssh in the inner environment "
412- "using Juju 2, attempting Juju 1.")
413- log.warning("Error was:\n{}".format(e.output))
414- else:
415- return
416-
417- # Try Juju 1.
418- model1 = get_inner_model(JUJU1, inner_model)
419- inner1 = Juju(JUJU1, model=model1, cfgdir=cfgdir, sudo=True)
420- cmd1 = inner1.format_set_model_config(key, value)
421- args1 = juju.ssh_args(landscape_unit, cmd1)
422- try:
423- check_output(args1, stderr=STDOUT, env=juju.env)
424- except CalledProcessError as e:
425- log.warning("Couldn't disable proxy-ssh in the inner environment "
426- "using Juju 1, collecting inner logs might fail.")
427- log.warning("Error was:\n{}".format(e.output))
428-
429-
430-
431-def find_inner_juju(juju, landscape_unit, inner_model=DEFAULT_MODEL):
432- """Return the juju dir and binary path, if any."""
433- # Identify the most recent juju "home" that landscape is using.
434- cmd = "sudo ls -rt {}/".format(LANDSCAPE_JUJU_HOME)
435- args = juju.ssh_args(landscape_unit, cmd)
436- try:
437- output = check_output(args, env=juju.env).strip()
438- except CalledProcessError:
439- return None
440- if output.startswith("sudo: "):
441- _, _, output = output.partition("\r\n")
442- indices = [m for m in output.split() if m and m.isdigit()]
443- if not indices:
444- return None
445- index = indices[-1]
446- juju_dir = os.path.join(LANDSCAPE_JUJU_HOME, index)
447-
448- # Try Juju 2.
449- model2 = get_inner_model(JUJU2, inner_model)
450- inner2 = Juju(JUJU2, model=model2, cfgdir=juju_dir, sudo="")
451- cmd2 = inner2.format_status()
452- args2 = juju.ssh_args(landscape_unit, cmd2)
453- if call(args2, env=juju.env) == 0:
454- log.info("using Juju 2 for inner model")
455- return inner2
456-
457- # Try Juju 1.
458- model1 = get_inner_model(JUJU1, inner_model)
459- inner1 = Juju(JUJU1, model=model1, cfgdir=juju_dir, sudo="landscape")
460- cmd1 = inner1.format_status()
461- args1 = juju.ssh_args(landscape_unit, cmd1)
462- if call(args1, env=juju.env) == 0:
463- log.info("using Juju 1 for inner model")
464- return inner1
465-
466- # We didn't find an inner model.
467- return None
468-
469-
470-def collect_inner_logs(juju, inner_model=DEFAULT_MODEL):
471- """Collect logs from an inner landscape[-server]/0 unit."""
472- log.info("Collecting logs on inner environment")
473- units = get_units(juju)
474- landscape_unit = get_landscape_unit(units)
475- if not landscape_unit:
476- log.info("No landscape[-server]/N found, skipping")
477- return
478- log.info("Found landscape unit {}".format(landscape_unit))
479-
480- disable_inner_ssh_proxy(juju, landscape_unit, inner_model)
481-
482- # Look up the inner model.
483- inner_juju = find_inner_juju(juju, landscape_unit, inner_model)
484- if inner_juju is None:
485- log.info(("No active inner environment found on {}, skipping"
486- ).format(landscape_unit))
487- return
488-
489- # Prepare to get the logs from the inner model.
490- collect_logs = "/tmp/collect-logs"
491- args = juju.push_args(landscape_unit, PRG, collect_logs)
492- call(args, env=juju.env)
493- filename = "inner-logs.tar.gz"
494- inner_filename = os.path.join("/tmp", filename)
495- args = juju.ssh_args(landscape_unit, "sudo rm -rf " + inner_filename)
496- call(args, env=juju.env)
497-
498- # Collect the logs for the inner model.
499- cmd = format_collect_logs(inner_juju, collect_logs, inner_filename)
500- args = juju.ssh_args(landscape_unit, cmd)
501- check_call(args, env=juju.env)
502-
503- # Copy the inner logs into a local directory.
504- log.info("Copying inner environment back")
505- cwd = os.getcwd()
506- target = os.path.join(cwd, filename)
507- args = juju.pull_args(landscape_unit, inner_filename, target)
508- check_call(args, env=juju.env)
509- try:
510- inner_dir = "landscape-0-inner-logs"
511- os.mkdir(inner_dir)
512- os.chdir(inner_dir)
513- try:
514- check_call(["tar", "-zxf", os.path.join(cwd, filename)])
515- finally:
516- os.chdir(cwd)
517- finally:
518- try:
519- os.remove(target)
520- except OSError as e:
521- if e.errno != errno.ENOENT:
522- log.warning(
523- "failed to remove inner logs tarball: {}".format(e))
524-
525-
526-def bundle_logs(tmpdir, tarfile, extrafiles=[]):
527- """
528- Create a tarball with the directories under tmpdir and the
529- specified extra files. The tar command is executed outside
530- of tmpdir, so we need to a) use an absolute path that
531- includes tmpdir; and b) strip the tmpdir so it's not part of
532- the tarball.
533- We don't want absolute paths for extra files because otherwise
534- this path would be inside the tarball.
535-
536- This allows you to have a tarball where this:
537- /tmp/tmpdir/foo
538- /tmp/tmpdir/bar
539- /home/ubuntu/data/log
540-
541- Becomes this inside the tarball:
542- foo
543- bar
544- data/log
545-
546- If collect-logs is run with CWD=/home/ubuntu and given data/log as
547- the extra file.
548- """
549- args = ["tar", "czf", tarfile]
550- # get rid of the tmpdir prefix
551- args.extend(["--transform", "s,{}/,,".format(tmpdir[1:])])
552- # need absolute paths since tmpdir isn't the cwd
553- args.extend(os.path.join(tmpdir, d) for d in sorted(os.listdir(tmpdir)))
554- if extrafiles:
555- args.extend(extrafiles)
556- call(args)
557-
558-
559-def get_juju(binary_path, model=DEFAULT_MODEL, cfgdir=None, inner=False):
560- """Return a Juju for the provided info."""
561- if model is DEFAULT_MODEL and inner and binary_path != JUJU1:
562- # We assume that this is a Landscape-bootstrapped controller.
563- model = "controller"
564- return Juju(binary_path, model=model, cfgdir=cfgdir)
565-
566-
567-def get_option_parser():
568- description = ("Collect logs from current juju environment and, if an "
569- "inner autopilot cloud is detected, include that.")
570- parser = ArgumentParser(description=description,
571- formatter_class=ArgumentDefaultsHelpFormatter)
572- parser.add_argument("--inner", action="store_true", default=False,
573- help="Collect logs for an inner model.")
574- parser.add_argument("--juju", default=JUJU2,
575- help="The Juju binary to use.")
576- parser.add_argument("--model", default=DEFAULT_MODEL,
577- help="The Juju model to use.")
578- parser.add_argument("--inner-model", default=DEFAULT_MODEL,
579- help="The Juju model to use for the inner juju.")
580- parser.add_argument("--cfgdir",
581- help="The Juju config dir to use.")
582- parser.add_argument("tarfile", help="Full path to tarfile to create.")
583- parser.add_argument("extrafiles", help="Optional full path to extra "
584- "logfiles to include, space separated", nargs="*")
585- return parser
586-
587-
588-def main(tarfile, extrafiles, juju=None, inner_model=DEFAULT_MODEL,
589- inner=False):
590- if juju is None:
591- juju = Juju()
592-
593- # we need the absolute path because we will be changing
594- # the cwd
595- tmpdir = mkdtemp()
596- cwd = os.getcwd()
597- # logs are collected inside a temporary directory
598- os.chdir(tmpdir)
599- try:
600- collect_logs(juju)
601- if not inner:
602- try:
603- collect_inner_logs(juju, inner_model)
604- except:
605- log.warning("Collecting inner logs failed, continuing")
606- # we create the final tarball outside of tmpdir to we can
607- # add the extrafiles to the tarball root
608- os.chdir(cwd)
609- bundle_logs(tmpdir, tarfile, extrafiles)
610- log.info("created: %s" % tarfile)
611- finally:
612- call(["chmod", "-R", "u+w", tmpdir])
613- shutil.rmtree(tmpdir)
614-
615-
616-if __name__ == "__main__":
617- logging.basicConfig(
618- level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
619- parser = get_option_parser()
620- args = parser.parse_args(sys.argv[1:])
621- tarfile = os.path.abspath(args.tarfile)
622- juju = get_juju(args.juju, args.model, args.cfgdir, args.inner)
623- if args.inner:
624- log.info("# start inner ##############################")
625- try:
626- main(tarfile, args.extrafiles, juju, args.inner_model, args.inner)
627- finally:
628- if args.inner:
629- log.info("# end inner ################################")
630
631=== removed file 'test_collect-logs.py'
632--- test_collect-logs.py 2017-01-05 17:33:16 +0000
633+++ test_collect-logs.py 1970-01-01 00:00:00 +0000
634@@ -1,1063 +0,0 @@
635-# Copyright 2016 Canonical Limited. All rights reserved.
636-
637-# To run: "python -m unittest test_collect-logs"
638-
639-import errno
640-import os
641-import os.path
642-import shutil
643-import subprocess
644-import sys
645-import tempfile
646-from unittest import TestCase
647-
648-import mock
649-
650-
651-__file__ = os.path.abspath(__file__)
652-
653-script = type(sys)("collect-logs")
654-script.__file__ = os.path.abspath("collect-logs")
655-execfile("collect-logs", script.__dict__)
656-
657-
658-class FakeError(Exception):
659- """A specific error for which to check."""
660-
661-
662-def _create_file(filename, data=None):
663- """Create (or re-create) the identified file.
664-
665- If data is provided, it is written to the file. Otherwise it
666- will be empty.
667-
668- The file's directory is created if necessary.
669- """
670- dirname = os.path.dirname(os.path.abspath(filename))
671- try:
672- os.makedirs(dirname)
673- except OSError as e:
674- if e.errno != errno.EEXIST:
675- raise
676-
677- with open(filename, "w") as file:
678- if data:
679- file.write()
680-
681-
682-class _BaseTestCase(TestCase):
683-
684- MOCKED = None
685-
686- def setUp(self):
687- super(_BaseTestCase, self).setUp()
688-
689- self.orig_cwd = os.getcwd()
690- self.cwd = tempfile.mkdtemp()
691- os.chdir(self.cwd)
692-
693- self.tempdir = os.path.join(self.cwd, "tempdir")
694- os.mkdir(self.tempdir)
695-
696- self.orig = {}
697- for attr in self.MOCKED or ():
698- self.orig[attr] = getattr(script, attr)
699- setattr(script, attr, mock.Mock())
700-
701- self.juju = script.Juju()
702-
703- def tearDown(self):
704- for attr in self.MOCKED or ():
705- setattr(script, attr, self.orig[attr])
706-
707- shutil.rmtree(self.cwd)
708- os.chdir(self.orig_cwd)
709-
710- super(_BaseTestCase, self).tearDown()
711-
712- def _create_tempfile(self, filename, data=None):
713- """Create a file at the identified path, but rooted at the temp dir."""
714- _create_file(os.path.join(self.tempdir, filename), data)
715-
716- def assert_cwd(self, dirname):
717- """Ensure that the CWD matches the given directory."""
718- cwd = os.getcwd()
719- self.assertEqual(cwd, dirname)
720-
721-
722-class GetJujuTests(TestCase):
723-
724- def test_juju1_outer(self):
725- """
726- get_juju() returns a Juju prepped for a Juju 1 outer model.
727- """
728- juju = script.get_juju(script.JUJU1, inner=False)
729-
730- expected = script.Juju("juju", model=None)
731- self.assertEqual(juju, expected)
732-
733- def test_juju1_inner(self):
734- """
735- get_juju() returns a Juju prepped for a Juju 1 inner model.
736- """
737- cfgdir = "/var/lib/landscape/juju-homes/0"
738-
739- juju = script.get_juju(script.JUJU1, model=None, cfgdir=cfgdir,
740- inner=True)
741-
742- expected = script.Juju("juju", cfgdir=cfgdir)
743- self.assertEqual(juju, expected)
744-
745- def test_juju2_outer(self):
746- """
747- get_juju() returns a Juju prepped for a Juju 2 outer model.
748- """
749- juju = script.get_juju(script.JUJU2, inner=False)
750-
751- expected = script.Juju("juju-2.1", model=None)
752- self.assertEqual(juju, expected)
753-
754- def test_juju2_inner(self):
755- """
756- get_juju() returns a Juju prepped for a Juju 2 inner model.
757- """
758- cfgdir = "/var/lib/landscape/juju-homes/0"
759-
760- juju = script.get_juju(script.JUJU2, cfgdir=cfgdir, inner=True)
761-
762- expected = script.Juju("juju-2.1", model="controller", cfgdir=cfgdir)
763- self.assertEqual(juju, expected)
764-
765-
766-class MainTestCase(_BaseTestCase):
767-
768- MOCKED = ("collect_logs", "collect_inner_logs", "bundle_logs")
769-
770- def setUp(self):
771- super(MainTestCase, self).setUp()
772-
773- self.orig_mkdtemp = script.mkdtemp
774- script.mkdtemp = lambda: self.tempdir
775-
776- def tearDown(self):
777- script.mkdtemp = self.orig_mkdtemp
778-
779- super(MainTestCase, self).tearDown()
780-
781- def test_success(self):
782- """
783- main() calls collect_logs(), collect_inner_logs(), and bundle_logs().
784- """
785- tarfile = "/tmp/logs.tgz"
786- extrafiles = ["spam.py"]
787-
788- script.main(tarfile, extrafiles, juju=self.juju)
789-
790- script.collect_logs.assert_called_once_with(self.juju)
791- script.collect_inner_logs.assert_called_once_with(
792- self.juju, script.DEFAULT_MODEL)
793- script.bundle_logs.assert_called_once_with(
794- self.tempdir, tarfile, extrafiles)
795- self.assertFalse(os.path.exists(self.tempdir))
796-
797- def test_in_correct_directories(self):
798- """
799- main() calls its dependencies while in specific directories.
800- """
801- script.collect_logs.side_effect = (
802- lambda _: self.assert_cwd(self.tempdir))
803- script.collect_inner_logs.side_effect = (
804- lambda _: self.assert_cwd(self.tempdir))
805- script.bundle_logs.side_effect = lambda *a: self.assert_cwd(self.cwd)
806- tarfile = "/tmp/logs.tgz"
807- extrafiles = ["spam.py"]
808-
809- script.main(tarfile, extrafiles, juju=self.juju)
810-
811- def test_no_script_recursion_for_inner_model(self):
812- """
813- main() will not call collect_inner_logs() if --inner is True.
814- """
815- tarfile = "/tmp/logs.tgz"
816- extrafiles = ["spam.py"]
817- cfgdir = "/var/lib/landscape/juju-homes/0"
818- juju = script.get_juju(script.JUJU2, cfgdir)
819-
820- script.main(tarfile, extrafiles, juju=juju, inner=True)
821-
822- script.collect_logs.assert_called_once_with(juju)
823- script.collect_inner_logs.assert_not_called()
824- script.bundle_logs.assert_called_once_with(
825- self.tempdir, tarfile, extrafiles)
826- self.assertFalse(os.path.exists(self.tempdir))
827-
828- def test_cleanup(self):
829- """
830- main() cleans up the temp dir it creates.
831- """
832- tarfile = "/tmp/logs.tgz"
833- extrafiles = ["spam.py"]
834-
835- script.main(tarfile, extrafiles, juju=self.juju)
836-
837- self.assertFalse(os.path.exists(self.tempdir))
838-
839- def test_collect_logs_error(self):
840- """
841- main() doesn't handle the error when collect_logs() fails.
842-
843- It still cleans up the temp dir.
844- """
845- tarfile = "/tmp/logs.tgz"
846- extrafiles = ["spam.py"]
847- script.collect_logs.side_effect = FakeError()
848-
849- with self.assertRaises(FakeError):
850- script.main(tarfile, extrafiles, juju=self.juju)
851-
852- script.collect_logs.assert_called_once_with(self.juju)
853- script.collect_inner_logs.assert_not_called()
854- script.bundle_logs.assert_not_called()
855- self.assertFalse(os.path.exists(self.tempdir))
856-
857- def test_collect_inner_logs_error(self):
858- """
859- main() ignores the error when collect_inner_logs() fails.
860-
861- It still cleans up the temp dir.
862- """
863- tarfile = "/tmp/logs.tgz"
864- extrafiles = ["spam.py"]
865- script.collect_inner_logs.side_effect = FakeError()
866-
867- script.main(tarfile, extrafiles, juju=self.juju)
868-
869- script.collect_logs.assert_called_once_with(self.juju)
870- script.collect_inner_logs.assert_called_once_with(
871- self.juju, script.DEFAULT_MODEL)
872- script.bundle_logs.assert_called_once_with(
873- self.tempdir, tarfile, extrafiles)
874- self.assertFalse(os.path.exists(self.tempdir))
875-
876- def test_bundle_logs_error(self):
877- """
878- main() doesn't handle the error when bundle_logs() fails.
879-
880- It still cleans up the temp dir.
881- """
882- tarfile = "/tmp/logs.tgz"
883- extrafiles = ["spam.py"]
884- script.bundle_logs.side_effect = FakeError()
885-
886- with self.assertRaises(FakeError):
887- script.main(tarfile, extrafiles, juju=self.juju)
888-
889- script.collect_logs.assert_called_once_with(self.juju)
890- script.collect_inner_logs.assert_called_once_with(
891- self.juju, script.DEFAULT_MODEL)
892- script.bundle_logs.assert_called_once_with(
893- self.tempdir, tarfile, extrafiles)
894- self.assertFalse(os.path.exists(self.tempdir))
895-
896-
897-class CollectLogsTestCase(_BaseTestCase):
898-
899- MOCKED = ("get_units", "check_output", "call")
900-
901- def setUp(self):
902- super(CollectLogsTestCase, self).setUp()
903-
904- self.units = [
905- "landscape-server/0",
906- "postgresql/0",
907- "rabbitmq-server/0",
908- "haproxy/0",
909- ]
910- script.get_units.return_value = self.units[:]
911-
912- self.mp_map_orig = script._mp_map
913- script._mp_map = lambda f, a: map(f, a)
914-
915- os.chdir(self.tempdir)
916-
917- def tearDown(self):
918- script._mp_map = self.mp_map_orig
919-
920- super(CollectLogsTestCase, self).tearDown()
921-
922- def _call_side_effect(self, cmd, env=None):
923- """Perform the side effect of calling the mocked-out call()."""
924- if cmd[0] == "tar":
925- self.assertTrue(os.path.exists(cmd[-1]))
926- return
927- self.assertEqual(env, self.juju.env)
928- self.assertEqual(cmd[0], self.juju.binary_path)
929- _create_file(os.path.basename(cmd[2]))
930-
931- def test_success(self):
932- """
933- collect_logs() gathers "ps" output and logs from each unit.
934- """
935- script.call.side_effect = self._call_side_effect
936-
937- script.collect_logs(self.juju)
938-
939- script.get_units.assert_called_once_with(self.juju)
940- expected = []
941- units = self.units + ["0"]
942- # for _create_ps_output_file()
943- for unit in units:
944- cmd = "ps fauxww | sudo tee /var/log/ps-fauxww.txt"
945- expected.append(mock.call(["juju", "ssh", unit, cmd],
946- stderr=subprocess.STDOUT,
947- env=None,
948- ))
949- # for _create_log_tarball()
950- for unit in units:
951- tarfile = "/tmp/logs_{}.tar".format(unit.replace("/", "-")
952- if unit != "0"
953- else "bootstrap")
954- cmd = ("sudo tar --ignore-failed-read"
955- " --exclude=/var/lib/landscape/client/package/hash-id"
956- " --exclude=/var/lib/juju/containers/juju-*-lxc-template"
957- " -cf {}"
958- " $(sudo sh -c \"ls -1d {} 2>/dev/null\")"
959- ).format(
960- tarfile,
961- " ".join(["/var/log",
962- "/etc/hosts",
963- "/etc/network",
964- "/var/lib/landscape/client",
965- "/etc/apache2",
966- "/etc/haproxy",
967- "/var/lib/lxc/*/rootfs/var/log",
968- "/var/lib/juju/containers",
969- "/etc/nova",
970- "/etc/swift",
971- "/etc/neutron",
972- "/etc/ceph",
973- "/etc/glance",
974- ]),
975- )
976- expected.append(mock.call(["juju", "ssh", unit, cmd],
977- stderr=subprocess.STDOUT,
978- env=None,
979- ))
980- expected.append(mock.call(["juju", "ssh", unit,
981- "sudo gzip -f {}".format(tarfile)],
982- stderr=subprocess.STDOUT,
983- env=None,
984- ))
985- self.assertEqual(script.check_output.call_count, len(expected))
986- script.check_output.assert_has_calls(expected, any_order=True)
987- # for download_log_from_unit()
988- expected = []
989- for unit in units:
990- name = unit.replace("/", "-") if unit != "0" else "bootstrap"
991- filename = "logs_{}.tar.gz".format(name)
992- source = "{}:/tmp/{}".format(unit, filename)
993- expected.append(mock.call(["juju", "scp", source, "."], env=None))
994- expected.append(mock.call(["tar", "-C", name, "-xzf", filename]))
995- self.assertFalse(os.path.exists(filename))
996- self.assertEqual(script.call.call_count, len(expected))
997- script.call.assert_has_calls(expected, any_order=True)
998-
999- def test_inner(self):
1000- """
1001- collect_logs() gathers "ps" output and logs from each unit.
1002- Running in the inner model produces different commands.
1003- """
1004- cfgdir = "/var/lib/landscape/juju-homes/0"
1005- juju = script.Juju("juju-2.1", model="controller", cfgdir=cfgdir)
1006- self.juju = juju
1007- script.call.side_effect = self._call_side_effect
1008-
1009- script.collect_logs(juju)
1010-
1011- script.get_units.assert_called_once_with(juju)
1012- expected = []
1013- units = self.units + ["0"]
1014- # for _create_ps_output_file()
1015- for unit in units:
1016- cmd = "ps fauxww | sudo tee /var/log/ps-fauxww.txt"
1017- expected.append(mock.call(["juju-2.1", "ssh",
1018- "-m", "controller", unit, cmd],
1019- stderr=subprocess.STDOUT,
1020- env=juju.env,
1021- ))
1022- # for _create_log_tarball()
1023- for unit in units:
1024- tarfile = "/tmp/logs_{}.tar".format(unit.replace("/", "-")
1025- if unit != "0"
1026- else "bootstrap")
1027- cmd = ("sudo tar --ignore-failed-read"
1028- " --exclude=/var/lib/landscape/client/package/hash-id"
1029- " --exclude=/var/lib/juju/containers/juju-*-lxc-template"
1030- " -cf {}"
1031- " $(sudo sh -c \"ls -1d {} 2>/dev/null\")"
1032- ).format(
1033- tarfile,
1034- " ".join(["/var/log",
1035- "/etc/hosts",
1036- "/etc/network",
1037- "/var/lib/landscape/client",
1038- "/etc/apache2",
1039- "/etc/haproxy",
1040- "/var/lib/lxc/*/rootfs/var/log",
1041- "/var/lib/juju/containers",
1042- "/etc/nova",
1043- "/etc/swift",
1044- "/etc/neutron",
1045- "/etc/ceph",
1046- "/etc/glance",
1047- ]),
1048- )
1049- expected.append(mock.call(
1050- ["juju-2.1", "ssh", "-m", "controller", unit, cmd],
1051- stderr=subprocess.STDOUT,
1052- env=juju.env,
1053- ))
1054- expected.append(mock.call(
1055- ["juju-2.1", "ssh", "-m", "controller", unit,
1056- "sudo gzip -f {}".format(tarfile)],
1057- stderr=subprocess.STDOUT,
1058- env=juju.env,
1059- ))
1060- self.assertEqual(script.check_output.call_count, len(expected))
1061- script.check_output.assert_has_calls(expected, any_order=True)
1062- # for download_log_from_unit()
1063- expected = []
1064- for unit in units:
1065- name = unit.replace("/", "-") if unit != "0" else "bootstrap"
1066- filename = "logs_{}.tar.gz".format(name)
1067- source = "{}:/tmp/{}".format(unit, filename)
1068- expected.append(mock.call(
1069- ["juju-2.1", "scp", "-m", "controller", source, "."],
1070- env=juju.env))
1071- expected.append(mock.call(["tar", "-C", name, "-xzf", filename]))
1072- self.assertFalse(os.path.exists(filename))
1073- self.assertEqual(script.call.call_count, len(expected))
1074- script.call.assert_has_calls(expected, any_order=True)
1075-
1076- def test_get_units_failure(self):
1077- """
1078- collect_logs() does not handle errors from get_units().
1079- """
1080- script.get_units.side_effect = FakeError()
1081-
1082- with self.assertRaises(FakeError):
1083- script.collect_logs(self.juju)
1084-
1085- script.get_units.assert_called_once_with(self.juju)
1086- script.check_output.assert_not_called()
1087- script.call.assert_not_called()
1088-
1089- def test_check_output_failure(self):
1090- """
1091- collect_logs() does not handle errors from check_output().
1092- """
1093- script.check_output.side_effect = [mock.DEFAULT,
1094- FakeError(),
1095- ]
1096-
1097- with self.assertRaises(FakeError):
1098- script.collect_logs(self.juju)
1099-
1100- script.get_units.assert_called_once_with(self.juju)
1101- self.assertEqual(script.check_output.call_count, 2)
1102- script.call.assert_not_called()
1103-
1104- def test_call_failure(self):
1105- """
1106- collect_logs() does not handle errors from call().
1107- """
1108- def call_side_effect(cmd, env=None):
1109- # second use of call() for landscape-server/0
1110- if script.call.call_count == 2:
1111- raise FakeError()
1112- # first use of call() for postgresql/0
1113- if script.call.call_count == 3:
1114- raise FakeError()
1115- # all other uses of call() default to the normal side effect.
1116- return self._call_side_effect(cmd, env=env)
1117- script.call.side_effect = call_side_effect
1118-
1119- script.collect_logs(self.juju)
1120-
1121- script.get_units.assert_called_once_with(self.juju)
1122- units = self.units + ["0"]
1123- self.assertEqual(script.check_output.call_count, len(units) * 3)
1124- self.assertEqual(script.call.call_count, len(units) * 2 - 1)
1125- for unit in units:
1126- name = unit.replace("/", "-") if unit != "0" else "bootstrap"
1127- if unit == self.units[1]:
1128- self.assertFalse(os.path.exists(name))
1129- else:
1130- self.assertTrue(os.path.exists(name))
1131- filename = "logs_{}.tar.gz".format(name)
1132- self.assertFalse(os.path.exists(filename))
1133-
1134-
1135-class CollectInnerLogsTestCase(_BaseTestCase):
1136-
1137- MOCKED = ("get_units", "check_output", "call", "check_call")
1138-
1139- def setUp(self):
1140- super(CollectInnerLogsTestCase, self).setUp()
1141-
1142- self.units = [
1143- "landscape-server/0",
1144- "postgresql/0",
1145- "rabbitmq-server/0",
1146- "haproxy/0",
1147- ]
1148- script.get_units.return_value = self.units[:]
1149- script.check_output.return_value = "0\n"
1150- script.call.return_value = 0
1151-
1152- os.chdir(self.tempdir)
1153-
1154- def assert_clean(self):
1155- """Ensure that collect_inner_logs cleaned up after itself."""
1156- self.assert_cwd(self.tempdir)
1157- self.assertFalse(os.path.exists("inner-logs.tar.gz"))
1158-
1159- def test_juju_2(self):
1160- """
1161- collect_inner_logs() finds the inner model and runs collect-logs
1162- inside it. The resulting tarball is downloaded, extracted, and
1163- deleted.
1164- """
1165- def check_call_side_effect(cmd, env=None):
1166- self.assertEqual(env, self.juju.env)
1167- if script.check_call.call_count == 4:
1168- self.assert_cwd(self.tempdir)
1169- self._create_tempfile("inner-logs.tar.gz")
1170- elif script.check_call.call_count == 5:
1171- cwd = os.path.join(self.tempdir, "landscape-0-inner-logs")
1172- self.assert_cwd(cwd)
1173- return None
1174- script.check_call.side_effect = check_call_side_effect
1175-
1176- script.collect_inner_logs(self.juju)
1177-
1178- # Check get_units() calls.
1179- script.get_units.assert_called_once_with(self.juju)
1180- # Check check_output() calls.
1181- expected = []
1182- cmd = ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/"
1183- "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
1184- " juju-2.1 model-config -m controller proxy-ssh=false")
1185- expected.append(mock.call(["juju", "ssh", "landscape-server/0", cmd],
1186- stderr=subprocess.STDOUT,
1187- env=self.juju.env))
1188- expected.append(mock.call(
1189- ["juju", "ssh", "landscape-server/0",
1190- "sudo ls -rt /var/lib/landscape/juju-homes/"],
1191- env=self.juju.env))
1192- self.assertEqual(script.check_output.call_count, len(expected))
1193- script.check_output.assert_has_calls(expected, any_order=True)
1194- # Check call() calls.
1195- expected = [
1196- mock.call(["juju", "ssh", "landscape-server/0",
1197- ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/0 "
1198- "juju-2.1 status -m controller --format=yaml"),
1199- ], env=self.juju.env),
1200- mock.call(["juju", "scp",
1201- os.path.join(os.path.dirname(__file__), "collect-logs"),
1202- "landscape-server/0:/tmp/collect-logs",
1203- ], env=self.juju.env),
1204- mock.call(["juju", "ssh",
1205- "landscape-server/0",
1206- "sudo rm -rf /tmp/inner-logs.tar.gz",
1207- ], env=self.juju.env),
1208- ]
1209- self.assertEqual(script.call.call_count, len(expected))
1210- script.call.assert_has_calls(expected, any_order=True)
1211- # Check check_call() calls.
1212- cmd = ("sudo"
1213- " JUJU_DATA=/var/lib/landscape/juju-homes/0"
1214- " /tmp/collect-logs --inner --juju juju-2.1"
1215- " --model controller"
1216- " --cfgdir /var/lib/landscape/juju-homes/0"
1217- " /tmp/inner-logs.tar.gz")
1218- expected = [
1219- mock.call(["juju", "ssh", "landscape-server/0", cmd],
1220- env=self.juju.env),
1221- mock.call(["juju", "scp",
1222- "landscape-server/0:/tmp/inner-logs.tar.gz",
1223- os.path.join(self.tempdir, "inner-logs.tar.gz"),
1224- ], env=self.juju.env),
1225- mock.call(["tar", "-zxf", self.tempdir + "/inner-logs.tar.gz"]),
1226- ]
1227- self.assertEqual(script.check_call.call_count, len(expected))
1228- script.check_call.assert_has_calls(expected, any_order=True)
1229- self.assert_clean()
1230-
1231- def test_juju_1(self):
1232- """
1233- collect_inner_logs() finds the inner model and runs collect-logs
1234- inside it. The resulting tarball is downloaded, extracted, and
1235- deleted.
1236- """
1237- def check_call_side_effect(cmd, env=None):
1238- self.assertEqual(env, self.juju.env)
1239- if script.check_call.call_count == 4:
1240- self.assert_cwd(self.tempdir)
1241- self._create_tempfile("inner-logs.tar.gz")
1242- elif script.check_call.call_count == 5:
1243- cwd = os.path.join(self.tempdir, "landscape-0-inner-logs")
1244- self.assert_cwd(cwd)
1245- return None
1246- script.check_call.side_effect = check_call_side_effect
1247- script.call.side_effect = [1, 0, 0, 0]
1248- err = subprocess.CalledProcessError(1, "...", "<output>")
1249- script.check_output.side_effect = [err,
1250- mock.DEFAULT,
1251- mock.DEFAULT,
1252- ]
1253-
1254- script.collect_inner_logs(self.juju)
1255-
1256- # Check get_units() calls.
1257- script.get_units.assert_called_once_with(self.juju)
1258- # Check check_output() calls.
1259- expected = []
1260- cmd = ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/"
1261- "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
1262- " juju-2.1 model-config -m controller proxy-ssh=false")
1263- expected.append(mock.call(["juju", "ssh", "landscape-server/0", cmd],
1264- stderr=subprocess.STDOUT,
1265- env=None))
1266- cmd = ("sudo JUJU_HOME=/var/lib/landscape/juju-homes/"
1267- "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
1268- " juju set-env proxy-ssh=false")
1269- expected.append(mock.call(["juju", "ssh", "landscape-server/0", cmd],
1270- stderr=subprocess.STDOUT,
1271- env=None))
1272- expected.append(mock.call(
1273- ["juju", "ssh", "landscape-server/0",
1274- "sudo ls -rt /var/lib/landscape/juju-homes/"],
1275- env=None))
1276- self.assertEqual(script.check_output.call_count, len(expected))
1277- script.check_output.assert_has_calls(expected, any_order=True)
1278- # Check call() calls.
1279- expected = [
1280- mock.call(["juju", "ssh", "landscape-server/0",
1281- ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/0 "
1282- "juju-2.1 status -m controller --format=yaml"),
1283- ], env=None),
1284- mock.call(["juju", "ssh", "landscape-server/0",
1285- ("sudo -u landscape "
1286- "JUJU_HOME=/var/lib/landscape/juju-homes/0 "
1287- "juju status --format=yaml"),
1288- ], env=None),
1289- mock.call(["juju", "scp",
1290- os.path.join(os.path.dirname(__file__), "collect-logs"),
1291- "landscape-server/0:/tmp/collect-logs",
1292- ], env=None),
1293- mock.call(["juju", "ssh",
1294- "landscape-server/0",
1295- "sudo rm -rf /tmp/inner-logs.tar.gz",
1296- ], env=None),
1297- ]
1298- self.assertEqual(script.call.call_count, len(expected))
1299- script.call.assert_has_calls(expected, any_order=True)
1300- # Check check_call() calls.
1301- cmd = ("sudo -u landscape"
1302- " JUJU_HOME=/var/lib/landscape/juju-homes/0"
1303- " /tmp/collect-logs --inner --juju juju"
1304- " --cfgdir /var/lib/landscape/juju-homes/0"
1305- " /tmp/inner-logs.tar.gz")
1306- expected = [
1307- mock.call(["juju", "ssh", "landscape-server/0", cmd], env=None),
1308- mock.call(["juju", "scp",
1309- "landscape-server/0:/tmp/inner-logs.tar.gz",
1310- os.path.join(self.tempdir, "inner-logs.tar.gz"),
1311- ], env=None),
1312- mock.call(["tar", "-zxf", self.tempdir + "/inner-logs.tar.gz"]),
1313- ]
1314- self.assertEqual(script.check_call.call_count, len(expected))
1315- script.check_call.assert_has_calls(expected, any_order=True)
1316- self.assert_clean()
1317-
1318- def test_with_legacy_landscape_unit(self):
1319- """
1320- collect_inner_logs() correctly supports legacy landscape installations.
1321- """
1322- self.units[0] = "landscape/0"
1323- script.get_units.return_value = self.units[:]
1324- err = subprocess.CalledProcessError(1, "...", "<output>")
1325- script.check_output.side_effect = [err,
1326- mock.DEFAULT,
1327- mock.DEFAULT,
1328- ]
1329-
1330- script.collect_inner_logs(self.juju)
1331-
1332- expected = []
1333- cmd = ("sudo JUJU_DATA=/var/lib/landscape/juju-homes/"
1334- "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
1335- " juju-2.1 model-config -m controller proxy-ssh=false")
1336- expected.append(mock.call(["juju", "ssh", "landscape/0", cmd],
1337- stderr=subprocess.STDOUT,
1338- env=None))
1339- cmd = ("sudo JUJU_HOME=/var/lib/landscape/juju-homes/"
1340- "`sudo ls -rt /var/lib/landscape/juju-homes/ | tail -1`"
1341- " juju set-env proxy-ssh=false")
1342- expected.append(mock.call(["juju", "ssh", "landscape/0", cmd],
1343- stderr=subprocess.STDOUT,
1344- env=None))
1345- expected.append(mock.call(
1346- ["juju", "ssh", "landscape/0",
1347- "sudo ls -rt /var/lib/landscape/juju-homes/"],
1348- env=None))
1349- self.assertEqual(script.check_output.call_count, len(expected))
1350- script.check_output.assert_has_calls(expected, any_order=True)
1351- self.assert_clean()
1352-
1353- def test_no_units(self):
1354- """
1355- collect_inner_logs() is a noop if no units are found.
1356- """
1357- script.get_units.return_value = []
1358-
1359- script.collect_inner_logs(self.juju)
1360-
1361- script.get_units.assert_called_once_with(self.juju)
1362- script.check_output.assert_not_called()
1363- script.call.assert_not_called()
1364- script.check_call.assert_not_called()
1365- self.assert_clean()
1366-
1367- def test_no_landscape_server_unit(self):
1368- """
1369- collect_inner_logs() is a noop if the landscape unit isn't found.
1370- """
1371- del self.units[0]
1372- script.get_units.return_value = self.units[:]
1373-
1374- script.collect_inner_logs(self.juju)
1375-
1376- script.get_units.assert_called_once_with(self.juju)
1377- script.check_output.assert_not_called()
1378- script.call.assert_not_called()
1379- script.check_call.assert_not_called()
1380- self.assert_clean()
1381-
1382- def test_no_juju_homes(self):
1383- script.get_units.return_value = []
1384- script.check_output.return_value = ""
1385-
1386- script.collect_inner_logs(self.juju)
1387-
1388- script.get_units.assert_called_once_with(self.juju)
1389-
1390- script.get_units.assert_called_once_with(self.juju)
1391- script.check_output.assert_not_called()
1392- script.call.assert_not_called()
1393- script.check_call.assert_not_called()
1394- self.assert_clean()
1395-
1396- def test_get_units_failure(self):
1397- """
1398- collect_inner_logs() does not handle errors from get_units().
1399- """
1400- script.get_units.side_effect = FakeError()
1401-
1402- with self.assertRaises(FakeError):
1403- script.collect_inner_logs(self.juju)
1404-
1405- self.assertEqual(script.get_units.call_count, 1)
1406- script.check_output.assert_not_called()
1407- script.call.assert_not_called()
1408- script.check_call.assert_not_called()
1409- self.assert_cwd(self.tempdir)
1410- self.assert_clean()
1411-
1412- def test_check_output_failure_1(self):
1413- """
1414- collect_inner_logs() does not handle non-CalledProcessError
1415- errors when disabling the SSH proxy.
1416- """
1417- script.check_output.side_effect = FakeError()
1418-
1419- with self.assertRaises(FakeError):
1420- script.collect_inner_logs(self.juju)
1421-
1422- self.assertEqual(script.get_units.call_count, 1)
1423- self.assertEqual(script.check_output.call_count, 1)
1424- script.call.assert_not_called()
1425- script.check_call.assert_not_called()
1426- self.assert_cwd(self.tempdir)
1427- self.assert_clean()
1428-
1429- def test_check_output_failure_2(self):
1430- """
1431- collect_inner_logs() does not handle non-CalledProcessError
1432- errors when verifying the inner model is bootstrapped.
1433- """
1434- script.check_output.side_effect = [None,
1435- FakeError(),
1436- ]
1437-
1438- with self.assertRaises(FakeError):
1439- script.collect_inner_logs(self.juju)
1440-
1441- self.assertEqual(script.get_units.call_count, 1)
1442- self.assertEqual(script.check_output.call_count, 2)
1443- script.call.assert_not_called()
1444- script.check_call.assert_not_called()
1445- self.assert_cwd(self.tempdir)
1446- self.assert_clean()
1447-
1448- def test_call_juju2_failure(self):
1449- """
1450- collect_inner_logs() does not handle errors from call().
1451- """
1452- script.call.side_effect = FakeError()
1453-
1454- with self.assertRaises(FakeError):
1455- script.collect_inner_logs(self.juju)
1456-
1457- self.assertEqual(script.get_units.call_count, 1)
1458- self.assertEqual(script.check_output.call_count, 2)
1459- self.assertEqual(script.call.call_count, 1)
1460- script.check_call.assert_not_called()
1461- self.assert_cwd(self.tempdir)
1462- self.assert_clean()
1463-
1464- def test_call_juju1_failure(self):
1465- """
1466- collect_inner_logs() does not handle errors from call().
1467- """
1468- script.call.side_effect = [1,
1469- FakeError(),
1470- ]
1471-
1472- with self.assertRaises(FakeError):
1473- script.collect_inner_logs(self.juju)
1474-
1475- self.assertEqual(script.get_units.call_count, 1)
1476- self.assertEqual(script.check_output.call_count, 2)
1477- self.assertEqual(script.call.call_count, 2)
1478- script.check_call.assert_not_called()
1479- self.assert_cwd(self.tempdir)
1480- self.assert_clean()
1481-
1482- def test_call_juju2_nonzero_return(self):
1483- """
1484- When no Juju 2 model is detected, Juju 1 is tried.
1485- """
1486- script.call.side_effect = [1,
1487- mock.DEFAULT,
1488- mock.DEFAULT,
1489- mock.DEFAULT,
1490- ]
1491-
1492- script.collect_inner_logs(self.juju)
1493-
1494- self.assertEqual(script.get_units.call_count, 1)
1495- self.assertEqual(script.check_output.call_count, 2)
1496- self.assertEqual(script.call.call_count, 4)
1497- self.assertEqual(script.check_call.call_count, 3)
1498- self.assert_clean()
1499-
1500- def test_call_juju1_nonzero_return(self):
1501- """
1502- When no Juju 2 model is detected, Juju 1 is tried. When that
1503- is not detected, no inner logs are collected.
1504- """
1505- script.call.side_effect = [1,
1506- 1,
1507- ]
1508-
1509- script.collect_inner_logs(self.juju)
1510-
1511- self.assertEqual(script.get_units.call_count, 1)
1512- self.assertEqual(script.check_output.call_count, 2)
1513- self.assertEqual(script.call.call_count, 2)
1514- script.check_call.assert_not_called()
1515- self.assert_clean()
1516-
1517- def test_call_all_nonzero_return(self):
1518- """
1519- When no Juju 2 model is detected, Juju 1 is tried. When that
1520- is not detected, no inner logs are collected.
1521- """
1522- script.call.return_value = 1
1523-
1524- script.collect_inner_logs(self.juju)
1525-
1526- self.assertEqual(script.get_units.call_count, 1)
1527- self.assertEqual(script.check_output.call_count, 2)
1528- self.assertEqual(script.call.call_count, 2)
1529- script.check_call.assert_not_called()
1530- self.assert_clean()
1531-
1532- def test_check_call_failure_1(self):
1533- """
1534- collect_inner_logs() does not handle errors when running
1535- collect-logs in the inner model.
1536- """
1537- script.check_call.side_effect = FakeError()
1538-
1539- with self.assertRaises(FakeError):
1540- script.collect_inner_logs(self.juju)
1541-
1542- self.assertEqual(script.get_units.call_count, 1)
1543- self.assertEqual(script.check_output.call_count, 2)
1544- self.assertEqual(script.call.call_count, 3)
1545- self.assertEqual(script.check_call.call_count, 1)
1546- self.assert_clean()
1547-
1548- def test_check_call_failure_2(self):
1549- """
1550- collect_inner_logs() does not handle errors downloading the
1551- collected logs from the inner model.
1552-
1553- It does clean up, however.
1554- """
1555- script.check_call.side_effect = [None,
1556- FakeError(),
1557- ]
1558-
1559- with self.assertRaises(FakeError):
1560- script.collect_inner_logs(self.juju)
1561-
1562- self.assertEqual(script.get_units.call_count, 1)
1563- self.assertEqual(script.check_output.call_count, 2)
1564- self.assertEqual(script.call.call_count, 3)
1565- self.assertEqual(script.check_call.call_count, 2)
1566- self.assert_clean()
1567-
1568- def test_check_call_failure_3(self):
1569- def check_call_side_effect(cmd, env=None):
1570- self.assertEqual(env, self.juju.env)
1571- if script.check_call.call_count == 3:
1572- raise FakeError()
1573- if script.check_call.call_count == 2:
1574- self._create_tempfile("inner-logs.tar.gz")
1575- return None
1576- script.check_call.side_effect = check_call_side_effect
1577-
1578- with self.assertRaises(FakeError):
1579- script.collect_inner_logs(self.juju)
1580-
1581- self.assertEqual(script.get_units.call_count, 1)
1582- self.assertEqual(script.check_output.call_count, 2)
1583- self.assertEqual(script.call.call_count, 3)
1584- self.assertEqual(script.check_call.call_count, 3)
1585- self.assert_clean()
1586-
1587-
1588-class BundleLogsTestCase(_BaseTestCase):
1589-
1590- MOCKED = ("call",)
1591-
1592- def setUp(self):
1593- """
1594- bundle_logs() creates a tarball holding the files in the tempdir.
1595- """
1596- super(BundleLogsTestCase, self).setUp()
1597-
1598- os.chdir(self.tempdir)
1599-
1600- self._create_tempfile("bootstrap/var/log/syslog")
1601- self._create_tempfile("bootstrap/var/log/juju/all-machines.log")
1602- self._create_tempfile(
1603- "bootstrap/var/lib/lxc/deadbeef/rootfs/var/log/syslog")
1604- self._create_tempfile("bootstrap/var/lib/juju/containers")
1605- self._create_tempfile("landscape-server-0/var/log/syslog")
1606- self._create_tempfile("postgresql-0/var/log/syslog")
1607- self._create_tempfile("rabbitmq-server-0/var/log/syslog")
1608- self._create_tempfile("haproxy-0/var/log/syslog")
1609- self._create_tempfile(
1610- "landscape-0-inner-logs/bootstrap/var/log/syslog")
1611-
1612- self.extrafile = os.path.join(self.cwd, "spam.txt")
1613- _create_file(self.extrafile)
1614-
1615- def test_success_with_extra(self):
1616- """
1617- bundle_logs() works if extra files are included.
1618- """
1619- tarfile = "/tmp/logs.tgz"
1620- extrafiles = [self.extrafile]
1621-
1622- script.bundle_logs(self.tempdir, tarfile, extrafiles)
1623-
1624- script.call.assert_called_once_with(
1625- ["tar",
1626- "czf", tarfile,
1627- "--transform", "s,{}/,,".format(self.tempdir[1:]),
1628- os.path.join(self.tempdir, "bootstrap"),
1629- os.path.join(self.tempdir, "haproxy-0"),
1630- os.path.join(self.tempdir, "landscape-0-inner-logs"),
1631- os.path.join(self.tempdir, "landscape-server-0"),
1632- os.path.join(self.tempdir, "postgresql-0"),
1633- os.path.join(self.tempdir, "rabbitmq-server-0"),
1634- self.extrafile,
1635- ],
1636- )
1637-
1638- def test_success_without_extra(self):
1639- """
1640- bundle_logs() works if there aren't any extra files.
1641- """
1642- tarfile = "/tmp/logs.tgz"
1643-
1644- script.bundle_logs(self.tempdir, tarfile)
1645-
1646- script.call.assert_called_once_with(
1647- ["tar",
1648- "czf", tarfile,
1649- "--transform", "s,{}/,,".format(self.tempdir[1:]),
1650- os.path.join(self.tempdir, "bootstrap"),
1651- os.path.join(self.tempdir, "haproxy-0"),
1652- os.path.join(self.tempdir, "landscape-0-inner-logs"),
1653- os.path.join(self.tempdir, "landscape-server-0"),
1654- os.path.join(self.tempdir, "postgresql-0"),
1655- os.path.join(self.tempdir, "rabbitmq-server-0"),
1656- ],
1657- )
1658-
1659- def test_success_no_files(self):
1660- """
1661- bundle_logs() works even when the temp dir is empty.
1662- """
1663- for filename in os.listdir(self.tempdir):
1664- shutil.rmtree(os.path.join(self.tempdir, filename))
1665- tarfile = "/tmp/logs.tgz"
1666-
1667- script.bundle_logs(self.tempdir, tarfile)
1668-
1669- script.call.assert_called_once_with(
1670- ["tar",
1671- "czf", tarfile,
1672- "--transform", "s,{}/,,".format(self.tempdir[1:]),
1673- ],
1674- )
1675-
1676- def test_call_failure(self):
1677- """
1678- bundle_logs() does not handle errors when creating the tarball.
1679- """
1680- script.call.side_effect = FakeError()
1681- tarfile = "/tmp/logs.tgz"
1682-
1683- with self.assertRaises(FakeError):
1684- script.bundle_logs(self.tempdir, tarfile)
1685-
1686- script.call.assert_called_once_with(
1687- ["tar",
1688- "czf", tarfile,
1689- "--transform", "s,{}/,,".format(self.tempdir[1:]),
1690- os.path.join(self.tempdir, "bootstrap"),
1691- os.path.join(self.tempdir, "haproxy-0"),
1692- os.path.join(self.tempdir, "landscape-0-inner-logs"),
1693- os.path.join(self.tempdir, "landscape-server-0"),
1694- os.path.join(self.tempdir, "postgresql-0"),
1695- os.path.join(self.tempdir, "rabbitmq-server-0"),
1696- ],
1697- )

Subscribers

People subscribed via source and target branches

to all changes: