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