Merge lp:~veebers/juju-ci-tools/initial-log-forwarding-tests into lp:juju-ci-tools
- initial-log-forwarding-tests
- Merge into trunk
Status: | Approved |
---|---|
Approved by: | Martin Packman |
Approved revision: | 1510 |
Proposed branch: | lp:~veebers/juju-ci-tools/initial-log-forwarding-tests |
Merge into: | lp:juju-ci-tools |
Diff against target: |
707 lines (+661/-0) 7 files modified
assess_log_forward.py (+282/-0) certificates.py (+139/-0) jujupy.py (+7/-0) log_check.py (+55/-0) tests/test_assess_log_forward.py (+96/-0) tests/test_jujupy.py (+29/-0) tests/test_log_check.py (+53/-0) |
To merge this branch: | bzr merge lp:~veebers/juju-ci-tools/initial-log-forwarding-tests |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Packman (community) | Approve | ||
Review via email: mp+300039@code.launchpad.net |
Commit message
Add initial test for log forwarding feature
Description of the change
Adds initial test for the log-forwarding feature.
Handles creation of certificates tied to the rsylog sync as required by the feature
Bootstraps 2 environments, one being the rsyslog sink for logs the other being the emitter of logs.
Has a basic check that exercises the feature, there is further tests to be added but this MP is getting large as it is.
I added a new file 'certificates.py'. I was tempted to create a python package but this isn't a small step and wanted to converse with the team if it was the direction we wanted to go.
I.e. I would propose something like this initially:
juju_ci_tools/
__init__.py
certificates.py
- 1496. By Christopher Lee
-
Fix assertion messages.
- 1497. By Christopher Lee
-
Remove yet to be used methods.
Martin Packman (gz) wrote : | # |
This time with the comments...
Christopher Lee (veebers) wrote : | # |
Responded to comments ans made some changes. Still have a couple of changes to make.
- 1498. By Christopher Lee
-
Grammar fix and import fix.
- 1499. By Christopher Lee
-
Better naming for boostrap managers.
- 1500. By Christopher Lee
-
Change certificate details to not be a getter function.
- 1501. By Christopher Lee
-
Merge trunk
- 1502. By Christopher Lee
-
Rename and reduce assess function
- 1503. By Christopher Lee
-
Using python script run on remote machine for logs check.
- 1504. By Christopher Lee
-
Add failling test for get_controller_uuid
- 1505. By Christopher Lee
-
Make test pass for added get_controller_uuid
- 1506. By Christopher Lee
-
Modify test to use get_controller_uuid
- 1507. By Christopher Lee
-
Cater for ipv6 addresses + port. Incl. tests.
- 1508. By Christopher Lee
-
Fix remote python script.
Martin Packman (gz) wrote : | # |
Couple of notes from eyeballing this again, see inline comments.
Christopher Lee (veebers) wrote : | # |
Made the suggested changes.
- 1509. By Christopher Lee
-
Put check logic into separate file with tests.
Martin Packman (gz) wrote : | # |
Some comments on how to write the new code a little better.
- 1510. By Christopher Lee
-
Improved file existence checks and handling.
Martin Packman (gz) wrote : | # |
Thanks! Lets land it.
Unmerged revisions
- 1510. By Christopher Lee
-
Improved file existence checks and handling.
Preview Diff
1 | === added file 'assess_log_forward.py' |
2 | --- assess_log_forward.py 1970-01-01 00:00:00 +0000 |
3 | +++ assess_log_forward.py 2016-08-04 22:17:14 +0000 |
4 | @@ -0,0 +1,282 @@ |
5 | +#!/usr/bin/env python |
6 | +"""Test Juju's log forwarding feature. |
7 | + |
8 | +Log forwarding allows a controller to forward syslog from all models of a |
9 | +controller to a syslog host via TCP (using SSL). |
10 | + |
11 | +""" |
12 | + |
13 | +from __future__ import print_function |
14 | + |
15 | +import argparse |
16 | +import logging |
17 | +import os |
18 | +import re |
19 | +import sys |
20 | +import socket |
21 | +import subprocess |
22 | +from textwrap import dedent |
23 | + |
24 | +from assess_model_migration import get_bootstrap_managers |
25 | +import certificates |
26 | +from utility import ( |
27 | + add_basic_testing_arguments, |
28 | + configure_logging, |
29 | + JujuAssertionError, |
30 | + temp_dir, |
31 | +) |
32 | + |
33 | + |
34 | +__metaclass__ = type |
35 | + |
36 | + |
37 | +log = logging.getLogger("assess_log_forward") |
38 | + |
39 | + |
40 | +def assess_log_forward(bs_dummy, bs_rsyslog, upload_tools): |
41 | + """Ensure logs are forwarded after forwarding enabled after bootstrapping. |
42 | + |
43 | + Given 2 controllers set rsyslog and dummy: |
44 | + - setup rsyslog with secure details |
45 | + - Enable log forwarding on dummy |
46 | + - Ensure intial logs are present in the rsyslog sinks logs |
47 | + |
48 | + """ |
49 | + with bs_rsyslog.booted_context(upload_tools): |
50 | + log.info('Bootstrapped rsyslog environment') |
51 | + rsyslog = bs_rsyslog.client |
52 | + rsyslog_details = deploy_rsyslog(rsyslog) |
53 | + |
54 | + update_client_config(bs_dummy.client, rsyslog_details) |
55 | + |
56 | + with bs_dummy.existing_booted_context(upload_tools): |
57 | + log.info('Bootstrapped dummy environment') |
58 | + dummy_client = bs_dummy.client |
59 | + |
60 | + # ensure turning on log-fwd emits logs to the client. |
61 | + # Should I ensure that nothing turns up beforehand |
62 | + ensure_enabling_log_forwarding_forwards_previous_messages( |
63 | + rsyslog, dummy_client) |
64 | + |
65 | + |
66 | +def ensure_enabling_log_forwarding_forwards_previous_messages(rsyslog, dummy): |
67 | + """Assert when enabled log forwarding forwards messages from log start.""" |
68 | + uuid = dummy.get_controller_uuid() |
69 | + |
70 | + enable_log_forwarding(dummy) |
71 | + assert_initial_message_forwarded(rsyslog, uuid) |
72 | + |
73 | + |
74 | +def assert_initial_message_forwarded(rsyslog, uuid): |
75 | + """Assert that mention of the sources logs appear in the sinks logging. |
76 | + |
77 | + Given a rsyslog sink and an output source assert that logging details from |
78 | + the source appear in the sinks logging. |
79 | + Attempt a check over a period of time (10 seconds). |
80 | + |
81 | + :returns: As soon as the expected message appears. |
82 | + :raises JujuAssertionError: If the expected message does not appear in the |
83 | + given timeframe. |
84 | + :raises JujuAssertionError: If the log message check fails in an unexpected |
85 | + way. |
86 | + |
87 | + """ |
88 | + check_string = get_assert_regex(uuid) |
89 | + unit_machine = 'rsyslog/0' |
90 | + |
91 | + remote_script_path = create_check_script_on_unit(rsyslog, unit_machine) |
92 | + |
93 | + try: |
94 | + rsyslog.juju( |
95 | + 'ssh', |
96 | + ( |
97 | + unit_machine, |
98 | + 'sudo', |
99 | + 'python', |
100 | + remote_script_path, |
101 | + check_string, |
102 | + '/var/log/syslog')) |
103 | + log.info('Check script passed on target machine.') |
104 | + except subprocess.CalledProcessError: |
105 | + # This is where a failure happened |
106 | + raise JujuAssertionError('Forwarded log message never appeared.') |
107 | + |
108 | + |
109 | +def create_check_script_on_unit(client, unit_machine): |
110 | + script_path = os.path.join(os.path.dirname(__file__), 'log_check.py') |
111 | + script_dest_path = os.path.join('/tmp', os.path.basename(script_path)) |
112 | + client.juju( |
113 | + 'scp', |
114 | + (script_path, '{}:{}'.format(unit_machine, script_dest_path))) |
115 | + return script_dest_path |
116 | + |
117 | + |
118 | +def get_assert_regex(raw_uuid, message=None): |
119 | + """Create a regex string to check syslog file. |
120 | + |
121 | + If message is supplied it is expected to be escaped as needed (i.e. spaces) |
122 | + no further massaging will be done to the message string. |
123 | + |
124 | + """ |
125 | + # Maybe over simplified removing the last 8 characters |
126 | + uuid = re.escape(raw_uuid) |
127 | + short_uuid = re.escape(raw_uuid[:-8]) |
128 | + date_check = '[A-Z][a-z]{,2}\ +[0-9]+\ +[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}' |
129 | + machine = 'machine-0.{}'.format(uuid) |
130 | + agent = 'jujud-machine-agent-{}'.format(short_uuid) |
131 | + message = message or '.*' |
132 | + |
133 | + return '"^{datecheck}\ {machine}\ {agent}\ {message}$"'.format( |
134 | + datecheck=date_check, |
135 | + machine=machine, |
136 | + agent=agent, |
137 | + message=message) |
138 | + |
139 | + |
140 | +def enable_log_forwarding(client): |
141 | + client.juju( |
142 | + 'set-model-config', |
143 | + ('-m', 'controller', 'logforward-enabled=true'), include_e=False) |
144 | + |
145 | + |
146 | +def update_client_config(client, rsyslog_details): |
147 | + client.env.config['logforward-enabled'] = False |
148 | + client.env.config.update(rsyslog_details) |
149 | + |
150 | + |
151 | +def deploy_rsyslog(client): |
152 | + """Deploy and setup the rsyslog charm on client. |
153 | + |
154 | + :returns: Configuration details needed: cert, ca, key and ip:port. |
155 | + |
156 | + """ |
157 | + app_name = 'rsyslog' |
158 | + client.deploy('rsyslog', (app_name)) |
159 | + client.wait_for_started() |
160 | + client.juju('set-config', (app_name, 'protocol="tcp"')) |
161 | + client.juju('expose', app_name) |
162 | + |
163 | + return setup_tls_rsyslog(client, app_name) |
164 | + |
165 | + |
166 | +def setup_tls_rsyslog(client, app_name): |
167 | + unit_machine = '{}/0'.format(app_name) |
168 | + |
169 | + ip_address = get_unit_ipaddress(client, unit_machine) |
170 | + |
171 | + client.juju( |
172 | + 'ssh', |
173 | + (unit_machine, 'sudo apt-get install rsyslog-gnutls')) |
174 | + |
175 | + with temp_dir() as config_dir: |
176 | + install_rsyslog_config(client, config_dir, unit_machine) |
177 | + rsyslog_details = install_certificates( |
178 | + client, config_dir, ip_address, unit_machine) |
179 | + |
180 | + # restart rsyslog to take into affect all changes |
181 | + client.juju('ssh', (unit_machine, 'sudo', 'service', 'rsyslog', 'restart')) |
182 | + |
183 | + return rsyslog_details |
184 | + |
185 | + |
186 | +def install_certificates(client, config_dir, ip_address, unit_machine): |
187 | + cert, key = certificates.create_certificate(config_dir, ip_address) |
188 | + |
189 | + # Write contents to file to scp across |
190 | + ca_file = os.path.join(config_dir, 'ca.pem') |
191 | + with open(ca_file, 'wt') as f: |
192 | + f.write(certificates.ca_pem_contents) |
193 | + |
194 | + scp_command = ( |
195 | + '--', cert, key, ca_file, '{unit}:/home/ubuntu/'.format( |
196 | + unit=unit_machine)) |
197 | + client.juju('scp', scp_command) |
198 | + |
199 | + return _get_rsyslog_details(cert, key, ip_address) |
200 | + |
201 | + |
202 | +def _get_rsyslog_details(cert_file, key_file, ip_address): |
203 | + with open(cert_file, 'rt') as f: |
204 | + cert_contents = f.read() |
205 | + with open(key_file, 'rt') as f: |
206 | + key_contents = f.read() |
207 | + |
208 | + return { |
209 | + 'syslog-host': '{}'.format(add_port_to_ip(ip_address, '10514')), |
210 | + 'syslog-ca-cert': certificates.ca_pem_contents, |
211 | + 'syslog-client-cert': cert_contents, |
212 | + 'syslog-client-key': key_contents |
213 | + } |
214 | + |
215 | + |
216 | +def add_port_to_ip(ip_address, port): |
217 | + """Return an ipv4/ipv6 address with port added to `ip_address`.""" |
218 | + try: |
219 | + socket.inet_aton(ip_address) |
220 | + return '{}:{}'.format(ip_address, port) |
221 | + except socket.error: |
222 | + try: |
223 | + socket.inet_pton(socket.AF_INET6, ip_address) |
224 | + return '[{}]:{}'.format(ip_address, port) |
225 | + except socket.error: |
226 | + pass |
227 | + raise ValueError( |
228 | + 'IP Address "{}" is neither an ipv4 or ipv6 address.'.format( |
229 | + ip_address)) |
230 | + |
231 | + |
232 | +def install_rsyslog_config(client, config_dir, unit_machine): |
233 | + config = write_rsyslog_config_file(config_dir) |
234 | + client.juju('scp', (config, '{unit}:/tmp'.format(unit=unit_machine))) |
235 | + client.juju( |
236 | + 'ssh', |
237 | + (unit_machine, 'sudo', 'mv', '/tmp/{}'.format( |
238 | + os.path.basename(config)), '/etc/rsyslog.d/')) |
239 | + |
240 | + |
241 | +def get_unit_ipaddress(client, unit_name): |
242 | + status = client.get_status() |
243 | + return status.get_unit(unit_name)['public-address'] |
244 | + |
245 | + |
246 | +def write_rsyslog_config_file(tmp_dir): |
247 | + """Write rsyslog config file to `tmp_dir`/10-securelogging.conf.""" |
248 | + config = dedent("""\ |
249 | + # make gtls driver the default |
250 | + $DefaultNetstreamDriver gtls |
251 | + |
252 | + # certificate files |
253 | + $DefaultNetstreamDriverCAFile /home/ubuntu/ca.pem |
254 | + $DefaultNetstreamDriverCertFile /home/ubuntu/cert.pem |
255 | + $DefaultNetstreamDriverKeyFile /home/ubuntu/key.pem |
256 | + |
257 | + $ModLoad imtcp # load TCP listener |
258 | + $InputTCPServerStreamDriverAuthMode x509/name |
259 | + $InputTCPServerStreamDriverPermittedPeer anyServer |
260 | + $InputTCPServerStreamDriverMode 1 # run driver in TLS-only mode |
261 | + $InputTCPServerRun 10514 # port 10514 |
262 | + """) |
263 | + config_path = os.path.join(tmp_dir, '10-securelogging.conf') |
264 | + with open(config_path, 'wt') as f: |
265 | + f.write(config) |
266 | + return config_path |
267 | + |
268 | + |
269 | +def parse_args(argv): |
270 | + """Parse all arguments.""" |
271 | + parser = argparse.ArgumentParser( |
272 | + description="Test log forwarding of logs.") |
273 | + add_basic_testing_arguments(parser) |
274 | + return parser.parse_args(argv) |
275 | + |
276 | + |
277 | +def main(argv=None): |
278 | + args = parse_args(argv) |
279 | + configure_logging(args.verbose) |
280 | + bs_dummy, bs_rsyslog = get_bootstrap_managers(args) |
281 | + assess_log_forward(bs_dummy, bs_rsyslog, args.upload_tools) |
282 | + return 0 |
283 | + |
284 | + |
285 | +if __name__ == '__main__': |
286 | + sys.exit(main()) |
287 | |
288 | === added file 'certificates.py' |
289 | --- certificates.py 1970-01-01 00:00:00 +0000 |
290 | +++ certificates.py 2016-08-04 22:17:14 +0000 |
291 | @@ -0,0 +1,139 @@ |
292 | +from OpenSSL import crypto |
293 | +import os |
294 | +from textwrap import dedent |
295 | + |
296 | + |
297 | +def create_certificate(target_dir, ip_address): |
298 | + """Generate a cert and key file incl. IP SAN for `ip_address` |
299 | + |
300 | + Creates a cert.pem and key.pem file signed with a known ca cert. |
301 | + The generated cert will contain a IP SAN (subject alternative name) that |
302 | + includes the ip address of the server. This is required for log-forwarding. |
303 | + |
304 | + :return: tuple containing generated cert, key filepath pair |
305 | + |
306 | + """ |
307 | + ip_address = 'IP:{}'.format(ip_address) |
308 | + |
309 | + key = crypto.PKey() |
310 | + key.generate_key(crypto.TYPE_RSA, 2048) |
311 | + |
312 | + csr_contents = generate_csr(target_dir, key, ip_address) |
313 | + req = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_contents) |
314 | + |
315 | + ca_cert = crypto.load_certificate( |
316 | + crypto.FILETYPE_PEM, ca_pem_contents) |
317 | + ca_key = crypto.load_privatekey( |
318 | + crypto.FILETYPE_PEM, ca_key_pem_contents) |
319 | + |
320 | + cert = crypto.X509() |
321 | + cert.set_version(0x2) |
322 | + cert.set_subject(req.get_subject()) |
323 | + cert.set_serial_number(1) |
324 | + cert.gmtime_adj_notBefore(0) |
325 | + cert.gmtime_adj_notAfter(24 * 60 * 60) |
326 | + cert.set_issuer(ca_cert.get_subject()) |
327 | + cert.set_pubkey(req.get_pubkey()) |
328 | + cert.add_extensions([ |
329 | + crypto.X509Extension('subjectAltName', False, ip_address), |
330 | + crypto.X509Extension( |
331 | + 'extendedKeyUsage', False, 'clientAuth, serverAuth'), |
332 | + crypto.X509Extension( |
333 | + 'keyUsage', True, 'keyEncipherment'), |
334 | + ]) |
335 | + cert.sign(ca_key, "sha256") |
336 | + |
337 | + cert_filepath = os.path.join(target_dir, 'cert.pem') |
338 | + with open(cert_filepath, 'wt') as f: |
339 | + f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) |
340 | + |
341 | + key_filepath = os.path.join(target_dir, 'key.pem') |
342 | + with open(key_filepath, 'wt') as f: |
343 | + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) |
344 | + |
345 | + return (cert_filepath, key_filepath) |
346 | + |
347 | + |
348 | +def generate_csr(target_dir, key, ip_address): |
349 | + req = crypto.X509Req() |
350 | + req.set_version(0x2) |
351 | + req.get_subject().CN = "anyServer" |
352 | + # Add the IP SAN |
353 | + req.add_extensions([ |
354 | + crypto.X509Extension("subjectAltName", False, ip_address) |
355 | + ]) |
356 | + req.set_pubkey(key) |
357 | + req.sign(key, "sha256") |
358 | + |
359 | + return crypto.dump_certificate_request(crypto.FILETYPE_PEM, req) |
360 | + |
361 | + |
362 | +ca_pem_contents = dedent("""\ |
363 | + -----BEGIN CERTIFICATE----- |
364 | + MIIEFTCCAn2gAwIBAgIBBzANBgkqhkiG9w0BAQsFADAjMRIwEAYDVQQDEwlhbnlT |
365 | + ZXJ2ZXIxDTALBgNVBAoTBGp1anUwHhcNMTYwNzExMDQyOTM1WhcNMjYwNzA5MDQy |
366 | + OTM1WjAjMRIwEAYDVQQDEwlhbnlTZXJ2ZXIxDTALBgNVBAoTBGp1anUwggGiMA0G |
367 | + CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCn6OxY33yAirABoE4UaZJBOnQORIzC |
368 | + 125R71E2TG5gSHjHKA70L0C3dgyWhW9wcyhUbXBuz8Oep2J7kHvzuUPw2AWXI+Y2 |
369 | + c0afWVqfj5kuyUpGhXsqylyf7NDPFs8hwGA6ZCFS3oUAvX8awsVucklxGeZNXZNK |
370 | + ZFilXKaX1Z3soORmKFZzVfDRqDuofZ2E0tmPh9C5gQ8qswjdBnTrj+0rCnvNekO0 |
371 | + aND6AlkBHU+87pvcax0uUF6PYkXxPikKk1ftCQSII5oB5ksAtRpcZsYl5hT3U/t1 |
372 | + DOA7c35RuIx7ogkcXP9jZ6J2tkmX+GMtUF29KEEnVCht32VDX+C3yS6lbfQB4oDt |
373 | + Yp3wXRY/LXTW7XTUrhoXB4nkYbw59gis5Cr7zDtUpiWFVYgy/kbxalljSM4N3w2i |
374 | + dtfxJHYjTfK98124qbCBb4A4ZNBJE2jy//lSIcIMXJv1LXQtTqR4rO1j6TBurohF |
375 | + NmUYpy3Zv7gn2CkfX6QfNFIj8elKT6dd+RUCAwEAAaNUMFIwDwYDVR0TAQH/BAUw |
376 | + AwEB/zAPBgNVHREECDAGhwQKwoylMA8GA1UdDwEB/wQFAwMHBAAwHQYDVR0OBBYE |
377 | + FP+v8GAqHiUCIygXbwWzbUhl/22DMA0GCSqGSIb3DQEBCwUAA4IBgQBVYKeT1O2M |
378 | + U3OPOy0IwqcA1/64rS1GlRmiw+papmjsy3aru03r8igahnbFd7wQawHaCScXbI/n |
379 | + OAPT4PDGXn6b71t4uHwWaM8wde159RO3G32N/VfhV6LPRUQunmAZh5QcJK6wWpYu |
380 | + B1f0dPkU+Q1AfX12oTOX/ld2/o7jaVswHoHoW6K2WQmwzlRQ953J+RJ7jXfrYDKl |
381 | + OAp3Hb69wAN4Ayc1s92iYUwV5q8UaHQoskHOLWJu964yFBHL8SLe6TLD+Jjv05Mc |
382 | + Ca7NKq/n25VTDNNaXl5MCNZ048m/GGHfktxxCddaF2grhC5HTUetwkq026PE0Wcq |
383 | + P+cDrIq6uTA25QqyBYistSa/7z2o0NBi56ySRqxlP2J2TPFZyOb+ZiA4EgYY5no5 |
384 | + u2E+WuKZLVWl7eaQYOHgfYzFf3CvalSBwIjNynRwD/2Ebk7K29GPrIugb3V2+Vwh |
385 | + rltUXOHUkFGjEHIhr8zixfCxh5OzPJMnJwCZZRYzMO0/0Gw7ll9DmH0= |
386 | + -----END CERTIFICATE----- |
387 | + """) |
388 | + |
389 | + |
390 | +ca_key_pem_contents = dedent("""\ |
391 | + -----BEGIN RSA PRIVATE KEY----- |
392 | + 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 |
429 | + -----END RSA PRIVATE KEY----- |
430 | + """) |
431 | |
432 | === modified file 'jujupy.py' |
433 | --- jujupy.py 2016-08-03 21:23:26 +0000 |
434 | +++ jujupy.py 2016-08-04 22:17:14 +0000 |
435 | @@ -1476,6 +1476,13 @@ |
436 | env = self.env.clone(model_name=name) |
437 | return self.clone(env=env) |
438 | |
439 | + def get_controller_uuid(self): |
440 | + name = self.env.controller.name |
441 | + output_yaml = self.get_juju_output( |
442 | + 'show-controller', '--format', 'yaml', include_e=False) |
443 | + output = yaml.safe_load(output_yaml) |
444 | + return output[name]['details']['uuid'] |
445 | + |
446 | def get_controller_client(self): |
447 | """Return a client for the controller model. May return self. |
448 | |
449 | |
450 | === added file 'log_check.py' |
451 | --- log_check.py 1970-01-01 00:00:00 +0000 |
452 | +++ log_check.py 2016-08-04 22:17:14 +0000 |
453 | @@ -0,0 +1,55 @@ |
454 | +#!/usr/bin/env python |
455 | +"""Simple looped check over provided file for regex content.""" |
456 | +from __future__ import print_function |
457 | + |
458 | +import argparse |
459 | +import os |
460 | +import subprocess |
461 | +import sys |
462 | +import time |
463 | + |
464 | + |
465 | +class check_result: |
466 | + success = 0 |
467 | + failure = 1 |
468 | + exception = 2 |
469 | + |
470 | + |
471 | +def check_file(check_string, file_path): |
472 | + print('Checking for:\n{}'.format(check_string)) |
473 | + for _ in range(0, 10): |
474 | + try: |
475 | + with open(file_path, 'r') as f: |
476 | + subprocess.check_call(['sudo', 'egrep', check_string], stdin=f) |
477 | + print('Log content found. No need to continue.') |
478 | + return check_result.success |
479 | + except subprocess.CalledProcessError as e: |
480 | + if e.returncode == 1: |
481 | + time.sleep(1) |
482 | + else: |
483 | + return check_result.exception |
484 | + print('Unexpected error with file check.') |
485 | + return check_result.failure |
486 | + |
487 | + |
488 | +def parse_args(argv=None): |
489 | + parser = argparse.ArgumentParser( |
490 | + description='File content check.') |
491 | + parser.add_argument( |
492 | + 'regex', help='Regex string to check file with.') |
493 | + parser.add_argument( |
494 | + 'file_path', help='Path to file to check.') |
495 | + return parser.parse_args(argv) |
496 | + |
497 | + |
498 | +def main(argv=None): |
499 | + args = parse_args(argv) |
500 | + try: |
501 | + return check_file(args.regex, args.file_path) |
502 | + except EnvironmentError as e: |
503 | + print('Cannot open file "{}": {}'.format(args.file_path, str(e))) |
504 | + return check_result.exception |
505 | + |
506 | + |
507 | +if __name__ == '__main__': |
508 | + sys.exit(main()) |
509 | |
510 | === added file 'tests/test_assess_log_forward.py' |
511 | --- tests/test_assess_log_forward.py 1970-01-01 00:00:00 +0000 |
512 | +++ tests/test_assess_log_forward.py 2016-08-04 22:17:14 +0000 |
513 | @@ -0,0 +1,96 @@ |
514 | +"""Tests for assess_log_forward module.""" |
515 | + |
516 | +import argparse |
517 | +from mock import patch |
518 | +import re |
519 | +import StringIO |
520 | + |
521 | +import assess_log_forward as alf |
522 | +from tests import ( |
523 | + parse_error, |
524 | + TestCase, |
525 | +) |
526 | + |
527 | + |
528 | +class TestParseArgs(TestCase): |
529 | + |
530 | + def test_common_args(self): |
531 | + args = alf.parse_args( |
532 | + ['an-env', '/bin/juju', '/tmp/logs', 'an-env-mod']) |
533 | + self.assertEqual( |
534 | + args, |
535 | + argparse.Namespace( |
536 | + env='an-env', |
537 | + juju_bin='/bin/juju', |
538 | + temp_env_name='an-env-mod', |
539 | + debug=False, |
540 | + agent_stream=None, |
541 | + agent_url=None, |
542 | + bootstrap_host=None, |
543 | + keep_env=False, |
544 | + logs='/tmp/logs', |
545 | + machine=[], |
546 | + region=None, |
547 | + series=None, |
548 | + upload_tools=False, |
549 | + verbose=20)) |
550 | + |
551 | + def test_help(self): |
552 | + fake_stdout = StringIO.StringIO() |
553 | + with parse_error(self) as fake_stderr: |
554 | + with patch("sys.stdout", fake_stdout): |
555 | + alf.parse_args(["--help"]) |
556 | + self.assertEqual("", fake_stderr.getvalue()) |
557 | + self.assertIn( |
558 | + 'Test log forwarding of logs.', |
559 | + fake_stdout.getvalue()) |
560 | + |
561 | + |
562 | +class TestAssertRegex(TestCase): |
563 | + |
564 | + def test_default_message_check(self): |
565 | + self.assertTrue( |
566 | + alf.get_assert_regex('').endswith('.*$"')) |
567 | + |
568 | + def test_fails_when_uuid_doesnt_match(self): |
569 | + uuid = 'fail' |
570 | + check = alf.get_assert_regex(uuid) |
571 | + failing_string = 'abc' |
572 | + |
573 | + self.assertIsNone(re.search(check, failing_string)) |
574 | + |
575 | + def test_succeeds_with_matching_uuid(self): |
576 | + uuid = '1234567812345678' |
577 | + short_uuid = uuid[:-8] |
578 | + check = alf.get_assert_regex( |
579 | + uuid, message='abc').rstrip('"').strip('"') |
580 | + success = 'Jul 13 00:00:00 machine-0.{} '\ |
581 | + 'jujud-machine-agent-{} abc'.format( |
582 | + uuid, short_uuid) |
583 | + |
584 | + self.assertIsNotNone( |
585 | + re.search(check, success)) |
586 | + |
587 | + |
588 | +class TestAddPortToIP(TestCase): |
589 | + def test_adds_port_to_ipv4(self): |
590 | + ip_address = '192.168.1.1' |
591 | + port = '123' |
592 | + expected = '192.168.1.1:123' |
593 | + self.assertEqual( |
594 | + alf.add_port_to_ip(ip_address, port), |
595 | + expected |
596 | + ) |
597 | + |
598 | + def test_adds_port_to_ipv6(self): |
599 | + ip_address = '1fff:0:a88:85a3::ac1f' |
600 | + port = '123' |
601 | + expected = '[1fff:0:a88:85a3::ac1f]:123' |
602 | + self.assertEqual( |
603 | + alf.add_port_to_ip(ip_address, port), |
604 | + expected |
605 | + ) |
606 | + |
607 | + def test_raises_ValueError_on_invalid_address(self): |
608 | + with self.assertRaises(ValueError): |
609 | + alf.add_port_to_ip('abc', 'abc') |
610 | |
611 | === modified file 'tests/test_jujupy.py' |
612 | --- tests/test_jujupy.py 2016-08-03 21:23:26 +0000 |
613 | +++ tests/test_jujupy.py 2016-08-04 22:17:14 +0000 |
614 | @@ -2343,6 +2343,35 @@ |
615 | controller_name = client.get_controller_model_name() |
616 | self.assertEqual('controller', controller_name) |
617 | |
618 | + def test_get_controller_uuid_returns_uuid(self): |
619 | + controller_uuid = 'eb67e1eb-6c54-45f5-8b6a-b6243be97202' |
620 | + yaml_string = dedent("""\ |
621 | + foo: |
622 | + details: |
623 | + uuid: {uuid} |
624 | + api-endpoints: ['10.194.140.213:17070'] |
625 | + cloud: lxd |
626 | + region: localhost |
627 | + models: |
628 | + controller: |
629 | + uuid: {uuid} |
630 | + default: |
631 | + uuid: 772cdd39-b454-4bd5-8704-dc9aa9ff1750 |
632 | + current-model: default |
633 | + account: |
634 | + user: admin@local |
635 | + bootstrap-config: |
636 | + config: |
637 | + cloud: lxd |
638 | + cloud-type: lxd |
639 | + region: localhost""".format(uuid=controller_uuid)) |
640 | + client = EnvJujuClient(JujuData('foo'), None, None) |
641 | + with patch.object(client, 'get_juju_output', return_value=yaml_string): |
642 | + self.assertEqual( |
643 | + client.get_controller_uuid(), |
644 | + controller_uuid |
645 | + ) |
646 | + |
647 | def test_get_controller_client(self): |
648 | client = EnvJujuClient( |
649 | JujuData('foo', {'bar': 'baz'}, 'myhome'), None, None) |
650 | |
651 | === added file 'tests/test_log_check.py' |
652 | --- tests/test_log_check.py 1970-01-01 00:00:00 +0000 |
653 | +++ tests/test_log_check.py 2016-08-04 22:17:14 +0000 |
654 | @@ -0,0 +1,53 @@ |
655 | +"""Tests for log_check script.""" |
656 | + |
657 | +from mock import patch |
658 | +import subprocess |
659 | + |
660 | +import log_check as lc |
661 | +from tests import TestCase |
662 | + |
663 | + |
664 | +class TestCheckFile(TestCase): |
665 | + |
666 | + regex = 'test123' |
667 | + file_path = '/tmp/file.log' |
668 | + |
669 | + def test_calls_check_call(self): |
670 | + with patch.object(lc.subprocess, 'check_call') as m_checkcall: |
671 | + lc.check_file(self.regex, self.file_path) |
672 | + |
673 | + m_checkcall.assert_called_once_with( |
674 | + ['sudo', 'egrep', self.regex, self.file_path]) |
675 | + |
676 | + def test_fails_after_attempting_multiple_times(self): |
677 | + with patch.object(lc.subprocess, 'check_call') as m_checkcall: |
678 | + m_checkcall.side_effect = subprocess.CalledProcessError( |
679 | + 1, ['sudo', 'egrep', self.regex, self.file_path]) |
680 | + with patch.object(lc.time, 'sleep') as m_sleep: |
681 | + self.assertEqual( |
682 | + lc.check_file(self.regex, self.file_path), |
683 | + lc.check_result.failure) |
684 | + self.assertEqual(m_sleep.call_count, 10) |
685 | + |
686 | + def test_fails_when_meeting_unexpected_outcome(self): |
687 | + with patch.object(lc.subprocess, 'check_call') as m_checkcall: |
688 | + m_checkcall.side_effect = subprocess.CalledProcessError( |
689 | + -1, ['sudo', 'egrep', self.regex, self.file_path]) |
690 | + self.assertEqual( |
691 | + lc.check_file(self.regex, self.file_path), |
692 | + lc.check_result.exception) |
693 | + |
694 | + def test_succeeds_when_regex_found(self): |
695 | + with patch.object(lc.subprocess, 'check_call'): |
696 | + self.assertEqual( |
697 | + lc.check_file(self.regex, self.file_path), |
698 | + lc.check_result.success) |
699 | + |
700 | + |
701 | +class TestParseArgs(TestCase): |
702 | + |
703 | + def test_basic_args(self): |
704 | + args = ['test .*', '/tmp/log.file'] |
705 | + parsed = lc.parse_args(args) |
706 | + self.assertEqual(parsed.regex, 'test .*') |
707 | + self.assertEqual(parsed.file_path, '/tmp/log.file') |
Have a bunch of random comments inline. Overall looks like a good approach, my main points of feedback:
* Assess scripts this complex can benefit from a bit more structure. A bunch of cooperating functions gets hard to follow around the 200 line mark. An encapsulating class for each environment seems reasonable.
* A bunch of the setup work is done by manual ssh steps on charms. Ideally, most of this complexity is encapsulated in the charm instead.
* I'd generally have more unit tests, but can see how a bunch of the current code is not amenable to it.