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 | 1 | #!/usr/bin/env python | ||
6 | 2 | """Test Juju's log forwarding feature. | ||
7 | 3 | |||
8 | 4 | Log forwarding allows a controller to forward syslog from all models of a | ||
9 | 5 | controller to a syslog host via TCP (using SSL). | ||
10 | 6 | |||
11 | 7 | """ | ||
12 | 8 | |||
13 | 9 | from __future__ import print_function | ||
14 | 10 | |||
15 | 11 | import argparse | ||
16 | 12 | import logging | ||
17 | 13 | import os | ||
18 | 14 | import re | ||
19 | 15 | import sys | ||
20 | 16 | import socket | ||
21 | 17 | import subprocess | ||
22 | 18 | from textwrap import dedent | ||
23 | 19 | |||
24 | 20 | from assess_model_migration import get_bootstrap_managers | ||
25 | 21 | import certificates | ||
26 | 22 | from utility import ( | ||
27 | 23 | add_basic_testing_arguments, | ||
28 | 24 | configure_logging, | ||
29 | 25 | JujuAssertionError, | ||
30 | 26 | temp_dir, | ||
31 | 27 | ) | ||
32 | 28 | |||
33 | 29 | |||
34 | 30 | __metaclass__ = type | ||
35 | 31 | |||
36 | 32 | |||
37 | 33 | log = logging.getLogger("assess_log_forward") | ||
38 | 34 | |||
39 | 35 | |||
40 | 36 | def assess_log_forward(bs_dummy, bs_rsyslog, upload_tools): | ||
41 | 37 | """Ensure logs are forwarded after forwarding enabled after bootstrapping. | ||
42 | 38 | |||
43 | 39 | Given 2 controllers set rsyslog and dummy: | ||
44 | 40 | - setup rsyslog with secure details | ||
45 | 41 | - Enable log forwarding on dummy | ||
46 | 42 | - Ensure intial logs are present in the rsyslog sinks logs | ||
47 | 43 | |||
48 | 44 | """ | ||
49 | 45 | with bs_rsyslog.booted_context(upload_tools): | ||
50 | 46 | log.info('Bootstrapped rsyslog environment') | ||
51 | 47 | rsyslog = bs_rsyslog.client | ||
52 | 48 | rsyslog_details = deploy_rsyslog(rsyslog) | ||
53 | 49 | |||
54 | 50 | update_client_config(bs_dummy.client, rsyslog_details) | ||
55 | 51 | |||
56 | 52 | with bs_dummy.existing_booted_context(upload_tools): | ||
57 | 53 | log.info('Bootstrapped dummy environment') | ||
58 | 54 | dummy_client = bs_dummy.client | ||
59 | 55 | |||
60 | 56 | # ensure turning on log-fwd emits logs to the client. | ||
61 | 57 | # Should I ensure that nothing turns up beforehand | ||
62 | 58 | ensure_enabling_log_forwarding_forwards_previous_messages( | ||
63 | 59 | rsyslog, dummy_client) | ||
64 | 60 | |||
65 | 61 | |||
66 | 62 | def ensure_enabling_log_forwarding_forwards_previous_messages(rsyslog, dummy): | ||
67 | 63 | """Assert when enabled log forwarding forwards messages from log start.""" | ||
68 | 64 | uuid = dummy.get_controller_uuid() | ||
69 | 65 | |||
70 | 66 | enable_log_forwarding(dummy) | ||
71 | 67 | assert_initial_message_forwarded(rsyslog, uuid) | ||
72 | 68 | |||
73 | 69 | |||
74 | 70 | def assert_initial_message_forwarded(rsyslog, uuid): | ||
75 | 71 | """Assert that mention of the sources logs appear in the sinks logging. | ||
76 | 72 | |||
77 | 73 | Given a rsyslog sink and an output source assert that logging details from | ||
78 | 74 | the source appear in the sinks logging. | ||
79 | 75 | Attempt a check over a period of time (10 seconds). | ||
80 | 76 | |||
81 | 77 | :returns: As soon as the expected message appears. | ||
82 | 78 | :raises JujuAssertionError: If the expected message does not appear in the | ||
83 | 79 | given timeframe. | ||
84 | 80 | :raises JujuAssertionError: If the log message check fails in an unexpected | ||
85 | 81 | way. | ||
86 | 82 | |||
87 | 83 | """ | ||
88 | 84 | check_string = get_assert_regex(uuid) | ||
89 | 85 | unit_machine = 'rsyslog/0' | ||
90 | 86 | |||
91 | 87 | remote_script_path = create_check_script_on_unit(rsyslog, unit_machine) | ||
92 | 88 | |||
93 | 89 | try: | ||
94 | 90 | rsyslog.juju( | ||
95 | 91 | 'ssh', | ||
96 | 92 | ( | ||
97 | 93 | unit_machine, | ||
98 | 94 | 'sudo', | ||
99 | 95 | 'python', | ||
100 | 96 | remote_script_path, | ||
101 | 97 | check_string, | ||
102 | 98 | '/var/log/syslog')) | ||
103 | 99 | log.info('Check script passed on target machine.') | ||
104 | 100 | except subprocess.CalledProcessError: | ||
105 | 101 | # This is where a failure happened | ||
106 | 102 | raise JujuAssertionError('Forwarded log message never appeared.') | ||
107 | 103 | |||
108 | 104 | |||
109 | 105 | def create_check_script_on_unit(client, unit_machine): | ||
110 | 106 | script_path = os.path.join(os.path.dirname(__file__), 'log_check.py') | ||
111 | 107 | script_dest_path = os.path.join('/tmp', os.path.basename(script_path)) | ||
112 | 108 | client.juju( | ||
113 | 109 | 'scp', | ||
114 | 110 | (script_path, '{}:{}'.format(unit_machine, script_dest_path))) | ||
115 | 111 | return script_dest_path | ||
116 | 112 | |||
117 | 113 | |||
118 | 114 | def get_assert_regex(raw_uuid, message=None): | ||
119 | 115 | """Create a regex string to check syslog file. | ||
120 | 116 | |||
121 | 117 | If message is supplied it is expected to be escaped as needed (i.e. spaces) | ||
122 | 118 | no further massaging will be done to the message string. | ||
123 | 119 | |||
124 | 120 | """ | ||
125 | 121 | # Maybe over simplified removing the last 8 characters | ||
126 | 122 | uuid = re.escape(raw_uuid) | ||
127 | 123 | short_uuid = re.escape(raw_uuid[:-8]) | ||
128 | 124 | date_check = '[A-Z][a-z]{,2}\ +[0-9]+\ +[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}' | ||
129 | 125 | machine = 'machine-0.{}'.format(uuid) | ||
130 | 126 | agent = 'jujud-machine-agent-{}'.format(short_uuid) | ||
131 | 127 | message = message or '.*' | ||
132 | 128 | |||
133 | 129 | return '"^{datecheck}\ {machine}\ {agent}\ {message}$"'.format( | ||
134 | 130 | datecheck=date_check, | ||
135 | 131 | machine=machine, | ||
136 | 132 | agent=agent, | ||
137 | 133 | message=message) | ||
138 | 134 | |||
139 | 135 | |||
140 | 136 | def enable_log_forwarding(client): | ||
141 | 137 | client.juju( | ||
142 | 138 | 'set-model-config', | ||
143 | 139 | ('-m', 'controller', 'logforward-enabled=true'), include_e=False) | ||
144 | 140 | |||
145 | 141 | |||
146 | 142 | def update_client_config(client, rsyslog_details): | ||
147 | 143 | client.env.config['logforward-enabled'] = False | ||
148 | 144 | client.env.config.update(rsyslog_details) | ||
149 | 145 | |||
150 | 146 | |||
151 | 147 | def deploy_rsyslog(client): | ||
152 | 148 | """Deploy and setup the rsyslog charm on client. | ||
153 | 149 | |||
154 | 150 | :returns: Configuration details needed: cert, ca, key and ip:port. | ||
155 | 151 | |||
156 | 152 | """ | ||
157 | 153 | app_name = 'rsyslog' | ||
158 | 154 | client.deploy('rsyslog', (app_name)) | ||
159 | 155 | client.wait_for_started() | ||
160 | 156 | client.juju('set-config', (app_name, 'protocol="tcp"')) | ||
161 | 157 | client.juju('expose', app_name) | ||
162 | 158 | |||
163 | 159 | return setup_tls_rsyslog(client, app_name) | ||
164 | 160 | |||
165 | 161 | |||
166 | 162 | def setup_tls_rsyslog(client, app_name): | ||
167 | 163 | unit_machine = '{}/0'.format(app_name) | ||
168 | 164 | |||
169 | 165 | ip_address = get_unit_ipaddress(client, unit_machine) | ||
170 | 166 | |||
171 | 167 | client.juju( | ||
172 | 168 | 'ssh', | ||
173 | 169 | (unit_machine, 'sudo apt-get install rsyslog-gnutls')) | ||
174 | 170 | |||
175 | 171 | with temp_dir() as config_dir: | ||
176 | 172 | install_rsyslog_config(client, config_dir, unit_machine) | ||
177 | 173 | rsyslog_details = install_certificates( | ||
178 | 174 | client, config_dir, ip_address, unit_machine) | ||
179 | 175 | |||
180 | 176 | # restart rsyslog to take into affect all changes | ||
181 | 177 | client.juju('ssh', (unit_machine, 'sudo', 'service', 'rsyslog', 'restart')) | ||
182 | 178 | |||
183 | 179 | return rsyslog_details | ||
184 | 180 | |||
185 | 181 | |||
186 | 182 | def install_certificates(client, config_dir, ip_address, unit_machine): | ||
187 | 183 | cert, key = certificates.create_certificate(config_dir, ip_address) | ||
188 | 184 | |||
189 | 185 | # Write contents to file to scp across | ||
190 | 186 | ca_file = os.path.join(config_dir, 'ca.pem') | ||
191 | 187 | with open(ca_file, 'wt') as f: | ||
192 | 188 | f.write(certificates.ca_pem_contents) | ||
193 | 189 | |||
194 | 190 | scp_command = ( | ||
195 | 191 | '--', cert, key, ca_file, '{unit}:/home/ubuntu/'.format( | ||
196 | 192 | unit=unit_machine)) | ||
197 | 193 | client.juju('scp', scp_command) | ||
198 | 194 | |||
199 | 195 | return _get_rsyslog_details(cert, key, ip_address) | ||
200 | 196 | |||
201 | 197 | |||
202 | 198 | def _get_rsyslog_details(cert_file, key_file, ip_address): | ||
203 | 199 | with open(cert_file, 'rt') as f: | ||
204 | 200 | cert_contents = f.read() | ||
205 | 201 | with open(key_file, 'rt') as f: | ||
206 | 202 | key_contents = f.read() | ||
207 | 203 | |||
208 | 204 | return { | ||
209 | 205 | 'syslog-host': '{}'.format(add_port_to_ip(ip_address, '10514')), | ||
210 | 206 | 'syslog-ca-cert': certificates.ca_pem_contents, | ||
211 | 207 | 'syslog-client-cert': cert_contents, | ||
212 | 208 | 'syslog-client-key': key_contents | ||
213 | 209 | } | ||
214 | 210 | |||
215 | 211 | |||
216 | 212 | def add_port_to_ip(ip_address, port): | ||
217 | 213 | """Return an ipv4/ipv6 address with port added to `ip_address`.""" | ||
218 | 214 | try: | ||
219 | 215 | socket.inet_aton(ip_address) | ||
220 | 216 | return '{}:{}'.format(ip_address, port) | ||
221 | 217 | except socket.error: | ||
222 | 218 | try: | ||
223 | 219 | socket.inet_pton(socket.AF_INET6, ip_address) | ||
224 | 220 | return '[{}]:{}'.format(ip_address, port) | ||
225 | 221 | except socket.error: | ||
226 | 222 | pass | ||
227 | 223 | raise ValueError( | ||
228 | 224 | 'IP Address "{}" is neither an ipv4 or ipv6 address.'.format( | ||
229 | 225 | ip_address)) | ||
230 | 226 | |||
231 | 227 | |||
232 | 228 | def install_rsyslog_config(client, config_dir, unit_machine): | ||
233 | 229 | config = write_rsyslog_config_file(config_dir) | ||
234 | 230 | client.juju('scp', (config, '{unit}:/tmp'.format(unit=unit_machine))) | ||
235 | 231 | client.juju( | ||
236 | 232 | 'ssh', | ||
237 | 233 | (unit_machine, 'sudo', 'mv', '/tmp/{}'.format( | ||
238 | 234 | os.path.basename(config)), '/etc/rsyslog.d/')) | ||
239 | 235 | |||
240 | 236 | |||
241 | 237 | def get_unit_ipaddress(client, unit_name): | ||
242 | 238 | status = client.get_status() | ||
243 | 239 | return status.get_unit(unit_name)['public-address'] | ||
244 | 240 | |||
245 | 241 | |||
246 | 242 | def write_rsyslog_config_file(tmp_dir): | ||
247 | 243 | """Write rsyslog config file to `tmp_dir`/10-securelogging.conf.""" | ||
248 | 244 | config = dedent("""\ | ||
249 | 245 | # make gtls driver the default | ||
250 | 246 | $DefaultNetstreamDriver gtls | ||
251 | 247 | |||
252 | 248 | # certificate files | ||
253 | 249 | $DefaultNetstreamDriverCAFile /home/ubuntu/ca.pem | ||
254 | 250 | $DefaultNetstreamDriverCertFile /home/ubuntu/cert.pem | ||
255 | 251 | $DefaultNetstreamDriverKeyFile /home/ubuntu/key.pem | ||
256 | 252 | |||
257 | 253 | $ModLoad imtcp # load TCP listener | ||
258 | 254 | $InputTCPServerStreamDriverAuthMode x509/name | ||
259 | 255 | $InputTCPServerStreamDriverPermittedPeer anyServer | ||
260 | 256 | $InputTCPServerStreamDriverMode 1 # run driver in TLS-only mode | ||
261 | 257 | $InputTCPServerRun 10514 # port 10514 | ||
262 | 258 | """) | ||
263 | 259 | config_path = os.path.join(tmp_dir, '10-securelogging.conf') | ||
264 | 260 | with open(config_path, 'wt') as f: | ||
265 | 261 | f.write(config) | ||
266 | 262 | return config_path | ||
267 | 263 | |||
268 | 264 | |||
269 | 265 | def parse_args(argv): | ||
270 | 266 | """Parse all arguments.""" | ||
271 | 267 | parser = argparse.ArgumentParser( | ||
272 | 268 | description="Test log forwarding of logs.") | ||
273 | 269 | add_basic_testing_arguments(parser) | ||
274 | 270 | return parser.parse_args(argv) | ||
275 | 271 | |||
276 | 272 | |||
277 | 273 | def main(argv=None): | ||
278 | 274 | args = parse_args(argv) | ||
279 | 275 | configure_logging(args.verbose) | ||
280 | 276 | bs_dummy, bs_rsyslog = get_bootstrap_managers(args) | ||
281 | 277 | assess_log_forward(bs_dummy, bs_rsyslog, args.upload_tools) | ||
282 | 278 | return 0 | ||
283 | 279 | |||
284 | 280 | |||
285 | 281 | if __name__ == '__main__': | ||
286 | 282 | sys.exit(main()) | ||
287 | 0 | 283 | ||
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 | 1 | from OpenSSL import crypto | ||
293 | 2 | import os | ||
294 | 3 | from textwrap import dedent | ||
295 | 4 | |||
296 | 5 | |||
297 | 6 | def create_certificate(target_dir, ip_address): | ||
298 | 7 | """Generate a cert and key file incl. IP SAN for `ip_address` | ||
299 | 8 | |||
300 | 9 | Creates a cert.pem and key.pem file signed with a known ca cert. | ||
301 | 10 | The generated cert will contain a IP SAN (subject alternative name) that | ||
302 | 11 | includes the ip address of the server. This is required for log-forwarding. | ||
303 | 12 | |||
304 | 13 | :return: tuple containing generated cert, key filepath pair | ||
305 | 14 | |||
306 | 15 | """ | ||
307 | 16 | ip_address = 'IP:{}'.format(ip_address) | ||
308 | 17 | |||
309 | 18 | key = crypto.PKey() | ||
310 | 19 | key.generate_key(crypto.TYPE_RSA, 2048) | ||
311 | 20 | |||
312 | 21 | csr_contents = generate_csr(target_dir, key, ip_address) | ||
313 | 22 | req = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_contents) | ||
314 | 23 | |||
315 | 24 | ca_cert = crypto.load_certificate( | ||
316 | 25 | crypto.FILETYPE_PEM, ca_pem_contents) | ||
317 | 26 | ca_key = crypto.load_privatekey( | ||
318 | 27 | crypto.FILETYPE_PEM, ca_key_pem_contents) | ||
319 | 28 | |||
320 | 29 | cert = crypto.X509() | ||
321 | 30 | cert.set_version(0x2) | ||
322 | 31 | cert.set_subject(req.get_subject()) | ||
323 | 32 | cert.set_serial_number(1) | ||
324 | 33 | cert.gmtime_adj_notBefore(0) | ||
325 | 34 | cert.gmtime_adj_notAfter(24 * 60 * 60) | ||
326 | 35 | cert.set_issuer(ca_cert.get_subject()) | ||
327 | 36 | cert.set_pubkey(req.get_pubkey()) | ||
328 | 37 | cert.add_extensions([ | ||
329 | 38 | crypto.X509Extension('subjectAltName', False, ip_address), | ||
330 | 39 | crypto.X509Extension( | ||
331 | 40 | 'extendedKeyUsage', False, 'clientAuth, serverAuth'), | ||
332 | 41 | crypto.X509Extension( | ||
333 | 42 | 'keyUsage', True, 'keyEncipherment'), | ||
334 | 43 | ]) | ||
335 | 44 | cert.sign(ca_key, "sha256") | ||
336 | 45 | |||
337 | 46 | cert_filepath = os.path.join(target_dir, 'cert.pem') | ||
338 | 47 | with open(cert_filepath, 'wt') as f: | ||
339 | 48 | f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) | ||
340 | 49 | |||
341 | 50 | key_filepath = os.path.join(target_dir, 'key.pem') | ||
342 | 51 | with open(key_filepath, 'wt') as f: | ||
343 | 52 | f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) | ||
344 | 53 | |||
345 | 54 | return (cert_filepath, key_filepath) | ||
346 | 55 | |||
347 | 56 | |||
348 | 57 | def generate_csr(target_dir, key, ip_address): | ||
349 | 58 | req = crypto.X509Req() | ||
350 | 59 | req.set_version(0x2) | ||
351 | 60 | req.get_subject().CN = "anyServer" | ||
352 | 61 | # Add the IP SAN | ||
353 | 62 | req.add_extensions([ | ||
354 | 63 | crypto.X509Extension("subjectAltName", False, ip_address) | ||
355 | 64 | ]) | ||
356 | 65 | req.set_pubkey(key) | ||
357 | 66 | req.sign(key, "sha256") | ||
358 | 67 | |||
359 | 68 | return crypto.dump_certificate_request(crypto.FILETYPE_PEM, req) | ||
360 | 69 | |||
361 | 70 | |||
362 | 71 | ca_pem_contents = dedent("""\ | ||
363 | 72 | -----BEGIN CERTIFICATE----- | ||
364 | 73 | MIIEFTCCAn2gAwIBAgIBBzANBgkqhkiG9w0BAQsFADAjMRIwEAYDVQQDEwlhbnlT | ||
365 | 74 | ZXJ2ZXIxDTALBgNVBAoTBGp1anUwHhcNMTYwNzExMDQyOTM1WhcNMjYwNzA5MDQy | ||
366 | 75 | OTM1WjAjMRIwEAYDVQQDEwlhbnlTZXJ2ZXIxDTALBgNVBAoTBGp1anUwggGiMA0G | ||
367 | 76 | CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCn6OxY33yAirABoE4UaZJBOnQORIzC | ||
368 | 77 | 125R71E2TG5gSHjHKA70L0C3dgyWhW9wcyhUbXBuz8Oep2J7kHvzuUPw2AWXI+Y2 | ||
369 | 78 | c0afWVqfj5kuyUpGhXsqylyf7NDPFs8hwGA6ZCFS3oUAvX8awsVucklxGeZNXZNK | ||
370 | 79 | ZFilXKaX1Z3soORmKFZzVfDRqDuofZ2E0tmPh9C5gQ8qswjdBnTrj+0rCnvNekO0 | ||
371 | 80 | aND6AlkBHU+87pvcax0uUF6PYkXxPikKk1ftCQSII5oB5ksAtRpcZsYl5hT3U/t1 | ||
372 | 81 | DOA7c35RuIx7ogkcXP9jZ6J2tkmX+GMtUF29KEEnVCht32VDX+C3yS6lbfQB4oDt | ||
373 | 82 | Yp3wXRY/LXTW7XTUrhoXB4nkYbw59gis5Cr7zDtUpiWFVYgy/kbxalljSM4N3w2i | ||
374 | 83 | dtfxJHYjTfK98124qbCBb4A4ZNBJE2jy//lSIcIMXJv1LXQtTqR4rO1j6TBurohF | ||
375 | 84 | NmUYpy3Zv7gn2CkfX6QfNFIj8elKT6dd+RUCAwEAAaNUMFIwDwYDVR0TAQH/BAUw | ||
376 | 85 | AwEB/zAPBgNVHREECDAGhwQKwoylMA8GA1UdDwEB/wQFAwMHBAAwHQYDVR0OBBYE | ||
377 | 86 | FP+v8GAqHiUCIygXbwWzbUhl/22DMA0GCSqGSIb3DQEBCwUAA4IBgQBVYKeT1O2M | ||
378 | 87 | U3OPOy0IwqcA1/64rS1GlRmiw+papmjsy3aru03r8igahnbFd7wQawHaCScXbI/n | ||
379 | 88 | OAPT4PDGXn6b71t4uHwWaM8wde159RO3G32N/VfhV6LPRUQunmAZh5QcJK6wWpYu | ||
380 | 89 | B1f0dPkU+Q1AfX12oTOX/ld2/o7jaVswHoHoW6K2WQmwzlRQ953J+RJ7jXfrYDKl | ||
381 | 90 | OAp3Hb69wAN4Ayc1s92iYUwV5q8UaHQoskHOLWJu964yFBHL8SLe6TLD+Jjv05Mc | ||
382 | 91 | Ca7NKq/n25VTDNNaXl5MCNZ048m/GGHfktxxCddaF2grhC5HTUetwkq026PE0Wcq | ||
383 | 92 | P+cDrIq6uTA25QqyBYistSa/7z2o0NBi56ySRqxlP2J2TPFZyOb+ZiA4EgYY5no5 | ||
384 | 93 | u2E+WuKZLVWl7eaQYOHgfYzFf3CvalSBwIjNynRwD/2Ebk7K29GPrIugb3V2+Vwh | ||
385 | 94 | rltUXOHUkFGjEHIhr8zixfCxh5OzPJMnJwCZZRYzMO0/0Gw7ll9DmH0= | ||
386 | 95 | -----END CERTIFICATE----- | ||
387 | 96 | """) | ||
388 | 97 | |||
389 | 98 | |||
390 | 99 | ca_key_pem_contents = dedent("""\ | ||
391 | 100 | -----BEGIN RSA PRIVATE KEY----- | ||
392 | 101 | MIIG4wIBAAKCAYEAp+jsWN98gIqwAaBOFGmSQTp0DkSMwtduUe9RNkxuYEh4xygO | ||
393 | 102 | 9C9At3YMloVvcHMoVG1wbs/Dnqdie5B787lD8NgFlyPmNnNGn1lan4+ZLslKRoV7 | ||
394 | 103 | Kspcn+zQzxbPIcBgOmQhUt6FAL1/GsLFbnJJcRnmTV2TSmRYpVyml9Wd7KDkZihW | ||
395 | 104 | c1Xw0ag7qH2dhNLZj4fQuYEPKrMI3QZ064/tKwp7zXpDtGjQ+gJZAR1PvO6b3Gsd | ||
396 | 105 | LlBej2JF8T4pCpNX7QkEiCOaAeZLALUaXGbGJeYU91P7dQzgO3N+UbiMe6IJHFz/ | ||
397 | 106 | Y2eidrZJl/hjLVBdvShBJ1Qobd9lQ1/gt8kupW30AeKA7WKd8F0WPy101u101K4a | ||
398 | 107 | FweJ5GG8OfYIrOQq+8w7VKYlhVWIMv5G8WpZY0jODd8NonbX8SR2I03yvfNduKmw | ||
399 | 108 | gW+AOGTQSRNo8v/5UiHCDFyb9S10LU6keKztY+kwbq6IRTZlGKct2b+4J9gpH1+k | ||
400 | 109 | HzRSI/HpSk+nXfkVAgMBAAECggGAJigjVYrr8xYRKz1voOngx5vt9bQUPM7SDiKR | ||
401 | 110 | VQKHbq/panCq/Uijr01PTQFjsq0otA7upu/l527oTWYnFNq8GsYsdw08apFFsj6O | ||
402 | 111 | /oWWbPBnRaFdvPqhk+IwDW+EgIoEFCDfBcL1fJaThNRQI2orUF1vXZNvPk+RaXql | ||
403 | 112 | jQmJStXBMYnnI2ybPjm53O821ZFIyXo2r4Epni1zTS8DcOiTH93RBn/LVPsgyj+w | ||
404 | 113 | VDWCAlBC8RMSXYz8AB93/3t9vh5/VTE8qRC9j6lqTxNsUYlCsHuB/j6A7XqFU6U7 | ||
405 | 114 | BVkKUHXRKo2nNcKwjsfPlnk/M41JT/N5RIpTbXRiBgZklIcXxxWdYDGD6M7n2YiP | ||
406 | 115 | dMwmLZIxPRVp7LTQIxrztkqL5Kp/X9DasI6BPCgifxm4spvjMn5X+k5x4E6GABC2 | ||
407 | 116 | lx/cgriOl+nxgsy4372Kpt62srPRu4Vajr6DDH6nR1O0vxqu4ifawoe7YAUzXzvi | ||
408 | 117 | 5kFWNzpnQ9pZ9s8iW0xP4eAuVZydAoHBAMEToj0he4vY5NyH/axf6u9BA/gkXn4D | ||
409 | 118 | z38uYykYLr5b8BdEpbB0xZ/LgFOq45ZJcEYo0NjPLgiKuvtvZAKXm0Pka4a8D9Cp | ||
410 | 119 | NhhoIN9iarZxgDkwvPX2VO1oGB8G/C5WlB2Y0P7QW9wxXZjA0KOkSJEdLP9kBvuQ | ||
411 | 120 | s/eezIYUiM6upvqPqwKtniMYH1Dz3pApId/APUre0Qo52ITJGr6D849BfMqKYb5Z | ||
412 | 121 | 4ifBUeztydZy8goNHIv4yERUVGoHVviWpwKBwQDeoZ+EGqv010U7ioMIhkJnt4CY | ||
413 | 122 | CrAHOFJye+Th1wRHGGFy/UOe8SwxwZPAbexH/+HgC5IQ9FSx5SIDuaSWmjOd0DUi | ||
414 | 123 | Lih2+J3T29haP2259gCvy9UtU+MGW6hP+bhdyJl1SmxSetfDAToAA5tBTSjcu4ea | ||
415 | 124 | 8bKZwm7gHwxnXMuuGkkIUNSul1P9FwUEi3ZaefF3LN3P03e0T93n97DWCKA5yL2w | ||
416 | 125 | tx7Y8o8AGyBaajPj9S8jLvw8bMzaSuXizucL5eMCgcBdX29gfObQtO3JMQMe76wg | ||
417 | 126 | VKLkyEHiU1lvujE+WHGSoce0mQBAG9jO9I106PnzXkSryWVm1JsAiobuvenxzvvJ | ||
418 | 127 | k5fkquJDGPIOT51GKsRMwwstnUJk+OINhf/UUX53smsi/RplgMJL9Ju9GdJMsVBe | ||
419 | 128 | zWtLf0ZZNpuyLtveI+QdgB1Eo2Iig3AsrKfIcIe71AiLut5pbORPO7ZYUSFb7VhG | ||
420 | 129 | eXcuREoM0k8qxrUmDcFEsoYXEkwx7Ph9AwNn23DV+5UCgcEA2ojWN2ui/czOJdsa | ||
421 | 130 | MqTvzDWBoj1je0LbE4vwKYvRpCQXjDN1TDC6zACTk2GTfT19MFrLP59G//TGhdeV | ||
422 | 131 | 60tkfXXiojGjAN2ct1jnL/dxMwh6thWkpUDh6dzRA+hCBLUjhdHPMMtqvf2XPGpN | ||
423 | 132 | 3TTrdnkSbJLyWSJVieSQXWnmeXlN1T7a9qKPDDGreEGZpMhssSo2dYnDyBhZ4Bjv | ||
424 | 133 | 2blP5kjZgvzN5/F5U4ZNJNN5KjwD0EqPyJSYJXM943xrqe83AoHAUYcDXY+TEpvQ | ||
425 | 134 | WSHib0P+0yX4uZblgAqWk6nPKFIS1mw4mCO71vRHbxztA9gmqxhdSU2aDhHBslIg | ||
426 | 135 | 50eGW9aaTaR6M6vsULA4danJso8Fzgiaz3oxOwSkxBdIu1F0Mr6JlI5PEN21vKXX | ||
427 | 136 | tsiC2JJEasQbEbNLA5X4hX/jXWwPw0JGMW6UR6RaMHevA09579COUFrtEguZfDi6 | ||
428 | 137 | 1xP72bo/RzQ1cWLjb5QVkf155q/BpzrWYQJwo/8TEIL33XZcMES5 | ||
429 | 138 | -----END RSA PRIVATE KEY----- | ||
430 | 139 | """) | ||
431 | 0 | 140 | ||
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 | 1476 | env = self.env.clone(model_name=name) | 1476 | env = self.env.clone(model_name=name) |
437 | 1477 | return self.clone(env=env) | 1477 | return self.clone(env=env) |
438 | 1478 | 1478 | ||
439 | 1479 | def get_controller_uuid(self): | ||
440 | 1480 | name = self.env.controller.name | ||
441 | 1481 | output_yaml = self.get_juju_output( | ||
442 | 1482 | 'show-controller', '--format', 'yaml', include_e=False) | ||
443 | 1483 | output = yaml.safe_load(output_yaml) | ||
444 | 1484 | return output[name]['details']['uuid'] | ||
445 | 1485 | |||
446 | 1479 | def get_controller_client(self): | 1486 | def get_controller_client(self): |
447 | 1480 | """Return a client for the controller model. May return self. | 1487 | """Return a client for the controller model. May return self. |
448 | 1481 | 1488 | ||
449 | 1482 | 1489 | ||
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 | 1 | #!/usr/bin/env python | ||
455 | 2 | """Simple looped check over provided file for regex content.""" | ||
456 | 3 | from __future__ import print_function | ||
457 | 4 | |||
458 | 5 | import argparse | ||
459 | 6 | import os | ||
460 | 7 | import subprocess | ||
461 | 8 | import sys | ||
462 | 9 | import time | ||
463 | 10 | |||
464 | 11 | |||
465 | 12 | class check_result: | ||
466 | 13 | success = 0 | ||
467 | 14 | failure = 1 | ||
468 | 15 | exception = 2 | ||
469 | 16 | |||
470 | 17 | |||
471 | 18 | def check_file(check_string, file_path): | ||
472 | 19 | print('Checking for:\n{}'.format(check_string)) | ||
473 | 20 | for _ in range(0, 10): | ||
474 | 21 | try: | ||
475 | 22 | with open(file_path, 'r') as f: | ||
476 | 23 | subprocess.check_call(['sudo', 'egrep', check_string], stdin=f) | ||
477 | 24 | print('Log content found. No need to continue.') | ||
478 | 25 | return check_result.success | ||
479 | 26 | except subprocess.CalledProcessError as e: | ||
480 | 27 | if e.returncode == 1: | ||
481 | 28 | time.sleep(1) | ||
482 | 29 | else: | ||
483 | 30 | return check_result.exception | ||
484 | 31 | print('Unexpected error with file check.') | ||
485 | 32 | return check_result.failure | ||
486 | 33 | |||
487 | 34 | |||
488 | 35 | def parse_args(argv=None): | ||
489 | 36 | parser = argparse.ArgumentParser( | ||
490 | 37 | description='File content check.') | ||
491 | 38 | parser.add_argument( | ||
492 | 39 | 'regex', help='Regex string to check file with.') | ||
493 | 40 | parser.add_argument( | ||
494 | 41 | 'file_path', help='Path to file to check.') | ||
495 | 42 | return parser.parse_args(argv) | ||
496 | 43 | |||
497 | 44 | |||
498 | 45 | def main(argv=None): | ||
499 | 46 | args = parse_args(argv) | ||
500 | 47 | try: | ||
501 | 48 | return check_file(args.regex, args.file_path) | ||
502 | 49 | except EnvironmentError as e: | ||
503 | 50 | print('Cannot open file "{}": {}'.format(args.file_path, str(e))) | ||
504 | 51 | return check_result.exception | ||
505 | 52 | |||
506 | 53 | |||
507 | 54 | if __name__ == '__main__': | ||
508 | 55 | sys.exit(main()) | ||
509 | 0 | 56 | ||
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 | 1 | """Tests for assess_log_forward module.""" | ||
515 | 2 | |||
516 | 3 | import argparse | ||
517 | 4 | from mock import patch | ||
518 | 5 | import re | ||
519 | 6 | import StringIO | ||
520 | 7 | |||
521 | 8 | import assess_log_forward as alf | ||
522 | 9 | from tests import ( | ||
523 | 10 | parse_error, | ||
524 | 11 | TestCase, | ||
525 | 12 | ) | ||
526 | 13 | |||
527 | 14 | |||
528 | 15 | class TestParseArgs(TestCase): | ||
529 | 16 | |||
530 | 17 | def test_common_args(self): | ||
531 | 18 | args = alf.parse_args( | ||
532 | 19 | ['an-env', '/bin/juju', '/tmp/logs', 'an-env-mod']) | ||
533 | 20 | self.assertEqual( | ||
534 | 21 | args, | ||
535 | 22 | argparse.Namespace( | ||
536 | 23 | env='an-env', | ||
537 | 24 | juju_bin='/bin/juju', | ||
538 | 25 | temp_env_name='an-env-mod', | ||
539 | 26 | debug=False, | ||
540 | 27 | agent_stream=None, | ||
541 | 28 | agent_url=None, | ||
542 | 29 | bootstrap_host=None, | ||
543 | 30 | keep_env=False, | ||
544 | 31 | logs='/tmp/logs', | ||
545 | 32 | machine=[], | ||
546 | 33 | region=None, | ||
547 | 34 | series=None, | ||
548 | 35 | upload_tools=False, | ||
549 | 36 | verbose=20)) | ||
550 | 37 | |||
551 | 38 | def test_help(self): | ||
552 | 39 | fake_stdout = StringIO.StringIO() | ||
553 | 40 | with parse_error(self) as fake_stderr: | ||
554 | 41 | with patch("sys.stdout", fake_stdout): | ||
555 | 42 | alf.parse_args(["--help"]) | ||
556 | 43 | self.assertEqual("", fake_stderr.getvalue()) | ||
557 | 44 | self.assertIn( | ||
558 | 45 | 'Test log forwarding of logs.', | ||
559 | 46 | fake_stdout.getvalue()) | ||
560 | 47 | |||
561 | 48 | |||
562 | 49 | class TestAssertRegex(TestCase): | ||
563 | 50 | |||
564 | 51 | def test_default_message_check(self): | ||
565 | 52 | self.assertTrue( | ||
566 | 53 | alf.get_assert_regex('').endswith('.*$"')) | ||
567 | 54 | |||
568 | 55 | def test_fails_when_uuid_doesnt_match(self): | ||
569 | 56 | uuid = 'fail' | ||
570 | 57 | check = alf.get_assert_regex(uuid) | ||
571 | 58 | failing_string = 'abc' | ||
572 | 59 | |||
573 | 60 | self.assertIsNone(re.search(check, failing_string)) | ||
574 | 61 | |||
575 | 62 | def test_succeeds_with_matching_uuid(self): | ||
576 | 63 | uuid = '1234567812345678' | ||
577 | 64 | short_uuid = uuid[:-8] | ||
578 | 65 | check = alf.get_assert_regex( | ||
579 | 66 | uuid, message='abc').rstrip('"').strip('"') | ||
580 | 67 | success = 'Jul 13 00:00:00 machine-0.{} '\ | ||
581 | 68 | 'jujud-machine-agent-{} abc'.format( | ||
582 | 69 | uuid, short_uuid) | ||
583 | 70 | |||
584 | 71 | self.assertIsNotNone( | ||
585 | 72 | re.search(check, success)) | ||
586 | 73 | |||
587 | 74 | |||
588 | 75 | class TestAddPortToIP(TestCase): | ||
589 | 76 | def test_adds_port_to_ipv4(self): | ||
590 | 77 | ip_address = '192.168.1.1' | ||
591 | 78 | port = '123' | ||
592 | 79 | expected = '192.168.1.1:123' | ||
593 | 80 | self.assertEqual( | ||
594 | 81 | alf.add_port_to_ip(ip_address, port), | ||
595 | 82 | expected | ||
596 | 83 | ) | ||
597 | 84 | |||
598 | 85 | def test_adds_port_to_ipv6(self): | ||
599 | 86 | ip_address = '1fff:0:a88:85a3::ac1f' | ||
600 | 87 | port = '123' | ||
601 | 88 | expected = '[1fff:0:a88:85a3::ac1f]:123' | ||
602 | 89 | self.assertEqual( | ||
603 | 90 | alf.add_port_to_ip(ip_address, port), | ||
604 | 91 | expected | ||
605 | 92 | ) | ||
606 | 93 | |||
607 | 94 | def test_raises_ValueError_on_invalid_address(self): | ||
608 | 95 | with self.assertRaises(ValueError): | ||
609 | 96 | alf.add_port_to_ip('abc', 'abc') | ||
610 | 0 | 97 | ||
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 | 2343 | controller_name = client.get_controller_model_name() | 2343 | controller_name = client.get_controller_model_name() |
616 | 2344 | self.assertEqual('controller', controller_name) | 2344 | self.assertEqual('controller', controller_name) |
617 | 2345 | 2345 | ||
618 | 2346 | def test_get_controller_uuid_returns_uuid(self): | ||
619 | 2347 | controller_uuid = 'eb67e1eb-6c54-45f5-8b6a-b6243be97202' | ||
620 | 2348 | yaml_string = dedent("""\ | ||
621 | 2349 | foo: | ||
622 | 2350 | details: | ||
623 | 2351 | uuid: {uuid} | ||
624 | 2352 | api-endpoints: ['10.194.140.213:17070'] | ||
625 | 2353 | cloud: lxd | ||
626 | 2354 | region: localhost | ||
627 | 2355 | models: | ||
628 | 2356 | controller: | ||
629 | 2357 | uuid: {uuid} | ||
630 | 2358 | default: | ||
631 | 2359 | uuid: 772cdd39-b454-4bd5-8704-dc9aa9ff1750 | ||
632 | 2360 | current-model: default | ||
633 | 2361 | account: | ||
634 | 2362 | user: admin@local | ||
635 | 2363 | bootstrap-config: | ||
636 | 2364 | config: | ||
637 | 2365 | cloud: lxd | ||
638 | 2366 | cloud-type: lxd | ||
639 | 2367 | region: localhost""".format(uuid=controller_uuid)) | ||
640 | 2368 | client = EnvJujuClient(JujuData('foo'), None, None) | ||
641 | 2369 | with patch.object(client, 'get_juju_output', return_value=yaml_string): | ||
642 | 2370 | self.assertEqual( | ||
643 | 2371 | client.get_controller_uuid(), | ||
644 | 2372 | controller_uuid | ||
645 | 2373 | ) | ||
646 | 2374 | |||
647 | 2346 | def test_get_controller_client(self): | 2375 | def test_get_controller_client(self): |
648 | 2347 | client = EnvJujuClient( | 2376 | client = EnvJujuClient( |
649 | 2348 | JujuData('foo', {'bar': 'baz'}, 'myhome'), None, None) | 2377 | JujuData('foo', {'bar': 'baz'}, 'myhome'), None, None) |
650 | 2349 | 2378 | ||
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 | 1 | """Tests for log_check script.""" | ||
656 | 2 | |||
657 | 3 | from mock import patch | ||
658 | 4 | import subprocess | ||
659 | 5 | |||
660 | 6 | import log_check as lc | ||
661 | 7 | from tests import TestCase | ||
662 | 8 | |||
663 | 9 | |||
664 | 10 | class TestCheckFile(TestCase): | ||
665 | 11 | |||
666 | 12 | regex = 'test123' | ||
667 | 13 | file_path = '/tmp/file.log' | ||
668 | 14 | |||
669 | 15 | def test_calls_check_call(self): | ||
670 | 16 | with patch.object(lc.subprocess, 'check_call') as m_checkcall: | ||
671 | 17 | lc.check_file(self.regex, self.file_path) | ||
672 | 18 | |||
673 | 19 | m_checkcall.assert_called_once_with( | ||
674 | 20 | ['sudo', 'egrep', self.regex, self.file_path]) | ||
675 | 21 | |||
676 | 22 | def test_fails_after_attempting_multiple_times(self): | ||
677 | 23 | with patch.object(lc.subprocess, 'check_call') as m_checkcall: | ||
678 | 24 | m_checkcall.side_effect = subprocess.CalledProcessError( | ||
679 | 25 | 1, ['sudo', 'egrep', self.regex, self.file_path]) | ||
680 | 26 | with patch.object(lc.time, 'sleep') as m_sleep: | ||
681 | 27 | self.assertEqual( | ||
682 | 28 | lc.check_file(self.regex, self.file_path), | ||
683 | 29 | lc.check_result.failure) | ||
684 | 30 | self.assertEqual(m_sleep.call_count, 10) | ||
685 | 31 | |||
686 | 32 | def test_fails_when_meeting_unexpected_outcome(self): | ||
687 | 33 | with patch.object(lc.subprocess, 'check_call') as m_checkcall: | ||
688 | 34 | m_checkcall.side_effect = subprocess.CalledProcessError( | ||
689 | 35 | -1, ['sudo', 'egrep', self.regex, self.file_path]) | ||
690 | 36 | self.assertEqual( | ||
691 | 37 | lc.check_file(self.regex, self.file_path), | ||
692 | 38 | lc.check_result.exception) | ||
693 | 39 | |||
694 | 40 | def test_succeeds_when_regex_found(self): | ||
695 | 41 | with patch.object(lc.subprocess, 'check_call'): | ||
696 | 42 | self.assertEqual( | ||
697 | 43 | lc.check_file(self.regex, self.file_path), | ||
698 | 44 | lc.check_result.success) | ||
699 | 45 | |||
700 | 46 | |||
701 | 47 | class TestParseArgs(TestCase): | ||
702 | 48 | |||
703 | 49 | def test_basic_args(self): | ||
704 | 50 | args = ['test .*', '/tmp/log.file'] | ||
705 | 51 | parsed = lc.parse_args(args) | ||
706 | 52 | self.assertEqual(parsed.regex, 'test .*') | ||
707 | 53 | 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.