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())