2
"""Test Juju's log forwarding feature.
4
Log forwarding allows a controller to forward syslog from all models of a
5
controller to a syslog host via TCP (using SSL).
9
from __future__ import print_function
18
from textwrap import dedent
20
from assess_model_migration import get_bootstrap_managers
22
from jujucharm import local_charm_path
24
add_basic_testing_arguments,
34
log = logging.getLogger("assess_log_forward")
37
def assess_log_forward(bs_dummy, bs_rsyslog, upload_tools):
38
"""Ensure logs are forwarded after forwarding enabled after bootstrapping.
40
Given 2 controllers set rsyslog and dummy:
41
- setup rsyslog with secure details
42
- Enable log forwarding on dummy
43
- Ensure intial logs are present in the rsyslog sinks logs
46
with bs_rsyslog.booted_context(upload_tools):
47
log.info('Bootstrapped rsyslog environment')
48
rsyslog = bs_rsyslog.client
49
rsyslog_details = deploy_rsyslog(rsyslog)
51
update_client_config(bs_dummy.client, rsyslog_details)
53
with bs_dummy.existing_booted_context(upload_tools):
54
log.info('Bootstrapped dummy environment')
55
dummy_client = bs_dummy.client
57
unit_machine = 'rsyslog/0'
58
remote_script_path = create_check_script_on_unit(
59
rsyslog, unit_machine)
61
ensure_enabling_log_forwarding_forwards_previous_messages(
62
rsyslog, dummy_client, unit_machine, remote_script_path)
63
ensure_multiple_models_forward_messages(
64
rsyslog, dummy_client, unit_machine, remote_script_path)
67
def ensure_multiple_models_forward_messages(
68
rsyslog, dummy, unit_machine, remote_check_path):
69
"""Assert that logs of multiple models are forwarded.
71
:raises JujuAssertionError: If the expected message does not appear in the
73
:raises JujuAssertionError: If the log message check fails in an unexpected
76
enable_log_forwarding(dummy)
78
model1 = dummy.add_model(
79
dummy.env.clone('{}-{}'.format(dummy.env.environment, 'model1')))
81
charm_path = local_charm_path(
82
charm='dummy-source', juju_ver=model1.version)
84
model1.deploy(charm_path)
85
model1.wait_for_started()
87
model1_check_string = get_assert_regex(model1.get_model_uuid())
89
check_remote_log_for_content(
90
rsyslog, unit_machine, model1_check_string, remote_check_path)
93
def ensure_enabling_log_forwarding_forwards_previous_messages(
94
rsyslog, dummy, unit_machine, remote_check_path):
95
"""Assert that mention of the sources logs appear in the sinks logging.
97
Given a rsyslog sink and an output source assert that logging details from
98
the source appear in the sinks logging.
99
Attempt a check over a period of time (10 seconds).
101
:raises JujuAssertionError: If the expected message does not appear in the
103
:raises JujuAssertionError: If the log message check fails in an unexpected
107
uuid = dummy.get_controller_model_uuid()
109
enable_log_forwarding(dummy)
110
check_string = get_assert_regex(uuid)
112
check_remote_log_for_content(
113
rsyslog, unit_machine, check_string, remote_check_path)
116
def check_remote_log_for_content(
117
remote_machine, unit, check_string, script_path):
128
log.info('Check script passed on target machine.')
129
except subprocess.CalledProcessError:
130
# This is where a failure happened
131
raise JujuAssertionError('Forwarded log message never appeared.')
134
def create_check_script_on_unit(client, unit_machine):
135
script_path = os.path.join(os.path.dirname(__file__), 'log_check.py')
136
script_dest_path = os.path.join('/tmp', os.path.basename(script_path))
139
(script_path, '{}:{}'.format(unit_machine, script_dest_path)))
140
return script_dest_path
143
def get_assert_regex(raw_uuid, message=None):
144
"""Create a regex string to check syslog file.
146
If message is supplied it is expected to be escaped as needed (i.e. spaces)
147
no further massaging will be done to the message string.
150
# Maybe over simplified removing the last 8 characters
151
uuid = re.escape(raw_uuid)
152
short_uuid = re.escape(raw_uuid[:-8])
153
date_check = '[A-Z][a-z]{,2}\ +[0-9]+\ +[0-9]{1,2}:[0-9]{1,2}:[0-9]{1,2}'
154
machine = 'machine-0.{}'.format(uuid)
155
agent = 'jujud-machine-agent-{}'.format(short_uuid)
156
message = message or '.*'
158
return '"^{datecheck}\ {machine}\ {agent}\ {message}$"'.format(
159
datecheck=date_check,
165
def enable_log_forwarding(client):
166
client.get_controller_client().set_env_option('logforward-enabled', 'true')
169
def update_client_config(client, rsyslog_details):
170
client.env.config['logforward-enabled'] = False
171
client.env.config.update(rsyslog_details)
174
def deploy_rsyslog(client):
175
"""Deploy and setup the rsyslog charm on client.
177
:returns: Configuration details needed: cert, ca, key and ip:port.
181
client.deploy('rsyslog', (app_name))
182
client.wait_for_started()
183
client.set_config(app_name, {'protocol': 'tcp'})
184
client.juju('expose', app_name)
186
return setup_tls_rsyslog(client, app_name)
189
def setup_tls_rsyslog(client, app_name):
190
unit_machine = '{}/0'.format(app_name)
192
ip_address = get_unit_ipaddress(client, unit_machine)
196
(unit_machine, 'sudo apt-get install rsyslog-gnutls'))
198
with temp_dir() as config_dir:
199
install_rsyslog_config(client, config_dir, unit_machine)
200
rsyslog_details = install_certificates(
201
client, config_dir, ip_address, unit_machine)
203
# restart rsyslog to take into affect all changes
204
client.juju('ssh', (unit_machine, 'sudo', 'service', 'rsyslog', 'restart'))
206
return rsyslog_details
209
def install_certificates(client, config_dir, ip_address, unit_machine):
210
cert, key = certificates.create_certificate(config_dir, ip_address)
212
# Write contents to file to scp across
213
ca_file = os.path.join(config_dir, 'ca.pem')
214
with open(ca_file, 'wt') as f:
215
f.write(certificates.ca_pem_contents)
218
'--', cert, key, ca_file, '{unit}:/home/ubuntu/'.format(
220
client.juju('scp', scp_command)
222
return _get_rsyslog_details(cert, key, ip_address)
225
def _get_rsyslog_details(cert_file, key_file, ip_address):
226
with open(cert_file, 'rt') as f:
227
cert_contents = f.read()
228
with open(key_file, 'rt') as f:
229
key_contents = f.read()
232
'syslog-host': '{}'.format(add_port_to_ip(ip_address, '10514')),
233
'syslog-ca-cert': certificates.ca_pem_contents,
234
'syslog-client-cert': cert_contents,
235
'syslog-client-key': key_contents
239
def add_port_to_ip(ip_address, port):
240
"""Return an ipv4/ipv6 address with port added to `ip_address`."""
242
socket.inet_aton(ip_address)
243
return '{}:{}'.format(ip_address, port)
246
socket.inet_pton(socket.AF_INET6, ip_address)
247
return '[{}]:{}'.format(ip_address, port)
251
'IP Address "{}" is neither an ipv4 or ipv6 address.'.format(
255
def install_rsyslog_config(client, config_dir, unit_machine):
256
config = write_rsyslog_config_file(config_dir)
257
client.juju('scp', (config, '{unit}:/tmp'.format(unit=unit_machine)))
260
(unit_machine, 'sudo', 'mv', '/tmp/{}'.format(
261
os.path.basename(config)), '/etc/rsyslog.d/'))
264
def get_unit_ipaddress(client, unit_name):
265
status = client.get_status()
266
return status.get_unit(unit_name)['public-address']
269
def write_rsyslog_config_file(tmp_dir):
270
"""Write rsyslog config file to `tmp_dir`/10-securelogging.conf."""
272
# make gtls driver the default
273
$DefaultNetstreamDriver gtls
276
$DefaultNetstreamDriverCAFile /home/ubuntu/ca.pem
277
$DefaultNetstreamDriverCertFile /home/ubuntu/cert.pem
278
$DefaultNetstreamDriverKeyFile /home/ubuntu/key.pem
280
$ModLoad imtcp # load TCP listener
281
$InputTCPServerStreamDriverAuthMode x509/name
282
$InputTCPServerStreamDriverPermittedPeer anyServer
283
$InputTCPServerStreamDriverMode 1 # run driver in TLS-only mode
284
$InputTCPServerRun 10514 # port 10514
286
config_path = os.path.join(tmp_dir, '10-securelogging.conf')
287
with open(config_path, 'wt') as f:
292
def parse_args(argv):
293
"""Parse all arguments."""
294
parser = argparse.ArgumentParser(
295
description="Test log forwarding of logs.")
296
add_basic_testing_arguments(parser)
297
return parser.parse_args(argv)
301
args = parse_args(argv)
302
configure_logging(args.verbose)
303
bs_dummy, bs_rsyslog = get_bootstrap_managers(args)
304
assess_log_forward(bs_dummy, bs_rsyslog, args.upload_tools)
308
if __name__ == '__main__':