Merge ~chad.smith/cloud-init:collect-logs into cloud-init:master

Proposed by Chad Smith
Status: Merged
Approved by: Scott Moser
Approved revision: ef77fe49051d67d7486e3ef7dd4dd662271abd1e
Merged at revision: e626966ee7d339b53d2c8b14a8f2ff8e3fe892ee
Proposed branch: ~chad.smith/cloud-init:collect-logs
Merge into: cloud-init:master
Diff against target: 439 lines (+354/-6)
7 files modified
cloudinit/apport.py (+105/-0)
cloudinit/cmd/devel/logs.py (+101/-0)
cloudinit/cmd/devel/tests/__init__.py (+0/-0)
cloudinit/cmd/devel/tests/test_logs.py (+120/-0)
cloudinit/cmd/main.py (+10/-1)
packages/debian/rules.in (+1/-0)
tests/unittests/test_cli.py (+17/-5)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser Approve
Review via email: mp+330626@code.launchpad.net

Commit message

cmdline: add collect-logs subcommand.

Add a new collect-logs sub command to the cloud-init CLI. This script
will collect all logs pertinent to a cloud-init run and store them in a
compressed tar-gzipped file. This tarfile can be attached to any
cloud-init bug filed in order to aid in bug triage and resolution.

A cloudinit.apport module is also added that allows apport interaction.
Here is an example bug filed via ubuntu-bug cloud-init: LP: #1716975.

Once the apport launcher is packaged in cloud-init, bugs can be filed
against cloud-init with the following command:
  ubuntu-bug cloud-init

LP: #1607345

Description of the change

cmdline: cloud-init collect-logs

Add a new collect-logs parameter to the cloud-init CLI. This script will
collect all logs pertinent to a cloud-init run and store them in a
compressed tar-gzipped file. this tarfile can be attached to any
cloud-init bug filed in order to aid in bug triage and resolution.

Added apport package-hooks script for cloud-init, here is an example bug filed via ubuntu-bug cloud-init: LP: #1716975.

Bugs can now be filed against cloud-init with the following command:
ubuntu-bug cloud-init

LP: #1607345

To post a comment you must log in.
~chad.smith/cloud-init:collect-logs updated
077f8e9... by Chad Smith

docstrings for functions and make the collect-logs script callable using main()

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:077f8e937c181c94373a0133097f2d5b904b3431
https://jenkins.ubuntu.com/server/job/cloud-init-ci/285/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/285/rebuild

review: Approve (continuous-integration)
~chad.smith/cloud-init:collect-logs updated
9428b5b... by Chad Smith

add apport package-hooks for cloud-init

c969087... by Chad Smith

tabs not spaces rules.in

7a2ae7b... by Chad Smith

rules.in updates to include apport package-hooks

1efed63... by Chad Smith

docstring fixup

66dad59... by Chad Smith

comment typos and 80 char line width

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:66dad59e4d258323a965959f585d70de2eb624f5
https://jenkins.ubuntu.com/server/job/cloud-init-ci/288/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/288/rebuild

review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Others (rbasak) disagree, but if I run:
  $ cloud-init collect-logs

And it shows me nothing. I wonder what it did, and if it did something.

I'd prefer:
  $ cloud-init collect-logs
  wrote cloud-init.tar.gz

other things:
 * 'version' file has no carriage return.

some comments inline.

review: Needs Fixing
~chad.smith/cloud-init:collect-logs updated
e06880a... by Chad Smith

Address smoser's review comments:

 - update collect-logs help summary "logs" -> "debug info"
 - fixup imports in apport cloud-init.py, separate VMware and OVF cloud
   choices, grep cloud-init.log for warnings or errors, use existing
   /var/lib/cloud/instance/user-data.txt instead of asking them to create
   /run/cloud-init/user-data.txt
 - fixup collect-logs (log.py) to use tempdir for log-dir creation, add
   newline in version file, cloudconfig-schema -> collect-logs copy paste error
   add vi comment to end of file

6d93c8c... by Chad Smith

add _write_command_output_to_file helper function per review comments

327f3ee... by Chad Smith

flakes and ProcessExecutionError instead of CalledProcessError

Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:327f3ee9cf6c1c41fd7e50894dd4d6290b1b6755
https://jenkins.ubuntu.com/server/job/cloud-init-ci/304/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/304/rebuild

review: Needs Fixing (continuous-integration)
~chad.smith/cloud-init:collect-logs updated
04260ae... by Chad Smith

unit tests validate that we add a trailing newline to cmd content if it does not already have a newline

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:04260ae927a931fdece8ca45c21fc709dc741793
https://jenkins.ubuntu.com/server/job/cloud-init-ci/305/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/305/rebuild

review: Approve (continuous-integration)
~chad.smith/cloud-init:collect-logs updated
c83e0ba... by Chad Smith

move cloud-init.pt -> cloudinit.apport. Add systemd-ordering tag to apport.py bug if we see Breaking ordering cycle per bug 1717477. Drop upstream inclusion of cloud-init.py, we will have to carry a minimal patch to deliver apport functionality to ubuntu releases

Revision history for this message
Chad Smith (chad.smith) wrote :

For full apport support: here's the wrapper we need to deliver to /usr/share/apport/package-hooks/cloud-init.py in supported distros (ubuntu/debian)

http://paste.ubuntu.com/25541681/

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:c83e0bac7edf9a77db07726fbf9950847a1997f9
https://jenkins.ubuntu.com/server/job/cloud-init-ci/307/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/307/rebuild

review: Needs Fixing (continuous-integration)
~chad.smith/cloud-init:collect-logs updated
dba1e05... by Chad Smith

add optional --include-userdata param to collect-logs. make the apport import optional

Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:dba1e05097eac1156a5bf69aa98e7fe0ae533515
https://jenkins.ubuntu.com/server/job/cloud-init-ci/310/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/310/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

minor changes suggested.
make those and we're good.
thanks.

review: Approve
Revision history for this message
Scott Moser (smoser) wrote :

I put this up
 https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/330858
for packaging of the apport lauincher

~chad.smith/cloud-init:collect-logs updated
ef77fe4... by Chad Smith

address review comments on license, dpkg-query structuring, docstrings. And dropped the optional newline append to cmd output

Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Scott Moser (smoser) :
review: Approve
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:ef77fe49051d67d7486e3ef7dd4dd662271abd1e
https://jenkins.ubuntu.com/server/job/cloud-init-ci/315/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/315/rebuild

review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/cloudinit/apport.py b/cloudinit/apport.py
0new file mode 1006440new file mode 100644
index 0000000..221f341
--- /dev/null
+++ b/cloudinit/apport.py
@@ -0,0 +1,105 @@
1# Copyright (C) 2017 Canonical Ltd.
2#
3# This file is part of cloud-init. See LICENSE file for license information.
4
5'''Cloud-init apport interface'''
6
7try:
8 from apport.hookutils import (
9 attach_file, attach_root_command_outputs, root_command_output)
10 has_apport = True
11except ImportError:
12 has_apport = False
13
14
15KNOWN_CLOUD_NAMES = [
16 'Amazon - Ec2', 'AliYun', 'AltCloud', 'Azure', 'Bigstep', 'CloudSigma',
17 'CloudStack', 'DigitalOcean', 'GCE - Google Compute Engine', 'MAAS',
18 'NoCloud', 'OpenNebula', 'OpenStack', 'OVF', 'Scaleway', 'SmartOS',
19 'VMware', 'Other']
20
21# Potentially clear text collected logs
22CLOUDINIT_LOG = '/var/log/cloud-init.log'
23CLOUDINIT_OUTPUT_LOG = '/var/log/cloud-init-output.log'
24USER_DATA_FILE = '/var/lib/cloud/instance/user-data.txt' # Optional
25
26
27def attach_cloud_init_logs(report, ui=None):
28 '''Attach cloud-init logs and tarfile from 'cloud-init collect-logs'.'''
29 attach_root_command_outputs(report, {
30 'cloud-init-log-warnings':
31 'egrep -i "warn|error" /var/log/cloud-init.log',
32 'cloud-init-output.log.txt': 'cat /var/log/cloud-init-output.log'})
33 root_command_output(
34 ['cloud-init', 'collect-logs', '-t', '/tmp/cloud-init-logs.tgz'])
35 attach_file(report, '/tmp/cloud-init-logs.tgz', 'logs.tgz')
36
37
38def attach_hwinfo(report, ui=None):
39 '''Optionally attach hardware info from lshw.'''
40 prompt = (
41 'Your device details (lshw) may be useful to developers when'
42 ' addressing this bug, but gathering it requires admin privileges.'
43 ' Would you like to include this info?')
44 if ui and ui.yesno(prompt):
45 attach_root_command_outputs(report, {'lshw.txt': 'lshw'})
46
47
48def attach_cloud_info(report, ui=None):
49 '''Prompt for cloud details if available.'''
50 if ui:
51 prompt = 'Is this machine running in a cloud environment?'
52 response = ui.yesno(prompt)
53 if response is None:
54 raise StopIteration # User cancelled
55 if response:
56 prompt = ('Please select the cloud vendor or environment in which'
57 ' this instance is running')
58 response = ui.choice(prompt, KNOWN_CLOUD_NAMES)
59 if response:
60 report['CloudName'] = KNOWN_CLOUD_NAMES[response[0]]
61 else:
62 report['CloudName'] = 'None'
63
64
65def attach_user_data(report, ui=None):
66 '''Optionally provide user-data if desired.'''
67 if ui:
68 prompt = (
69 'Your user-data or cloud-config file can optionally be provided'
70 ' from {0} and could be useful to developers when addressing this'
71 ' bug. Do you wish to attach user-data to this bug?'.format(
72 USER_DATA_FILE))
73 response = ui.yesno(prompt)
74 if response is None:
75 raise StopIteration # User cancelled
76 if response:
77 attach_file(report, USER_DATA_FILE, 'user_data.txt')
78
79
80def add_bug_tags(report):
81 '''Add any appropriate tags to the bug.'''
82 if 'JournalErrors' in report.keys():
83 errors = report['JournalErrors']
84 if 'Breaking ordering cycle' in errors:
85 report['Tags'] = 'systemd-ordering'
86
87
88def add_info(report, ui):
89 '''This is an entry point to run cloud-init's apport functionality.
90
91 Distros which want apport support will have a cloud-init package-hook at
92 /usr/share/apport/package-hooks/cloud-init.py which defines an add_info
93 function and returns the result of cloudinit.apport.add_info(report, ui).
94 '''
95 if not has_apport:
96 raise RuntimeError(
97 'No apport imports discovered. Apport functionality disabled')
98 attach_cloud_init_logs(report, ui)
99 attach_hwinfo(report, ui)
100 attach_cloud_info(report, ui)
101 attach_user_data(report, ui)
102 add_bug_tags(report)
103 return True
104
105# vi: ts=4 expandtab
diff --git a/cloudinit/cmd/devel/logs.py b/cloudinit/cmd/devel/logs.py
0new file mode 100644106new file mode 100644
index 0000000..35ca478
--- /dev/null
+++ b/cloudinit/cmd/devel/logs.py
@@ -0,0 +1,101 @@
1# Copyright (C) 2017 Canonical Ltd.
2#
3# This file is part of cloud-init. See LICENSE file for license information.
4
5"""Define 'collect-logs' utility and handler to include in cloud-init cmd."""
6
7import argparse
8from cloudinit.util import (
9 ProcessExecutionError, chdir, copy, ensure_dir, subp, write_file)
10from cloudinit.temp_utils import tempdir
11from datetime import datetime
12import os
13import shutil
14
15
16CLOUDINIT_LOGS = ['/var/log/cloud-init.log', '/var/log/cloud-init-output.log']
17CLOUDINIT_RUN_DIR = '/run/cloud-init'
18USER_DATA_FILE = '/var/lib/cloud/instance/user-data.txt' # Optional
19
20
21def get_parser(parser=None):
22 """Build or extend and arg parser for collect-logs utility.
23
24 @param parser: Optional existing ArgumentParser instance representing the
25 collect-logs subcommand which will be extended to support the args of
26 this utility.
27
28 @returns: ArgumentParser with proper argument configuration.
29 """
30 if not parser:
31 parser = argparse.ArgumentParser(
32 prog='collect-logs',
33 description='Collect and tar all cloud-init debug info')
34 parser.add_argument(
35 "--tarfile", '-t', default='cloud-init.tar.gz',
36 help=('The tarfile to create containing all collected logs.'
37 ' Default: cloud-init.tar.gz'))
38 parser.add_argument(
39 "--include-userdata", '-u', default=False, action='store_true',
40 dest='userdata', help=(
41 'Optionally include user-data from {0} which could contain'
42 ' sensitive information.'.format(USER_DATA_FILE)))
43 return parser
44
45
46def _write_command_output_to_file(cmd, filename):
47 """Helper which runs a command and writes output or error to filename."""
48 try:
49 out, _ = subp(cmd)
50 except ProcessExecutionError as e:
51 write_file(filename, str(e))
52 else:
53 write_file(filename, out)
54
55
56def collect_logs(tarfile, include_userdata):
57 """Collect all cloud-init logs and tar them up into the provided tarfile.
58
59 @param tarfile: The path of the tar-gzipped file to create.
60 @param include_userdata: Boolean, true means include user-data.
61 """
62 tarfile = os.path.abspath(tarfile)
63 date = datetime.utcnow().date().strftime('%Y-%m-%d')
64 log_dir = 'cloud-init-logs-{0}'.format(date)
65 with tempdir(dir='/tmp') as tmp_dir:
66 log_dir = os.path.join(tmp_dir, log_dir)
67 _write_command_output_to_file(
68 ['dpkg-query', '--show', "-f=${Version}\n", 'cloud-init'],
69 os.path.join(log_dir, 'version'))
70 _write_command_output_to_file(
71 ['dmesg'], os.path.join(log_dir, 'dmesg.txt'))
72 _write_command_output_to_file(
73 ['journalctl', '-o', 'short-precise'],
74 os.path.join(log_dir, 'journal.txt'))
75 for log in CLOUDINIT_LOGS:
76 copy(log, log_dir)
77 if include_userdata:
78 copy(USER_DATA_FILE, log_dir)
79 run_dir = os.path.join(log_dir, 'run')
80 ensure_dir(run_dir)
81 shutil.copytree(CLOUDINIT_RUN_DIR, os.path.join(run_dir, 'cloud-init'))
82 with chdir(tmp_dir):
83 subp(['tar', 'czvf', tarfile, log_dir.replace(tmp_dir + '/', '')])
84
85
86def handle_collect_logs_args(name, args):
87 """Handle calls to 'cloud-init collect-logs' as a subcommand."""
88 collect_logs(args.tarfile, args.userdata)
89
90
91def main():
92 """Tool to collect and tar all cloud-init related logs."""
93 parser = get_parser()
94 handle_collect_logs_args('collect-logs', parser.parse_args())
95 return 0
96
97
98if __name__ == '__main__':
99 main()
100
101# vi: ts=4 expandtab
diff --git a/cloudinit/cmd/devel/tests/__init__.py b/cloudinit/cmd/devel/tests/__init__.py
0new file mode 100644102new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cloudinit/cmd/devel/tests/__init__.py
diff --git a/cloudinit/cmd/devel/tests/test_logs.py b/cloudinit/cmd/devel/tests/test_logs.py
1new file mode 100644103new file mode 100644
index 0000000..dc4947c
--- /dev/null
+++ b/cloudinit/cmd/devel/tests/test_logs.py
@@ -0,0 +1,120 @@
1# This file is part of cloud-init. See LICENSE file for license information.
2
3from cloudinit.cmd.devel import logs
4from cloudinit.util import ensure_dir, load_file, subp, write_file
5from cloudinit.tests.helpers import FilesystemMockingTestCase, wrap_and_call
6from datetime import datetime
7import os
8
9
10class TestCollectLogs(FilesystemMockingTestCase):
11
12 def setUp(self):
13 super(TestCollectLogs, self).setUp()
14 self.new_root = self.tmp_dir()
15 self.run_dir = self.tmp_path('run', self.new_root)
16
17 def test_collect_logs_creates_tarfile(self):
18 """collect-logs creates a tarfile with all related cloud-init info."""
19 log1 = self.tmp_path('cloud-init.log', self.new_root)
20 write_file(log1, 'cloud-init-log')
21 log2 = self.tmp_path('cloud-init-output.log', self.new_root)
22 write_file(log2, 'cloud-init-output-log')
23 ensure_dir(self.run_dir)
24 write_file(self.tmp_path('results.json', self.run_dir), 'results')
25 output_tarfile = self.tmp_path('logs.tgz')
26
27 date = datetime.utcnow().date().strftime('%Y-%m-%d')
28 date_logdir = 'cloud-init-logs-{0}'.format(date)
29
30 expected_subp = {
31 ('dpkg-query', '--show', "-f=${Version}\n", 'cloud-init'):
32 '0.7fake\n',
33 ('dmesg',): 'dmesg-out\n',
34 ('journalctl', '-o', 'short-precise'): 'journal-out\n',
35 ('tar', 'czvf', output_tarfile, date_logdir): ''
36 }
37
38 def fake_subp(cmd):
39 cmd_tuple = tuple(cmd)
40 if cmd_tuple not in expected_subp:
41 raise AssertionError(
42 'Unexpected command provided to subp: {0}'.format(cmd))
43 if cmd == ['tar', 'czvf', output_tarfile, date_logdir]:
44 subp(cmd) # Pass through tar cmd so we can check output
45 return expected_subp[cmd_tuple], ''
46
47 wrap_and_call(
48 'cloudinit.cmd.devel.logs',
49 {'subp': {'side_effect': fake_subp},
50 'CLOUDINIT_LOGS': {'new': [log1, log2]},
51 'CLOUDINIT_RUN_DIR': {'new': self.run_dir}},
52 logs.collect_logs, output_tarfile, include_userdata=False)
53 # unpack the tarfile and check file contents
54 subp(['tar', 'zxvf', output_tarfile, '-C', self.new_root])
55 out_logdir = self.tmp_path(date_logdir, self.new_root)
56 self.assertEqual(
57 '0.7fake\n',
58 load_file(os.path.join(out_logdir, 'version')))
59 self.assertEqual(
60 'cloud-init-log',
61 load_file(os.path.join(out_logdir, 'cloud-init.log')))
62 self.assertEqual(
63 'cloud-init-output-log',
64 load_file(os.path.join(out_logdir, 'cloud-init-output.log')))
65 self.assertEqual(
66 'dmesg-out\n',
67 load_file(os.path.join(out_logdir, 'dmesg.txt')))
68 self.assertEqual(
69 'journal-out\n',
70 load_file(os.path.join(out_logdir, 'journal.txt')))
71 self.assertEqual(
72 'results',
73 load_file(
74 os.path.join(out_logdir, 'run', 'cloud-init', 'results.json')))
75
76 def test_collect_logs_includes_optional_userdata(self):
77 """collect-logs include userdata when --include-userdata is set."""
78 log1 = self.tmp_path('cloud-init.log', self.new_root)
79 write_file(log1, 'cloud-init-log')
80 log2 = self.tmp_path('cloud-init-output.log', self.new_root)
81 write_file(log2, 'cloud-init-output-log')
82 userdata = self.tmp_path('user-data.txt', self.new_root)
83 write_file(userdata, 'user-data')
84 ensure_dir(self.run_dir)
85 write_file(self.tmp_path('results.json', self.run_dir), 'results')
86 output_tarfile = self.tmp_path('logs.tgz')
87
88 date = datetime.utcnow().date().strftime('%Y-%m-%d')
89 date_logdir = 'cloud-init-logs-{0}'.format(date)
90
91 expected_subp = {
92 ('dpkg-query', '--show', "-f=${Version}\n", 'cloud-init'):
93 '0.7fake',
94 ('dmesg',): 'dmesg-out\n',
95 ('journalctl', '-o', 'short-precise'): 'journal-out\n',
96 ('tar', 'czvf', output_tarfile, date_logdir): ''
97 }
98
99 def fake_subp(cmd):
100 cmd_tuple = tuple(cmd)
101 if cmd_tuple not in expected_subp:
102 raise AssertionError(
103 'Unexpected command provided to subp: {0}'.format(cmd))
104 if cmd == ['tar', 'czvf', output_tarfile, date_logdir]:
105 subp(cmd) # Pass through tar cmd so we can check output
106 return expected_subp[cmd_tuple], ''
107
108 wrap_and_call(
109 'cloudinit.cmd.devel.logs',
110 {'subp': {'side_effect': fake_subp},
111 'CLOUDINIT_LOGS': {'new': [log1, log2]},
112 'CLOUDINIT_RUN_DIR': {'new': self.run_dir},
113 'USER_DATA_FILE': {'new': userdata}},
114 logs.collect_logs, output_tarfile, include_userdata=True)
115 # unpack the tarfile and check file contents
116 subp(['tar', 'zxvf', output_tarfile, '-C', self.new_root])
117 out_logdir = self.tmp_path(date_logdir, self.new_root)
118 self.assertEqual(
119 'user-data',
120 load_file(os.path.join(out_logdir, 'user-data.txt')))
diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py
index 68563e0..6fb9d9e 100644
--- a/cloudinit/cmd/main.py
+++ b/cloudinit/cmd/main.py
@@ -764,16 +764,25 @@ def main(sysv_args=None):
764 parser_devel = subparsers.add_parser(764 parser_devel = subparsers.add_parser(
765 'devel', help='Run development tools')765 'devel', help='Run development tools')
766766
767 parser_collect_logs = subparsers.add_parser(
768 'collect-logs', help='Collect and tar all cloud-init debug info')
769
767 if sysv_args:770 if sysv_args:
768 # Only load subparsers if subcommand is specified to avoid load cost771 # Only load subparsers if subcommand is specified to avoid load cost
769 if sysv_args[0] == 'analyze':772 if sysv_args[0] == 'analyze':
770 from cloudinit.analyze.__main__ import get_parser as analyze_parser773 from cloudinit.analyze.__main__ import get_parser as analyze_parser
771 # Construct analyze subcommand parser774 # Construct analyze subcommand parser
772 analyze_parser(parser_analyze)775 analyze_parser(parser_analyze)
773 if sysv_args[0] == 'devel':776 elif sysv_args[0] == 'devel':
774 from cloudinit.cmd.devel.parser import get_parser as devel_parser777 from cloudinit.cmd.devel.parser import get_parser as devel_parser
775 # Construct devel subcommand parser778 # Construct devel subcommand parser
776 devel_parser(parser_devel)779 devel_parser(parser_devel)
780 elif sysv_args[0] == 'collect-logs':
781 from cloudinit.cmd.devel.logs import (
782 get_parser as logs_parser, handle_collect_logs_args)
783 logs_parser(parser_collect_logs)
784 parser_collect_logs.set_defaults(
785 action=('collect-logs', handle_collect_logs_args))
777786
778 args = parser.parse_args(args=sysv_args)787 args = parser.parse_args(args=sysv_args)
779788
diff --git a/packages/debian/rules.in b/packages/debian/rules.in
index b87a5e8..4aa907e 100755
--- a/packages/debian/rules.in
+++ b/packages/debian/rules.in
@@ -10,6 +10,7 @@ PYVER ?= python${pyver}
10override_dh_install:10override_dh_install:
11 dh_install11 dh_install
12 install -d debian/cloud-init/etc/rsyslog.d12 install -d debian/cloud-init/etc/rsyslog.d
13 install -d debian/cloud-init/usr/share/apport/package-hooks
13 cp tools/21-cloudinit.conf debian/cloud-init/etc/rsyslog.d/21-cloudinit.conf14 cp tools/21-cloudinit.conf debian/cloud-init/etc/rsyslog.d/21-cloudinit.conf
14 install -D ./tools/Z99-cloud-locale-test.sh debian/cloud-init/etc/profile.d/Z99-cloud-locale-test.sh15 install -D ./tools/Z99-cloud-locale-test.sh debian/cloud-init/etc/profile.d/Z99-cloud-locale-test.sh
15 install -D ./tools/Z99-cloudinit-warnings.sh debian/cloud-init/etc/profile.d/Z99-cloudinit-warnings.sh16 install -D ./tools/Z99-cloudinit-warnings.sh debian/cloud-init/etc/profile.d/Z99-cloudinit-warnings.sh
diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py
index 495bdc9..258a9f0 100644
--- a/tests/unittests/test_cli.py
+++ b/tests/unittests/test_cli.py
@@ -72,18 +72,22 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
7272
73 def test_conditional_subcommands_from_entry_point_sys_argv(self):73 def test_conditional_subcommands_from_entry_point_sys_argv(self):
74 """Subcommands from entry-point are properly parsed from sys.argv."""74 """Subcommands from entry-point are properly parsed from sys.argv."""
75 stdout = six.StringIO()
76 self.patchStdoutAndStderr(stdout=stdout)
77
75 expected_errors = [78 expected_errors = [
76 'usage: cloud-init analyze', 'usage: cloud-init devel']79 'usage: cloud-init analyze', 'usage: cloud-init collect-logs',
77 conditional_subcommands = ['analyze', 'devel']80 'usage: cloud-init devel']
81 conditional_subcommands = ['analyze', 'collect-logs', 'devel']
78 # The cloud-init entrypoint calls main without passing sys_argv82 # The cloud-init entrypoint calls main without passing sys_argv
79 for subcommand in conditional_subcommands:83 for subcommand in conditional_subcommands:
80 with mock.patch('sys.argv', ['cloud-init', subcommand]):84 with mock.patch('sys.argv', ['cloud-init', subcommand, '-h']):
81 try:85 try:
82 cli.main()86 cli.main()
83 except SystemExit as e:87 except SystemExit as e:
84 self.assertEqual(2, e.code) # exit 2 on proper usage docs88 self.assertEqual(0, e.code) # exit 2 on proper -h usage
85 for error_message in expected_errors:89 for error_message in expected_errors:
86 self.assertIn(error_message, self.stderr.getvalue())90 self.assertIn(error_message, stdout.getvalue())
8791
88 def test_analyze_subcommand_parser(self):92 def test_analyze_subcommand_parser(self):
89 """The subcommand cloud-init analyze calls the correct subparser."""93 """The subcommand cloud-init analyze calls the correct subparser."""
@@ -94,6 +98,14 @@ class TestCLI(test_helpers.FilesystemMockingTestCase):
94 for subcommand in expected_subcommands:98 for subcommand in expected_subcommands:
95 self.assertIn(subcommand, error)99 self.assertIn(subcommand, error)
96100
101 def test_collect_logs_subcommand_parser(self):
102 """The subcommand cloud-init collect-logs calls the subparser."""
103 # Provide -h param to collect-logs to avoid having to mock behavior.
104 stdout = six.StringIO()
105 self.patchStdoutAndStderr(stdout=stdout)
106 self._call_main(['cloud-init', 'collect-logs', '-h'])
107 self.assertIn('usage: cloud-init collect-log', stdout.getvalue())
108
97 def test_devel_subcommand_parser(self):109 def test_devel_subcommand_parser(self):
98 """The subcommand cloud-init devel calls the correct subparser."""110 """The subcommand cloud-init devel calls the correct subparser."""
99 self._call_main(['cloud-init', 'devel'])111 self._call_main(['cloud-init', 'devel'])

Subscribers

People subscribed via source and target branches