Merge ~chad.smith/cloud-init:collect-logs into cloud-init:master
- Git
- lp:~chad.smith/cloud-init
- collect-logs
- Merge into master
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) |
Related bugs: |
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
- 077f8e9... by Chad Smith
-
docstrings for functions and make the collect-logs script callable using main()
Server Team CI bot (server-team-bot) wrote : | # |
- 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
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:66dad59e4d2
https:/
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:/
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.
- 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 ProcessExecutio
nError instead of CalledProcessError
Chad Smith (chad.smith) : | # |
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:327f3ee9cf6
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- 04260ae... by Chad Smith
-
unit tests validate that we add a trailing newline to cmd content if it does not already have a newline
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:04260ae927a
https:/
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:/
- 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
Chad Smith (chad.smith) wrote : | # |
For full apport support: here's the wrapper we need to deliver to /usr/share/
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:c83e0bac7ed
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
- dba1e05... by Chad Smith
-
add optional --include-userdata param to collect-logs. make the apport import optional
Server Team CI bot (server-team-bot) wrote : | # |
FAILED: Continuous integration, rev:dba1e05097e
https:/
Executed test runs:
SUCCESS: Checkout
FAILED: Unit & Style Tests
Click here to trigger a rebuild:
https:/
Scott Moser (smoser) wrote : | # |
minor changes suggested.
make those and we're good.
thanks.
Scott Moser (smoser) wrote : | # |
I put this up
https:/
for packaging of the apport lauincher
- ef77fe4... by Chad Smith
-
address review comments on license, dpkg-query structuring, docstrings. And dropped the optional newline append to cmd output
Chad Smith (chad.smith) : | # |
Scott Moser (smoser) : | # |
Server Team CI bot (server-team-bot) wrote : | # |
PASSED: Continuous integration, rev:ef77fe49051
https:/
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:/
Preview Diff
1 | diff --git a/cloudinit/apport.py b/cloudinit/apport.py |
2 | new file mode 100644 |
3 | index 0000000..221f341 |
4 | --- /dev/null |
5 | +++ b/cloudinit/apport.py |
6 | @@ -0,0 +1,105 @@ |
7 | +# Copyright (C) 2017 Canonical Ltd. |
8 | +# |
9 | +# This file is part of cloud-init. See LICENSE file for license information. |
10 | + |
11 | +'''Cloud-init apport interface''' |
12 | + |
13 | +try: |
14 | + from apport.hookutils import ( |
15 | + attach_file, attach_root_command_outputs, root_command_output) |
16 | + has_apport = True |
17 | +except ImportError: |
18 | + has_apport = False |
19 | + |
20 | + |
21 | +KNOWN_CLOUD_NAMES = [ |
22 | + 'Amazon - Ec2', 'AliYun', 'AltCloud', 'Azure', 'Bigstep', 'CloudSigma', |
23 | + 'CloudStack', 'DigitalOcean', 'GCE - Google Compute Engine', 'MAAS', |
24 | + 'NoCloud', 'OpenNebula', 'OpenStack', 'OVF', 'Scaleway', 'SmartOS', |
25 | + 'VMware', 'Other'] |
26 | + |
27 | +# Potentially clear text collected logs |
28 | +CLOUDINIT_LOG = '/var/log/cloud-init.log' |
29 | +CLOUDINIT_OUTPUT_LOG = '/var/log/cloud-init-output.log' |
30 | +USER_DATA_FILE = '/var/lib/cloud/instance/user-data.txt' # Optional |
31 | + |
32 | + |
33 | +def attach_cloud_init_logs(report, ui=None): |
34 | + '''Attach cloud-init logs and tarfile from 'cloud-init collect-logs'.''' |
35 | + attach_root_command_outputs(report, { |
36 | + 'cloud-init-log-warnings': |
37 | + 'egrep -i "warn|error" /var/log/cloud-init.log', |
38 | + 'cloud-init-output.log.txt': 'cat /var/log/cloud-init-output.log'}) |
39 | + root_command_output( |
40 | + ['cloud-init', 'collect-logs', '-t', '/tmp/cloud-init-logs.tgz']) |
41 | + attach_file(report, '/tmp/cloud-init-logs.tgz', 'logs.tgz') |
42 | + |
43 | + |
44 | +def attach_hwinfo(report, ui=None): |
45 | + '''Optionally attach hardware info from lshw.''' |
46 | + prompt = ( |
47 | + 'Your device details (lshw) may be useful to developers when' |
48 | + ' addressing this bug, but gathering it requires admin privileges.' |
49 | + ' Would you like to include this info?') |
50 | + if ui and ui.yesno(prompt): |
51 | + attach_root_command_outputs(report, {'lshw.txt': 'lshw'}) |
52 | + |
53 | + |
54 | +def attach_cloud_info(report, ui=None): |
55 | + '''Prompt for cloud details if available.''' |
56 | + if ui: |
57 | + prompt = 'Is this machine running in a cloud environment?' |
58 | + response = ui.yesno(prompt) |
59 | + if response is None: |
60 | + raise StopIteration # User cancelled |
61 | + if response: |
62 | + prompt = ('Please select the cloud vendor or environment in which' |
63 | + ' this instance is running') |
64 | + response = ui.choice(prompt, KNOWN_CLOUD_NAMES) |
65 | + if response: |
66 | + report['CloudName'] = KNOWN_CLOUD_NAMES[response[0]] |
67 | + else: |
68 | + report['CloudName'] = 'None' |
69 | + |
70 | + |
71 | +def attach_user_data(report, ui=None): |
72 | + '''Optionally provide user-data if desired.''' |
73 | + if ui: |
74 | + prompt = ( |
75 | + 'Your user-data or cloud-config file can optionally be provided' |
76 | + ' from {0} and could be useful to developers when addressing this' |
77 | + ' bug. Do you wish to attach user-data to this bug?'.format( |
78 | + USER_DATA_FILE)) |
79 | + response = ui.yesno(prompt) |
80 | + if response is None: |
81 | + raise StopIteration # User cancelled |
82 | + if response: |
83 | + attach_file(report, USER_DATA_FILE, 'user_data.txt') |
84 | + |
85 | + |
86 | +def add_bug_tags(report): |
87 | + '''Add any appropriate tags to the bug.''' |
88 | + if 'JournalErrors' in report.keys(): |
89 | + errors = report['JournalErrors'] |
90 | + if 'Breaking ordering cycle' in errors: |
91 | + report['Tags'] = 'systemd-ordering' |
92 | + |
93 | + |
94 | +def add_info(report, ui): |
95 | + '''This is an entry point to run cloud-init's apport functionality. |
96 | + |
97 | + Distros which want apport support will have a cloud-init package-hook at |
98 | + /usr/share/apport/package-hooks/cloud-init.py which defines an add_info |
99 | + function and returns the result of cloudinit.apport.add_info(report, ui). |
100 | + ''' |
101 | + if not has_apport: |
102 | + raise RuntimeError( |
103 | + 'No apport imports discovered. Apport functionality disabled') |
104 | + attach_cloud_init_logs(report, ui) |
105 | + attach_hwinfo(report, ui) |
106 | + attach_cloud_info(report, ui) |
107 | + attach_user_data(report, ui) |
108 | + add_bug_tags(report) |
109 | + return True |
110 | + |
111 | +# vi: ts=4 expandtab |
112 | diff --git a/cloudinit/cmd/devel/logs.py b/cloudinit/cmd/devel/logs.py |
113 | new file mode 100644 |
114 | index 0000000..35ca478 |
115 | --- /dev/null |
116 | +++ b/cloudinit/cmd/devel/logs.py |
117 | @@ -0,0 +1,101 @@ |
118 | +# Copyright (C) 2017 Canonical Ltd. |
119 | +# |
120 | +# This file is part of cloud-init. See LICENSE file for license information. |
121 | + |
122 | +"""Define 'collect-logs' utility and handler to include in cloud-init cmd.""" |
123 | + |
124 | +import argparse |
125 | +from cloudinit.util import ( |
126 | + ProcessExecutionError, chdir, copy, ensure_dir, subp, write_file) |
127 | +from cloudinit.temp_utils import tempdir |
128 | +from datetime import datetime |
129 | +import os |
130 | +import shutil |
131 | + |
132 | + |
133 | +CLOUDINIT_LOGS = ['/var/log/cloud-init.log', '/var/log/cloud-init-output.log'] |
134 | +CLOUDINIT_RUN_DIR = '/run/cloud-init' |
135 | +USER_DATA_FILE = '/var/lib/cloud/instance/user-data.txt' # Optional |
136 | + |
137 | + |
138 | +def get_parser(parser=None): |
139 | + """Build or extend and arg parser for collect-logs utility. |
140 | + |
141 | + @param parser: Optional existing ArgumentParser instance representing the |
142 | + collect-logs subcommand which will be extended to support the args of |
143 | + this utility. |
144 | + |
145 | + @returns: ArgumentParser with proper argument configuration. |
146 | + """ |
147 | + if not parser: |
148 | + parser = argparse.ArgumentParser( |
149 | + prog='collect-logs', |
150 | + description='Collect and tar all cloud-init debug info') |
151 | + parser.add_argument( |
152 | + "--tarfile", '-t', default='cloud-init.tar.gz', |
153 | + help=('The tarfile to create containing all collected logs.' |
154 | + ' Default: cloud-init.tar.gz')) |
155 | + parser.add_argument( |
156 | + "--include-userdata", '-u', default=False, action='store_true', |
157 | + dest='userdata', help=( |
158 | + 'Optionally include user-data from {0} which could contain' |
159 | + ' sensitive information.'.format(USER_DATA_FILE))) |
160 | + return parser |
161 | + |
162 | + |
163 | +def _write_command_output_to_file(cmd, filename): |
164 | + """Helper which runs a command and writes output or error to filename.""" |
165 | + try: |
166 | + out, _ = subp(cmd) |
167 | + except ProcessExecutionError as e: |
168 | + write_file(filename, str(e)) |
169 | + else: |
170 | + write_file(filename, out) |
171 | + |
172 | + |
173 | +def collect_logs(tarfile, include_userdata): |
174 | + """Collect all cloud-init logs and tar them up into the provided tarfile. |
175 | + |
176 | + @param tarfile: The path of the tar-gzipped file to create. |
177 | + @param include_userdata: Boolean, true means include user-data. |
178 | + """ |
179 | + tarfile = os.path.abspath(tarfile) |
180 | + date = datetime.utcnow().date().strftime('%Y-%m-%d') |
181 | + log_dir = 'cloud-init-logs-{0}'.format(date) |
182 | + with tempdir(dir='/tmp') as tmp_dir: |
183 | + log_dir = os.path.join(tmp_dir, log_dir) |
184 | + _write_command_output_to_file( |
185 | + ['dpkg-query', '--show', "-f=${Version}\n", 'cloud-init'], |
186 | + os.path.join(log_dir, 'version')) |
187 | + _write_command_output_to_file( |
188 | + ['dmesg'], os.path.join(log_dir, 'dmesg.txt')) |
189 | + _write_command_output_to_file( |
190 | + ['journalctl', '-o', 'short-precise'], |
191 | + os.path.join(log_dir, 'journal.txt')) |
192 | + for log in CLOUDINIT_LOGS: |
193 | + copy(log, log_dir) |
194 | + if include_userdata: |
195 | + copy(USER_DATA_FILE, log_dir) |
196 | + run_dir = os.path.join(log_dir, 'run') |
197 | + ensure_dir(run_dir) |
198 | + shutil.copytree(CLOUDINIT_RUN_DIR, os.path.join(run_dir, 'cloud-init')) |
199 | + with chdir(tmp_dir): |
200 | + subp(['tar', 'czvf', tarfile, log_dir.replace(tmp_dir + '/', '')]) |
201 | + |
202 | + |
203 | +def handle_collect_logs_args(name, args): |
204 | + """Handle calls to 'cloud-init collect-logs' as a subcommand.""" |
205 | + collect_logs(args.tarfile, args.userdata) |
206 | + |
207 | + |
208 | +def main(): |
209 | + """Tool to collect and tar all cloud-init related logs.""" |
210 | + parser = get_parser() |
211 | + handle_collect_logs_args('collect-logs', parser.parse_args()) |
212 | + return 0 |
213 | + |
214 | + |
215 | +if __name__ == '__main__': |
216 | + main() |
217 | + |
218 | +# vi: ts=4 expandtab |
219 | diff --git a/cloudinit/cmd/devel/tests/__init__.py b/cloudinit/cmd/devel/tests/__init__.py |
220 | new file mode 100644 |
221 | index 0000000..e69de29 |
222 | --- /dev/null |
223 | +++ b/cloudinit/cmd/devel/tests/__init__.py |
224 | diff --git a/cloudinit/cmd/devel/tests/test_logs.py b/cloudinit/cmd/devel/tests/test_logs.py |
225 | new file mode 100644 |
226 | index 0000000..dc4947c |
227 | --- /dev/null |
228 | +++ b/cloudinit/cmd/devel/tests/test_logs.py |
229 | @@ -0,0 +1,120 @@ |
230 | +# This file is part of cloud-init. See LICENSE file for license information. |
231 | + |
232 | +from cloudinit.cmd.devel import logs |
233 | +from cloudinit.util import ensure_dir, load_file, subp, write_file |
234 | +from cloudinit.tests.helpers import FilesystemMockingTestCase, wrap_and_call |
235 | +from datetime import datetime |
236 | +import os |
237 | + |
238 | + |
239 | +class TestCollectLogs(FilesystemMockingTestCase): |
240 | + |
241 | + def setUp(self): |
242 | + super(TestCollectLogs, self).setUp() |
243 | + self.new_root = self.tmp_dir() |
244 | + self.run_dir = self.tmp_path('run', self.new_root) |
245 | + |
246 | + def test_collect_logs_creates_tarfile(self): |
247 | + """collect-logs creates a tarfile with all related cloud-init info.""" |
248 | + log1 = self.tmp_path('cloud-init.log', self.new_root) |
249 | + write_file(log1, 'cloud-init-log') |
250 | + log2 = self.tmp_path('cloud-init-output.log', self.new_root) |
251 | + write_file(log2, 'cloud-init-output-log') |
252 | + ensure_dir(self.run_dir) |
253 | + write_file(self.tmp_path('results.json', self.run_dir), 'results') |
254 | + output_tarfile = self.tmp_path('logs.tgz') |
255 | + |
256 | + date = datetime.utcnow().date().strftime('%Y-%m-%d') |
257 | + date_logdir = 'cloud-init-logs-{0}'.format(date) |
258 | + |
259 | + expected_subp = { |
260 | + ('dpkg-query', '--show', "-f=${Version}\n", 'cloud-init'): |
261 | + '0.7fake\n', |
262 | + ('dmesg',): 'dmesg-out\n', |
263 | + ('journalctl', '-o', 'short-precise'): 'journal-out\n', |
264 | + ('tar', 'czvf', output_tarfile, date_logdir): '' |
265 | + } |
266 | + |
267 | + def fake_subp(cmd): |
268 | + cmd_tuple = tuple(cmd) |
269 | + if cmd_tuple not in expected_subp: |
270 | + raise AssertionError( |
271 | + 'Unexpected command provided to subp: {0}'.format(cmd)) |
272 | + if cmd == ['tar', 'czvf', output_tarfile, date_logdir]: |
273 | + subp(cmd) # Pass through tar cmd so we can check output |
274 | + return expected_subp[cmd_tuple], '' |
275 | + |
276 | + wrap_and_call( |
277 | + 'cloudinit.cmd.devel.logs', |
278 | + {'subp': {'side_effect': fake_subp}, |
279 | + 'CLOUDINIT_LOGS': {'new': [log1, log2]}, |
280 | + 'CLOUDINIT_RUN_DIR': {'new': self.run_dir}}, |
281 | + logs.collect_logs, output_tarfile, include_userdata=False) |
282 | + # unpack the tarfile and check file contents |
283 | + subp(['tar', 'zxvf', output_tarfile, '-C', self.new_root]) |
284 | + out_logdir = self.tmp_path(date_logdir, self.new_root) |
285 | + self.assertEqual( |
286 | + '0.7fake\n', |
287 | + load_file(os.path.join(out_logdir, 'version'))) |
288 | + self.assertEqual( |
289 | + 'cloud-init-log', |
290 | + load_file(os.path.join(out_logdir, 'cloud-init.log'))) |
291 | + self.assertEqual( |
292 | + 'cloud-init-output-log', |
293 | + load_file(os.path.join(out_logdir, 'cloud-init-output.log'))) |
294 | + self.assertEqual( |
295 | + 'dmesg-out\n', |
296 | + load_file(os.path.join(out_logdir, 'dmesg.txt'))) |
297 | + self.assertEqual( |
298 | + 'journal-out\n', |
299 | + load_file(os.path.join(out_logdir, 'journal.txt'))) |
300 | + self.assertEqual( |
301 | + 'results', |
302 | + load_file( |
303 | + os.path.join(out_logdir, 'run', 'cloud-init', 'results.json'))) |
304 | + |
305 | + def test_collect_logs_includes_optional_userdata(self): |
306 | + """collect-logs include userdata when --include-userdata is set.""" |
307 | + log1 = self.tmp_path('cloud-init.log', self.new_root) |
308 | + write_file(log1, 'cloud-init-log') |
309 | + log2 = self.tmp_path('cloud-init-output.log', self.new_root) |
310 | + write_file(log2, 'cloud-init-output-log') |
311 | + userdata = self.tmp_path('user-data.txt', self.new_root) |
312 | + write_file(userdata, 'user-data') |
313 | + ensure_dir(self.run_dir) |
314 | + write_file(self.tmp_path('results.json', self.run_dir), 'results') |
315 | + output_tarfile = self.tmp_path('logs.tgz') |
316 | + |
317 | + date = datetime.utcnow().date().strftime('%Y-%m-%d') |
318 | + date_logdir = 'cloud-init-logs-{0}'.format(date) |
319 | + |
320 | + expected_subp = { |
321 | + ('dpkg-query', '--show', "-f=${Version}\n", 'cloud-init'): |
322 | + '0.7fake', |
323 | + ('dmesg',): 'dmesg-out\n', |
324 | + ('journalctl', '-o', 'short-precise'): 'journal-out\n', |
325 | + ('tar', 'czvf', output_tarfile, date_logdir): '' |
326 | + } |
327 | + |
328 | + def fake_subp(cmd): |
329 | + cmd_tuple = tuple(cmd) |
330 | + if cmd_tuple not in expected_subp: |
331 | + raise AssertionError( |
332 | + 'Unexpected command provided to subp: {0}'.format(cmd)) |
333 | + if cmd == ['tar', 'czvf', output_tarfile, date_logdir]: |
334 | + subp(cmd) # Pass through tar cmd so we can check output |
335 | + return expected_subp[cmd_tuple], '' |
336 | + |
337 | + wrap_and_call( |
338 | + 'cloudinit.cmd.devel.logs', |
339 | + {'subp': {'side_effect': fake_subp}, |
340 | + 'CLOUDINIT_LOGS': {'new': [log1, log2]}, |
341 | + 'CLOUDINIT_RUN_DIR': {'new': self.run_dir}, |
342 | + 'USER_DATA_FILE': {'new': userdata}}, |
343 | + logs.collect_logs, output_tarfile, include_userdata=True) |
344 | + # unpack the tarfile and check file contents |
345 | + subp(['tar', 'zxvf', output_tarfile, '-C', self.new_root]) |
346 | + out_logdir = self.tmp_path(date_logdir, self.new_root) |
347 | + self.assertEqual( |
348 | + 'user-data', |
349 | + load_file(os.path.join(out_logdir, 'user-data.txt'))) |
350 | diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py |
351 | index 68563e0..6fb9d9e 100644 |
352 | --- a/cloudinit/cmd/main.py |
353 | +++ b/cloudinit/cmd/main.py |
354 | @@ -764,16 +764,25 @@ def main(sysv_args=None): |
355 | parser_devel = subparsers.add_parser( |
356 | 'devel', help='Run development tools') |
357 | |
358 | + parser_collect_logs = subparsers.add_parser( |
359 | + 'collect-logs', help='Collect and tar all cloud-init debug info') |
360 | + |
361 | if sysv_args: |
362 | # Only load subparsers if subcommand is specified to avoid load cost |
363 | if sysv_args[0] == 'analyze': |
364 | from cloudinit.analyze.__main__ import get_parser as analyze_parser |
365 | # Construct analyze subcommand parser |
366 | analyze_parser(parser_analyze) |
367 | - if sysv_args[0] == 'devel': |
368 | + elif sysv_args[0] == 'devel': |
369 | from cloudinit.cmd.devel.parser import get_parser as devel_parser |
370 | # Construct devel subcommand parser |
371 | devel_parser(parser_devel) |
372 | + elif sysv_args[0] == 'collect-logs': |
373 | + from cloudinit.cmd.devel.logs import ( |
374 | + get_parser as logs_parser, handle_collect_logs_args) |
375 | + logs_parser(parser_collect_logs) |
376 | + parser_collect_logs.set_defaults( |
377 | + action=('collect-logs', handle_collect_logs_args)) |
378 | |
379 | args = parser.parse_args(args=sysv_args) |
380 | |
381 | diff --git a/packages/debian/rules.in b/packages/debian/rules.in |
382 | index b87a5e8..4aa907e 100755 |
383 | --- a/packages/debian/rules.in |
384 | +++ b/packages/debian/rules.in |
385 | @@ -10,6 +10,7 @@ PYVER ?= python${pyver} |
386 | override_dh_install: |
387 | dh_install |
388 | install -d debian/cloud-init/etc/rsyslog.d |
389 | + install -d debian/cloud-init/usr/share/apport/package-hooks |
390 | cp tools/21-cloudinit.conf debian/cloud-init/etc/rsyslog.d/21-cloudinit.conf |
391 | install -D ./tools/Z99-cloud-locale-test.sh debian/cloud-init/etc/profile.d/Z99-cloud-locale-test.sh |
392 | install -D ./tools/Z99-cloudinit-warnings.sh debian/cloud-init/etc/profile.d/Z99-cloudinit-warnings.sh |
393 | diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py |
394 | index 495bdc9..258a9f0 100644 |
395 | --- a/tests/unittests/test_cli.py |
396 | +++ b/tests/unittests/test_cli.py |
397 | @@ -72,18 +72,22 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): |
398 | |
399 | def test_conditional_subcommands_from_entry_point_sys_argv(self): |
400 | """Subcommands from entry-point are properly parsed from sys.argv.""" |
401 | + stdout = six.StringIO() |
402 | + self.patchStdoutAndStderr(stdout=stdout) |
403 | + |
404 | expected_errors = [ |
405 | - 'usage: cloud-init analyze', 'usage: cloud-init devel'] |
406 | - conditional_subcommands = ['analyze', 'devel'] |
407 | + 'usage: cloud-init analyze', 'usage: cloud-init collect-logs', |
408 | + 'usage: cloud-init devel'] |
409 | + conditional_subcommands = ['analyze', 'collect-logs', 'devel'] |
410 | # The cloud-init entrypoint calls main without passing sys_argv |
411 | for subcommand in conditional_subcommands: |
412 | - with mock.patch('sys.argv', ['cloud-init', subcommand]): |
413 | + with mock.patch('sys.argv', ['cloud-init', subcommand, '-h']): |
414 | try: |
415 | cli.main() |
416 | except SystemExit as e: |
417 | - self.assertEqual(2, e.code) # exit 2 on proper usage docs |
418 | + self.assertEqual(0, e.code) # exit 2 on proper -h usage |
419 | for error_message in expected_errors: |
420 | - self.assertIn(error_message, self.stderr.getvalue()) |
421 | + self.assertIn(error_message, stdout.getvalue()) |
422 | |
423 | def test_analyze_subcommand_parser(self): |
424 | """The subcommand cloud-init analyze calls the correct subparser.""" |
425 | @@ -94,6 +98,14 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): |
426 | for subcommand in expected_subcommands: |
427 | self.assertIn(subcommand, error) |
428 | |
429 | + def test_collect_logs_subcommand_parser(self): |
430 | + """The subcommand cloud-init collect-logs calls the subparser.""" |
431 | + # Provide -h param to collect-logs to avoid having to mock behavior. |
432 | + stdout = six.StringIO() |
433 | + self.patchStdoutAndStderr(stdout=stdout) |
434 | + self._call_main(['cloud-init', 'collect-logs', '-h']) |
435 | + self.assertIn('usage: cloud-init collect-log', stdout.getvalue()) |
436 | + |
437 | def test_devel_subcommand_parser(self): |
438 | """The subcommand cloud-init devel calls the correct subparser.""" |
439 | self._call_main(['cloud-init', 'devel']) |
PASSED: Continuous integration, rev:077f8e937c1 81c94373a013309 7f2d5b904b3431 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 285/
https:/
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: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 285/rebuild
https:/