Merge lp:~veebers/juju-ci-tools/initial-log-forwarding-tests into lp:juju-ci-tools

Proposed by Christopher Lee
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
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

To post a comment you must log in.
1496. By Christopher Lee

Fix assertion messages.

1497. By Christopher Lee

Remove yet to be used methods.

Revision history for this message
Martin Packman (gz) wrote :

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.

Revision history for this message
Martin Packman (gz) wrote :

This time with the comments...

Revision history for this message
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.

Revision history for this message
Martin Packman (gz) wrote :

Replied to some comments.

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.

Revision history for this message
Martin Packman (gz) wrote :

Couple of notes from eyeballing this again, see inline comments.

Revision history for this message
Christopher Lee (veebers) wrote :

Made the suggested changes.

1509. By Christopher Lee

Put check logic into separate file with tests.

Revision history for this message
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.

Revision history for this message
Martin Packman (gz) wrote :

Thanks! Lets land it.

review: Approve

Unmerged revisions

1510. By Christopher Lee

Improved file existence checks and handling.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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+ MIIG4wIBAAKCAYEAp+jsWN98gIqwAaBOFGmSQTp0DkSMwtduUe9RNkxuYEh4xygO
393+ 9C9At3YMloVvcHMoVG1wbs/Dnqdie5B787lD8NgFlyPmNnNGn1lan4+ZLslKRoV7
394+ Kspcn+zQzxbPIcBgOmQhUt6FAL1/GsLFbnJJcRnmTV2TSmRYpVyml9Wd7KDkZihW
395+ c1Xw0ag7qH2dhNLZj4fQuYEPKrMI3QZ064/tKwp7zXpDtGjQ+gJZAR1PvO6b3Gsd
396+ LlBej2JF8T4pCpNX7QkEiCOaAeZLALUaXGbGJeYU91P7dQzgO3N+UbiMe6IJHFz/
397+ Y2eidrZJl/hjLVBdvShBJ1Qobd9lQ1/gt8kupW30AeKA7WKd8F0WPy101u101K4a
398+ FweJ5GG8OfYIrOQq+8w7VKYlhVWIMv5G8WpZY0jODd8NonbX8SR2I03yvfNduKmw
399+ gW+AOGTQSRNo8v/5UiHCDFyb9S10LU6keKztY+kwbq6IRTZlGKct2b+4J9gpH1+k
400+ HzRSI/HpSk+nXfkVAgMBAAECggGAJigjVYrr8xYRKz1voOngx5vt9bQUPM7SDiKR
401+ VQKHbq/panCq/Uijr01PTQFjsq0otA7upu/l527oTWYnFNq8GsYsdw08apFFsj6O
402+ /oWWbPBnRaFdvPqhk+IwDW+EgIoEFCDfBcL1fJaThNRQI2orUF1vXZNvPk+RaXql
403+ jQmJStXBMYnnI2ybPjm53O821ZFIyXo2r4Epni1zTS8DcOiTH93RBn/LVPsgyj+w
404+ VDWCAlBC8RMSXYz8AB93/3t9vh5/VTE8qRC9j6lqTxNsUYlCsHuB/j6A7XqFU6U7
405+ BVkKUHXRKo2nNcKwjsfPlnk/M41JT/N5RIpTbXRiBgZklIcXxxWdYDGD6M7n2YiP
406+ dMwmLZIxPRVp7LTQIxrztkqL5Kp/X9DasI6BPCgifxm4spvjMn5X+k5x4E6GABC2
407+ lx/cgriOl+nxgsy4372Kpt62srPRu4Vajr6DDH6nR1O0vxqu4ifawoe7YAUzXzvi
408+ 5kFWNzpnQ9pZ9s8iW0xP4eAuVZydAoHBAMEToj0he4vY5NyH/axf6u9BA/gkXn4D
409+ z38uYykYLr5b8BdEpbB0xZ/LgFOq45ZJcEYo0NjPLgiKuvtvZAKXm0Pka4a8D9Cp
410+ NhhoIN9iarZxgDkwvPX2VO1oGB8G/C5WlB2Y0P7QW9wxXZjA0KOkSJEdLP9kBvuQ
411+ s/eezIYUiM6upvqPqwKtniMYH1Dz3pApId/APUre0Qo52ITJGr6D849BfMqKYb5Z
412+ 4ifBUeztydZy8goNHIv4yERUVGoHVviWpwKBwQDeoZ+EGqv010U7ioMIhkJnt4CY
413+ CrAHOFJye+Th1wRHGGFy/UOe8SwxwZPAbexH/+HgC5IQ9FSx5SIDuaSWmjOd0DUi
414+ Lih2+J3T29haP2259gCvy9UtU+MGW6hP+bhdyJl1SmxSetfDAToAA5tBTSjcu4ea
415+ 8bKZwm7gHwxnXMuuGkkIUNSul1P9FwUEi3ZaefF3LN3P03e0T93n97DWCKA5yL2w
416+ tx7Y8o8AGyBaajPj9S8jLvw8bMzaSuXizucL5eMCgcBdX29gfObQtO3JMQMe76wg
417+ VKLkyEHiU1lvujE+WHGSoce0mQBAG9jO9I106PnzXkSryWVm1JsAiobuvenxzvvJ
418+ k5fkquJDGPIOT51GKsRMwwstnUJk+OINhf/UUX53smsi/RplgMJL9Ju9GdJMsVBe
419+ zWtLf0ZZNpuyLtveI+QdgB1Eo2Iig3AsrKfIcIe71AiLut5pbORPO7ZYUSFb7VhG
420+ eXcuREoM0k8qxrUmDcFEsoYXEkwx7Ph9AwNn23DV+5UCgcEA2ojWN2ui/czOJdsa
421+ MqTvzDWBoj1je0LbE4vwKYvRpCQXjDN1TDC6zACTk2GTfT19MFrLP59G//TGhdeV
422+ 60tkfXXiojGjAN2ct1jnL/dxMwh6thWkpUDh6dzRA+hCBLUjhdHPMMtqvf2XPGpN
423+ 3TTrdnkSbJLyWSJVieSQXWnmeXlN1T7a9qKPDDGreEGZpMhssSo2dYnDyBhZ4Bjv
424+ 2blP5kjZgvzN5/F5U4ZNJNN5KjwD0EqPyJSYJXM943xrqe83AoHAUYcDXY+TEpvQ
425+ WSHib0P+0yX4uZblgAqWk6nPKFIS1mw4mCO71vRHbxztA9gmqxhdSU2aDhHBslIg
426+ 50eGW9aaTaR6M6vsULA4danJso8Fzgiaz3oxOwSkxBdIu1F0Mr6JlI5PEN21vKXX
427+ tsiC2JJEasQbEbNLA5X4hX/jXWwPw0JGMW6UR6RaMHevA09579COUFrtEguZfDi6
428+ 1xP72bo/RzQ1cWLjb5QVkf155q/BpzrWYQJwo/8TEIL33XZcMES5
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')

Subscribers

People subscribed via source and target branches