github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/acceptancetests/assess_log_forward.py (about) 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 get_unit_public_ip, 27 JujuAssertionError, 28 temp_dir, 29 ) 30 31 32 __metaclass__ = type 33 34 35 log = logging.getLogger("assess_log_forward") 36 37 38 def assess_log_forward(bs_dummy, bs_rsyslog, upload_tools): 39 """Ensure logs are forwarded after forwarding enabled after bootstrapping. 40 41 Given 2 controllers set rsyslog and dummy: 42 - setup rsyslog with secure details 43 - Enable log forwarding on dummy 44 - Ensure intial logs are present in the rsyslog sinks logs 45 46 """ 47 with bs_rsyslog.booted_context(upload_tools): 48 log.info('Bootstrapped rsyslog environment') 49 rsyslog = bs_rsyslog.client 50 rsyslog_details = deploy_rsyslog(rsyslog) 51 52 update_client_config(bs_dummy.client, rsyslog_details) 53 54 with bs_dummy.existing_booted_context(upload_tools): 55 log.info('Bootstrapped dummy environment') 56 dummy_client = bs_dummy.client 57 58 unit_machine = 'rsyslog/0' 59 remote_script_path = create_check_script_on_unit( 60 rsyslog, unit_machine) 61 62 ensure_enabling_log_forwarding_forwards_previous_messages( 63 rsyslog, dummy_client, unit_machine, remote_script_path) 64 ensure_multiple_models_forward_messages( 65 rsyslog, dummy_client, unit_machine, remote_script_path) 66 67 68 def ensure_multiple_models_forward_messages( 69 rsyslog, dummy, unit_machine, remote_check_path): 70 """Assert that logs of multiple models are forwarded. 71 72 :raises JujuAssertionError: If the expected message does not appear in the 73 given timeframe. 74 :raises JujuAssertionError: If the log message check fails in an unexpected 75 way. 76 """ 77 model1 = dummy.add_model('{}-{}'.format(dummy.env.environment, 'model1')) 78 79 charm_path = local_charm_path( 80 charm='dummy-source', juju_ver=model1.version) 81 82 enable_log_forwarding(model1) 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.set_env_option('logforward-enabled', 'true') 167 client.get_controller_client().set_env_option('logforward-enabled', 'true') 168 169 170 def update_client_config(client, rsyslog_details): 171 client.env.update_config({'logforward-enabled': False}) 172 client.env.update_config(rsyslog_details) 173 174 175 def deploy_rsyslog(client): 176 """Deploy and setup the rsyslog charm on client. 177 178 :returns: Configuration details needed: cert, ca, key and ip:port. 179 180 """ 181 app_name = 'rsyslog' 182 client.deploy('rsyslog', (app_name)) 183 client.wait_for_started() 184 client.set_config(app_name, {'protocol': 'tcp'}) 185 client.juju('expose', app_name) 186 187 return setup_tls_rsyslog(client, app_name) 188 189 190 def setup_tls_rsyslog(client, app_name): 191 unit_machine = '{}/0'.format(app_name) 192 193 ip_address = get_unit_public_ip(client, unit_machine) 194 195 client.juju( 196 'ssh', 197 (unit_machine, 'sudo apt-get install rsyslog-gnutls')) 198 199 with temp_dir() as config_dir: 200 install_rsyslog_config(client, config_dir, unit_machine) 201 rsyslog_details = install_certificates( 202 client, config_dir, ip_address, unit_machine) 203 204 # restart rsyslog to take into affect all changes 205 client.juju('ssh', (unit_machine, 'sudo', 'service', 'rsyslog', 'restart')) 206 207 return rsyslog_details 208 209 210 def install_certificates(client, config_dir, ip_address, unit_machine): 211 cert, key = certificates.create_certificate(config_dir, ip_address) 212 213 # Write contents to file to scp across 214 ca_file = os.path.join(config_dir, 'ca.pem') 215 with open(ca_file, 'wt') as f: 216 f.write(certificates.ca_pem_contents) 217 218 scp_command = ( 219 '--', cert, key, ca_file, '{unit}:/home/ubuntu/'.format( 220 unit=unit_machine)) 221 client.juju('scp', scp_command) 222 223 return _get_rsyslog_details(cert, key, ip_address) 224 225 226 def _get_rsyslog_details(cert_file, key_file, ip_address): 227 with open(cert_file, 'rt') as f: 228 cert_contents = f.read() 229 with open(key_file, 'rt') as f: 230 key_contents = f.read() 231 232 return { 233 'syslog-host': '{}'.format(add_port_to_ip(ip_address, '10514')), 234 'syslog-ca-cert': certificates.ca_pem_contents, 235 'syslog-client-cert': cert_contents, 236 'syslog-client-key': key_contents 237 } 238 239 240 def add_port_to_ip(ip_address, port): 241 """Return an ipv4/ipv6 address with port added to `ip_address`.""" 242 try: 243 socket.inet_aton(ip_address) 244 return '{}:{}'.format(ip_address, port) 245 except socket.error: 246 try: 247 socket.inet_pton(socket.AF_INET6, ip_address) 248 return '[{}]:{}'.format(ip_address, port) 249 except socket.error: 250 pass 251 raise ValueError( 252 'IP Address "{}" is neither an ipv4 or ipv6 address.'.format( 253 ip_address)) 254 255 256 def install_rsyslog_config(client, config_dir, unit_machine): 257 config = write_rsyslog_config_file(config_dir) 258 client.juju('scp', (config, '{unit}:/tmp'.format(unit=unit_machine))) 259 client.juju( 260 'ssh', 261 (unit_machine, 'sudo', 'mv', '/tmp/{}'.format( 262 os.path.basename(config)), '/etc/rsyslog.d/')) 263 264 265 def write_rsyslog_config_file(tmp_dir): 266 """Write rsyslog config file to `tmp_dir`/10-securelogging.conf.""" 267 config = dedent("""\ 268 # make gtls driver the default 269 $DefaultNetstreamDriver gtls 270 271 # certificate files 272 $DefaultNetstreamDriverCAFile /home/ubuntu/ca.pem 273 $DefaultNetstreamDriverCertFile /home/ubuntu/cert.pem 274 $DefaultNetstreamDriverKeyFile /home/ubuntu/key.pem 275 276 $ModLoad imtcp # load TCP listener 277 $InputTCPServerStreamDriverAuthMode x509/name 278 $InputTCPServerStreamDriverPermittedPeer anyServer 279 $InputTCPServerStreamDriverMode 1 # run driver in TLS-only mode 280 $InputTCPServerRun 10514 # port 10514 281 """) 282 config_path = os.path.join(tmp_dir, '10-securelogging.conf') 283 with open(config_path, 'wt') as f: 284 f.write(config) 285 return config_path 286 287 288 def parse_args(argv): 289 """Parse all arguments.""" 290 parser = argparse.ArgumentParser( 291 description="Test log forwarding of logs.") 292 # Don't use existing as this test modifies controller settings. 293 add_basic_testing_arguments(parser, existing=False) 294 return parser.parse_args(argv) 295 296 297 def main(argv=None): 298 args = parse_args(argv) 299 configure_logging(args.verbose) 300 bs_dummy, bs_rsyslog = get_bootstrap_managers(args) 301 assess_log_forward(bs_dummy, bs_rsyslog, args.upload_tools) 302 return 0 303 304 305 if __name__ == '__main__': 306 sys.exit(main())