~andrewjbeach/juju-ci-tools/make-local-patcher

« back to all changes in this revision

Viewing changes to assess_log_forward.py

  • Committer: Curtis Hovey
  • Date: 2016-09-21 17:54:26 UTC
  • mto: This revision was merged to the branch mainline in revision 1612.
  • Revision ID: curtis@canonical.com-20160921175426-hmk3wlxrl1vpfuwr
Poll for the token, whcih might be None.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
"""Test Juju's log forwarding feature.
 
3
 
 
4
Log forwarding allows a controller to forward syslog from all models of a
 
5
controller to a syslog host via TCP (using SSL).
 
6
 
 
7
"""
 
8
 
 
9
from __future__ import print_function
 
10
 
 
11
import argparse
 
12
import logging
 
13
import os
 
14
import re
 
15
import sys
 
16
import socket
 
17
import subprocess
 
18
from textwrap import dedent
 
19
 
 
20
from assess_model_migration import get_bootstrap_managers
 
21
import certificates
 
22
from jujucharm import local_charm_path
 
23
from utility import (
 
24
    add_basic_testing_arguments,
 
25
    configure_logging,
 
26
    JujuAssertionError,
 
27
    temp_dir,
 
28
)
 
29
 
 
30
 
 
31
__metaclass__ = type
 
32
 
 
33
 
 
34
log = logging.getLogger("assess_log_forward")
 
35
 
 
36
 
 
37
def assess_log_forward(bs_dummy, bs_rsyslog, upload_tools):
 
38
    """Ensure logs are forwarded after forwarding enabled after bootstrapping.
 
39
 
 
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
 
44
 
 
45
    """
 
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)
 
50
 
 
51
        update_client_config(bs_dummy.client, rsyslog_details)
 
52
 
 
53
        with bs_dummy.existing_booted_context(upload_tools):
 
54
            log.info('Bootstrapped dummy environment')
 
55
            dummy_client = bs_dummy.client
 
56
 
 
57
            unit_machine = 'rsyslog/0'
 
58
            remote_script_path = create_check_script_on_unit(
 
59
                rsyslog, unit_machine)
 
60
 
 
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)
 
65
 
 
66
 
 
67
def ensure_multiple_models_forward_messages(
 
68
        rsyslog, dummy, unit_machine, remote_check_path):
 
69
    """Assert that logs of multiple models are forwarded.
 
70
 
 
71
    :raises JujuAssertionError: If the expected message does not appear in the
 
72
      given timeframe.
 
73
    :raises JujuAssertionError: If the log message check fails in an unexpected
 
74
      way.
 
75
    """
 
76
    enable_log_forwarding(dummy)
 
77
 
 
78
    model1 = dummy.add_model(
 
79
        dummy.env.clone('{}-{}'.format(dummy.env.environment, 'model1')))
 
80
 
 
81
    charm_path = local_charm_path(
 
82
        charm='dummy-source', juju_ver=model1.version)
 
83
 
 
84
    model1.deploy(charm_path)
 
85
    model1.wait_for_started()
 
86
 
 
87
    model1_check_string = get_assert_regex(model1.get_model_uuid())
 
88
 
 
89
    check_remote_log_for_content(
 
90
        rsyslog, unit_machine, model1_check_string, remote_check_path)
 
91
 
 
92
 
 
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.
 
96
 
 
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).
 
100
 
 
101
    :raises JujuAssertionError: If the expected message does not appear in the
 
102
      given timeframe.
 
103
    :raises JujuAssertionError: If the log message check fails in an unexpected
 
104
      way.
 
105
 
 
106
    """
 
107
    uuid = dummy.get_controller_model_uuid()
 
108
 
 
109
    enable_log_forwarding(dummy)
 
110
    check_string = get_assert_regex(uuid)
 
111
 
 
112
    check_remote_log_for_content(
 
113
        rsyslog, unit_machine, check_string, remote_check_path)
 
114
 
 
115
 
 
116
def check_remote_log_for_content(
 
117
        remote_machine, unit, check_string, script_path):
 
118
    try:
 
119
        remote_machine.juju(
 
120
            'ssh',
 
121
            (
 
122
                unit,
 
123
                'sudo',
 
124
                'python',
 
125
                script_path,
 
126
                check_string,
 
127
                '/var/log/syslog'))
 
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.')
 
132
 
 
133
 
 
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))
 
137
    client.juju(
 
138
        'scp',
 
139
        (script_path, '{}:{}'.format(unit_machine, script_dest_path)))
 
140
    return script_dest_path
 
141
 
 
142
 
 
143
def get_assert_regex(raw_uuid, message=None):
 
144
    """Create a regex string to check syslog file.
 
145
 
 
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.
 
148
 
 
149
    """
 
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 '.*'
 
157
 
 
158
    return '"^{datecheck}\ {machine}\ {agent}\ {message}$"'.format(
 
159
        datecheck=date_check,
 
160
        machine=machine,
 
161
        agent=agent,
 
162
        message=message)
 
163
 
 
164
 
 
165
def enable_log_forwarding(client):
 
166
    client.get_controller_client().set_env_option('logforward-enabled', 'true')
 
167
 
 
168
 
 
169
def update_client_config(client, rsyslog_details):
 
170
    client.env.config['logforward-enabled'] = False
 
171
    client.env.config.update(rsyslog_details)
 
172
 
 
173
 
 
174
def deploy_rsyslog(client):
 
175
    """Deploy and setup the rsyslog charm on client.
 
176
 
 
177
    :returns: Configuration details needed: cert, ca, key and ip:port.
 
178
 
 
179
    """
 
180
    app_name = 'rsyslog'
 
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)
 
185
 
 
186
    return setup_tls_rsyslog(client, app_name)
 
187
 
 
188
 
 
189
def setup_tls_rsyslog(client, app_name):
 
190
    unit_machine = '{}/0'.format(app_name)
 
191
 
 
192
    ip_address = get_unit_ipaddress(client, unit_machine)
 
193
 
 
194
    client.juju(
 
195
        'ssh',
 
196
        (unit_machine, 'sudo apt-get install rsyslog-gnutls'))
 
197
 
 
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)
 
202
 
 
203
    # restart rsyslog to take into affect all changes
 
204
    client.juju('ssh', (unit_machine, 'sudo', 'service', 'rsyslog', 'restart'))
 
205
 
 
206
    return rsyslog_details
 
207
 
 
208
 
 
209
def install_certificates(client, config_dir, ip_address, unit_machine):
 
210
    cert, key = certificates.create_certificate(config_dir, ip_address)
 
211
 
 
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)
 
216
 
 
217
    scp_command = (
 
218
        '--', cert, key, ca_file, '{unit}:/home/ubuntu/'.format(
 
219
            unit=unit_machine))
 
220
    client.juju('scp', scp_command)
 
221
 
 
222
    return _get_rsyslog_details(cert, key, ip_address)
 
223
 
 
224
 
 
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()
 
230
 
 
231
    return {
 
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
 
236
    }
 
237
 
 
238
 
 
239
def add_port_to_ip(ip_address, port):
 
240
    """Return an ipv4/ipv6 address with port added to `ip_address`."""
 
241
    try:
 
242
        socket.inet_aton(ip_address)
 
243
        return '{}:{}'.format(ip_address, port)
 
244
    except socket.error:
 
245
        try:
 
246
            socket.inet_pton(socket.AF_INET6, ip_address)
 
247
            return '[{}]:{}'.format(ip_address, port)
 
248
        except socket.error:
 
249
            pass
 
250
    raise ValueError(
 
251
        'IP Address "{}" is neither an ipv4 or ipv6 address.'.format(
 
252
            ip_address))
 
253
 
 
254
 
 
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)))
 
258
    client.juju(
 
259
        'ssh',
 
260
        (unit_machine, 'sudo', 'mv', '/tmp/{}'.format(
 
261
            os.path.basename(config)), '/etc/rsyslog.d/'))
 
262
 
 
263
 
 
264
def get_unit_ipaddress(client, unit_name):
 
265
    status = client.get_status()
 
266
    return status.get_unit(unit_name)['public-address']
 
267
 
 
268
 
 
269
def write_rsyslog_config_file(tmp_dir):
 
270
    """Write rsyslog config file to `tmp_dir`/10-securelogging.conf."""
 
271
    config = dedent("""\
 
272
    # make gtls driver the default
 
273
    $DefaultNetstreamDriver gtls
 
274
 
 
275
    # certificate files
 
276
    $DefaultNetstreamDriverCAFile /home/ubuntu/ca.pem
 
277
    $DefaultNetstreamDriverCertFile /home/ubuntu/cert.pem
 
278
    $DefaultNetstreamDriverKeyFile /home/ubuntu/key.pem
 
279
 
 
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
 
285
    """)
 
286
    config_path = os.path.join(tmp_dir, '10-securelogging.conf')
 
287
    with open(config_path, 'wt') as f:
 
288
        f.write(config)
 
289
    return config_path
 
290
 
 
291
 
 
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)
 
298
 
 
299
 
 
300
def main(argv=None):
 
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)
 
305
    return 0
 
306
 
 
307
 
 
308
if __name__ == '__main__':
 
309
    sys.exit(main())