Migrate security group logging tests
Changes from original tests: - Adjust migrated imports and configuration. - All tests needed added code to run on both podified/devstack. - Replaced test UUIDs with unique ones. - Migrated tester (instead of undercloud) plugin options for BGP jobs command validations. - Configuration files in tests according to OSP type. - Log files in tests according to OSP type. - Services restart according to OSP type. - New set service setting method verified for podified/devstack. - New base method 'is_setup_single_node'. - nc fixes: timeout added for udp check, replaced to short options. - Skip unsupported extensions list CLI test on devstack. - SGRs/ACLs amount test adjusted to devstack stateless/stateful 2 meter names. - Bash command wrapper fix since sh can't source openrc. Change-Id: Iae93a0db4a7d2f00a148c632bd6cc9dc90decd3b
This commit is contained in:
parent
e393f1248a
commit
a78af0235f
|
@ -22,6 +22,22 @@ whitebox_neutron_plugin_options = cfg.OptGroup(
|
||||||
)
|
)
|
||||||
|
|
||||||
WhiteboxNeutronPluginOptions = [
|
WhiteboxNeutronPluginOptions = [
|
||||||
|
cfg.BoolOpt('exec_on_tester',
|
||||||
|
default=True,
|
||||||
|
help='Specify whether to run validated commands on current '
|
||||||
|
'testing node or other host using ssh.'),
|
||||||
|
cfg.StrOpt('tester_ip',
|
||||||
|
default='127.0.0.1',
|
||||||
|
help='IP of host to execute validated commands.'),
|
||||||
|
cfg.StrOpt('tester_user',
|
||||||
|
default='zuul',
|
||||||
|
help='User at host to execute validated commands.'),
|
||||||
|
cfg.StrOpt('tester_pass',
|
||||||
|
default='12345678',
|
||||||
|
help='Password at host to execute validated commands.'),
|
||||||
|
cfg.StrOpt('tester_key_file',
|
||||||
|
default='',
|
||||||
|
help='Key file to access host to execute validated commands.'),
|
||||||
cfg.BoolOpt('node_power_change',
|
cfg.BoolOpt('node_power_change',
|
||||||
default=True,
|
default=True,
|
||||||
help='Whether to power off/on nodes, '
|
help='Whether to power off/on nodes, '
|
||||||
|
|
|
@ -259,6 +259,12 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase):
|
||||||
host['is_networker'] = (
|
host['is_networker'] = (
|
||||||
True if host['name'] in l3_agent_hosts else False)
|
True if host['name'] in l3_agent_hosts else False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_setup_single_node(cls):
|
||||||
|
if not hasattr(cls, 'nodes'):
|
||||||
|
cls.discover_nodes()
|
||||||
|
return len(cls.nodes) == 1
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_pod_of_service(cls, service='neutron'):
|
def get_pod_of_service(cls, service='neutron'):
|
||||||
pods_list = "oc get pods"
|
pods_list = "oc get pods"
|
||||||
|
|
|
@ -0,0 +1,922 @@
|
||||||
|
# Copyright 2024 Red Hat, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from pprint import pformat
|
||||||
|
import random
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
|
||||||
|
from neutron_lib import constants as neutron_lib_constants
|
||||||
|
from neutron_tempest_plugin.api import base as api_base
|
||||||
|
from neutron_tempest_plugin.common import shell
|
||||||
|
from neutron_tempest_plugin.common import ssh
|
||||||
|
from neutron_tempest_plugin.common import utils as common_utils
|
||||||
|
from neutron_tempest_plugin import config
|
||||||
|
from neutron_tempest_plugin import exceptions as bb_exceptions
|
||||||
|
from neutron_tempest_plugin.scenario import constants as const
|
||||||
|
from oslo_log import log
|
||||||
|
from paramiko import ssh_exception as ssh_exc
|
||||||
|
from tempest.common import waiters
|
||||||
|
from tempest.lib.common.utils import data_utils
|
||||||
|
from tempest.lib.common.utils import test_utils
|
||||||
|
from tempest.lib import decorators
|
||||||
|
from tempest.lib import exceptions as lib_exc
|
||||||
|
|
||||||
|
from whitebox_neutron_tempest_plugin.tests.scenario import base as wb_base
|
||||||
|
|
||||||
|
|
||||||
|
CONF = config.CONF
|
||||||
|
WB_CONF = CONF.whitebox_neutron_plugin_options
|
||||||
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseSecGroupLoggingTest(
|
||||||
|
wb_base.BaseTempestTestCaseOvn, api_base.BaseAdminNetworkTest):
|
||||||
|
credentials = ['primary', 'admin']
|
||||||
|
required_extensions = ['router', 'security-group', 'logging']
|
||||||
|
|
||||||
|
if WB_CONF.openstack_type == 'podified':
|
||||||
|
SG_LOG_FILE = '/var/log/containers/openvswitch/ovn-controller.log'
|
||||||
|
# NOTE(mblue): Configuration path not needed in next gen
|
||||||
|
ML2_CONF_FILE = ''
|
||||||
|
rotate_service_fix = '-crond'
|
||||||
|
elif WB_CONF.openstack_type == 'devstack':
|
||||||
|
SG_LOG_FILE = '/opt/stack/logs/ovn-controller.log'
|
||||||
|
ML2_CONF_FILE = '/etc/neutron/plugins/ml2/ml2_conf.ini'
|
||||||
|
rotate_service_fix = ''
|
||||||
|
else:
|
||||||
|
SG_LOG_FILE = '/var/log/containers/stdouts/ovn_controller.log'
|
||||||
|
ML2_CONF_FILE = ('/var/lib/config-data/puppet-generated'
|
||||||
|
'/neutron/etc/neutron/plugins/ml2/ml2_conf.ini')
|
||||||
|
rotate_service_fix = '-crond'
|
||||||
|
ROTATION_CHECK_CMD = (
|
||||||
|
'/usr/sbin/logrotate -s /var/lib/logrotate/'
|
||||||
|
'logrotate{0}.status /etc/logrotate{0}.conf').format(
|
||||||
|
rotate_service_fix)
|
||||||
|
SSH_DROP_EXCEPTIONS = (lib_exc.SSHTimeout,
|
||||||
|
ssh_exc.NoValidConnectionsError,
|
||||||
|
ssh_exc.SSHException,
|
||||||
|
socket.error,
|
||||||
|
socket.timeout,
|
||||||
|
ConnectionResetError,
|
||||||
|
EOFError)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resource_setup(cls):
|
||||||
|
super(BaseSecGroupLoggingTest, cls).resource_setup()
|
||||||
|
cls.discover_nodes()
|
||||||
|
for node in cls.nodes:
|
||||||
|
if not node['is_controller']:
|
||||||
|
continue
|
||||||
|
cls.check_service_setting(
|
||||||
|
host=node,
|
||||||
|
service='neutron',
|
||||||
|
config_files=(WB_CONF.neutron_config,),
|
||||||
|
param='service_plugins',
|
||||||
|
value='log',
|
||||||
|
msg='Security group logging not supported, skipping tests.')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_credentials(cls):
|
||||||
|
super(BaseSecGroupLoggingTest, cls).setup_credentials()
|
||||||
|
cls.network_client = cls.os_admin.network_client
|
||||||
|
if hasattr(
|
||||||
|
CONF.neutron_plugin_options, 'default_image_is_advanced') and \
|
||||||
|
CONF.neutron_plugin_options.default_image_is_advanced:
|
||||||
|
cls.flavor_ref = CONF.compute.flavor_ref
|
||||||
|
cls.image_ref = CONF.compute.image_ref
|
||||||
|
cls.username = CONF.validation.image_ssh_user
|
||||||
|
else:
|
||||||
|
cls.flavor_ref = (
|
||||||
|
CONF.neutron_plugin_options.advanced_image_flavor_ref)
|
||||||
|
cls.image_ref = CONF.neutron_plugin_options.advanced_image_ref
|
||||||
|
cls.username = CONF.neutron_plugin_options.advanced_image_ssh_user
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup_clients(cls):
|
||||||
|
super(BaseSecGroupLoggingTest, cls).setup_clients()
|
||||||
|
cls.project_id = cls.os_primary.credentials.tenant_id
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _common_resource_setup(cls):
|
||||||
|
"""Setup resources for both classes of security group logging tests:
|
||||||
|
with either stateless/stateful security groups.
|
||||||
|
"""
|
||||||
|
cls.network = cls.create_network()
|
||||||
|
cls.subnet = cls.create_subnet(cls.network)
|
||||||
|
router = cls.create_router_by_client()
|
||||||
|
cls.create_router_interface(router['id'], cls.subnet['id'])
|
||||||
|
cls.keypair = cls.create_keypair()
|
||||||
|
sg_kwargs = {
|
||||||
|
'name': data_utils.rand_name(
|
||||||
|
'{}-security-group'.format(cls.__class__.__name__))}
|
||||||
|
if not cls.is_secgrp_stateful:
|
||||||
|
sg_kwargs['stateful'] = False
|
||||||
|
cls.secgrp = cls.os_primary.network_client.create_security_group(
|
||||||
|
**sg_kwargs)['security_group']
|
||||||
|
cls.security_groups.append(cls.secgrp)
|
||||||
|
cls.icmp_rule = cls.create_pingable_secgroup_rule(
|
||||||
|
secgroup_id=cls.secgrp['id'])
|
||||||
|
if not cls.is_secgrp_stateful:
|
||||||
|
# NOTE(slaweq): in case of stateless security groups, there is no
|
||||||
|
# "related" or "established" traffic matching at all so even if
|
||||||
|
# egress traffic to 169.254.169.254 is allowed by default SG, we
|
||||||
|
# need to explicitly allow ingress traffic from the metadata server
|
||||||
|
# to be able to receive responses in the guest vm
|
||||||
|
cls.create_security_group_rule(
|
||||||
|
security_group_id=cls.secgrp['id'],
|
||||||
|
direction=neutron_lib_constants.INGRESS_DIRECTION,
|
||||||
|
protocol=neutron_lib_constants.PROTO_NAME_TCP,
|
||||||
|
remote_ip_prefix='169.254.169.254/32',
|
||||||
|
description='metadata out')
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(BaseSecGroupLoggingTest, self).setUp()
|
||||||
|
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
|
||||||
|
self.network_client.reset_quotas, self.project_id)
|
||||||
|
self.network_client.update_quotas(
|
||||||
|
self.project_id, security_group=-1, security_group_rule=-1)
|
||||||
|
self._hypervisors_counts = dict()
|
||||||
|
|
||||||
|
def _create_ssh_client(self, host, username=None, key_file=None):
|
||||||
|
"""Returns ssh client.
|
||||||
|
|
||||||
|
Default arguments return client with common credentials for tenant VM.
|
||||||
|
|
||||||
|
In case no key_file argument specified, using inherited self.keypair,
|
||||||
|
and if that fails, trying keys from ssh agent or trying any found key
|
||||||
|
under ~/.ssh/ as last resort.
|
||||||
|
"""
|
||||||
|
if not username:
|
||||||
|
username = self.username
|
||||||
|
if not key_file:
|
||||||
|
pkey = self.keypair['private_key']
|
||||||
|
else:
|
||||||
|
pkey = None
|
||||||
|
try:
|
||||||
|
_client = ssh.Client(host=host,
|
||||||
|
username=username,
|
||||||
|
pkey=pkey,
|
||||||
|
key_filename=key_file)
|
||||||
|
except ssh_exc.SSHException:
|
||||||
|
_client = ssh.Client(host=host,
|
||||||
|
username=username,
|
||||||
|
look_for_keys=True)
|
||||||
|
return _client
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def try_ssh_traffic(cls, ssh_client):
|
||||||
|
"""Let simple ssh command be carried out or time out."""
|
||||||
|
try:
|
||||||
|
ssh_client.exec_command('true', timeout=30)
|
||||||
|
except cls.SSH_DROP_EXCEPTIONS:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _try_traffic(_transport_type, fip, port, timeout=None):
|
||||||
|
# NOTE(mblue): long form nc options not supported in cirros
|
||||||
|
_udp_opt = ' -u' if _transport_type == 'udp' else ''
|
||||||
|
cmd = 'echo redhatcat | nc{} -w {} {} {}'.format(
|
||||||
|
_udp_opt, timeout, fip, port)
|
||||||
|
try:
|
||||||
|
shell.execute(cmd)
|
||||||
|
except bb_exceptions.ShellCommandFailed:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def try_tcp_traffic(self, fip, port=9999, timeout=10):
|
||||||
|
"""Try to initiate a TCP connection, regardless if succeeded."""
|
||||||
|
self._try_traffic('tcp', fip, port, timeout)
|
||||||
|
|
||||||
|
def try_udp_traffic(self, fip, port=9999, timeout=10):
|
||||||
|
"""Try to send UDP traffic."""
|
||||||
|
self._try_traffic('udp', fip, port, timeout)
|
||||||
|
|
||||||
|
def _get_logs_and_counts(self, hypervisor_ssh, start_track):
|
||||||
|
# create dictionary to track values of a hypervisor if it doesn't exist
|
||||||
|
self._hypervisors_counts.setdefault(hypervisor_ssh.host, dict())
|
||||||
|
# tracks A value, before test traffic sent to be logged
|
||||||
|
if start_track:
|
||||||
|
_track_value = int(hypervisor_ssh.exec_command(
|
||||||
|
"sudo grep acl_log {} | tail -n1 | cut -d '|' -f 2"
|
||||||
|
.format(self.SG_LOG_FILE), timeout=120))
|
||||||
|
self._hypervisors_counts[hypervisor_ssh.host]['A'] = _track_value
|
||||||
|
LOG.debug("Start log count value A on '%s' is %d",
|
||||||
|
hypervisor_ssh.host, _track_value)
|
||||||
|
# tracks B value, after test traffic sent to be logged
|
||||||
|
# (extracts logs from file right away, to avoid race conditions).
|
||||||
|
else:
|
||||||
|
cmds_output = hypervisor_ssh.exec_command(
|
||||||
|
("B=$(sudo grep acl_log {0} | tail -n1 | cut -d '|' -f 2 | "
|
||||||
|
"sed 's/^0*//') && echo $B && "
|
||||||
|
"sudo grep acl_log {0} | tail -n $(($B-{1}))").format(
|
||||||
|
self.SG_LOG_FILE,
|
||||||
|
self._hypervisors_counts[hypervisor_ssh.host]['A']),
|
||||||
|
timeout=120).splitlines()
|
||||||
|
# save B in instance, and log in tempest the B value
|
||||||
|
_track_value = int(cmds_output[0])
|
||||||
|
self._hypervisors_counts[hypervisor_ssh.host]['B'] = _track_value
|
||||||
|
LOG.debug("End log count value B on '%s' is %d",
|
||||||
|
hypervisor_ssh.host, _track_value)
|
||||||
|
# parse and save logs retrieved, per hypervisor tracked counts
|
||||||
|
self._hypervisors_counts[hypervisor_ssh.host][
|
||||||
|
'tested_logs'] = "\n".join(cmds_output[1:])
|
||||||
|
# log in tempest the retrieved entries amount
|
||||||
|
_test_logs_amount = \
|
||||||
|
self._hypervisors_counts[hypervisor_ssh.host]['B'] - \
|
||||||
|
self._hypervisors_counts[hypervisor_ssh.host]['A']
|
||||||
|
self._hypervisors_counts[hypervisor_ssh.host][
|
||||||
|
'test_logs_amount'] = _test_logs_amount
|
||||||
|
LOG.debug(
|
||||||
|
"Retrieved %d log entries for test assertions from '%s'.",
|
||||||
|
_test_logs_amount, hypervisor_ssh.host)
|
||||||
|
|
||||||
|
def start_track_log(self, hypervisor_ssh):
|
||||||
|
"""Sets count start value (A), in order to track detla of relevant
|
||||||
|
log entries for test.
|
||||||
|
|
||||||
|
Usually done before test traffic is sent.
|
||||||
|
|
||||||
|
Multiple hypervisors (computes) can be tracked simultaneously.
|
||||||
|
"""
|
||||||
|
self._get_logs_and_counts(hypervisor_ssh, start_track=True)
|
||||||
|
|
||||||
|
def retrieve_tracked_log(self, hypervisor_ssh):
|
||||||
|
"""Sets count end value (B), and accordingly save only test log
|
||||||
|
entries of relevant given hypervisor, used later for test assertions.
|
||||||
|
(In same ssh session and consecutive command to avoid race condition).
|
||||||
|
|
||||||
|
Multiple hypervisors (computes) can be tracked simultaneously.
|
||||||
|
"""
|
||||||
|
self._get_logs_and_counts(hypervisor_ssh, start_track=False)
|
||||||
|
|
||||||
|
def _check_log(self, should_log, pattern, fail_msg, hypervisor_ssh):
|
||||||
|
if should_log:
|
||||||
|
self.assertRegex(
|
||||||
|
self._hypervisors_counts[hypervisor_ssh.host]['tested_logs'],
|
||||||
|
pattern,
|
||||||
|
fail_msg.format('should'))
|
||||||
|
else:
|
||||||
|
self.assertNotRegex(
|
||||||
|
self._hypervisors_counts[hypervisor_ssh.host]['tested_logs'],
|
||||||
|
pattern,
|
||||||
|
fail_msg.format('should not'))
|
||||||
|
|
||||||
|
def check_log_tcp(self, should_log, hypervisor_ssh, port=9999):
|
||||||
|
pattern = 'acl_log.*tcp.*tp_dst={}'.format(port)
|
||||||
|
fail_msg = ('TCP traffic to port {} {{}} '
|
||||||
|
'be logged in test log entries.').format(port)
|
||||||
|
self._check_log(should_log, pattern, fail_msg, hypervisor_ssh)
|
||||||
|
|
||||||
|
def check_log_udp(self, should_log, hypervisor_ssh, port=9999):
|
||||||
|
pattern = 'acl_log.*udp.*tp_dst={}'.format(port)
|
||||||
|
fail_msg = ('UDP traffic to port {} {{}} '
|
||||||
|
'be logged in test log entries.').format(port)
|
||||||
|
self._check_log(should_log, pattern, fail_msg, hypervisor_ssh)
|
||||||
|
|
||||||
|
def check_log_icmp(
|
||||||
|
self, should_log, hypervisor_ssh, both_directions=False):
|
||||||
|
fail_msg = 'ICMP {} traffic {{}} be logged in tested log entries.'
|
||||||
|
# pairs of logging pattern, and matching failure message
|
||||||
|
patterns = [
|
||||||
|
(r'acl_log.*icmp.*icmp_type=8', fail_msg.format('request'))]
|
||||||
|
if both_directions:
|
||||||
|
patterns.append(
|
||||||
|
(r'acl_log.*icmp.*icmp_type=0', fail_msg.format('reply')))
|
||||||
|
for ptn in patterns:
|
||||||
|
self._check_log(should_log, *ptn, hypervisor_ssh)
|
||||||
|
|
||||||
|
def check_log_ssh(self, should_log, hypervisor_ssh):
|
||||||
|
# in RFE all test cases use drop verdict for ssh, therefore hardcoded
|
||||||
|
pattern = r'acl_log.*verdict=drop.*tcp.*tp_dst=22'
|
||||||
|
fail_msg = 'ssh traffic {} be logged in tested log entries.'
|
||||||
|
self._check_log(should_log, pattern, fail_msg, hypervisor_ssh)
|
||||||
|
|
||||||
|
# Wrapper function for server creation with hypervisor info,
|
||||||
|
# floating ip, vm and hypervisor ssh clients.
|
||||||
|
#
|
||||||
|
# VM on same/different compute according to ID, using schedular hints.
|
||||||
|
#
|
||||||
|
# Returns dictionary of server details with additions.
|
||||||
|
def _create_server(self, scheduler_hints=None):
|
||||||
|
server_params = {
|
||||||
|
'flavor_ref': self.flavor_ref,
|
||||||
|
'image_ref': self.image_ref,
|
||||||
|
'key_name': self.keypair['name'],
|
||||||
|
'networks': [{'uuid': self.network['id']}],
|
||||||
|
'security_groups': [{'name': self.secgrp['id']}],
|
||||||
|
'name': data_utils.rand_name('{}-vm'.format(
|
||||||
|
self.__class__.__name__))
|
||||||
|
}
|
||||||
|
if scheduler_hints:
|
||||||
|
server_params['scheduler_hints'] = scheduler_hints
|
||||||
|
server_id = self.create_server(**server_params)['server']['id']
|
||||||
|
waiters.wait_for_server_status(
|
||||||
|
self.os_admin.servers_client, server_id,
|
||||||
|
const.SERVER_STATUS_ACTIVE)
|
||||||
|
server = self.os_admin.servers_client.show_server(
|
||||||
|
server_id)['server']
|
||||||
|
server_port = self.client.list_ports(
|
||||||
|
network_id=self.network['id'],
|
||||||
|
device_id=server['id'])['ports'][0]
|
||||||
|
server['fip'] = self.create_floatingip(
|
||||||
|
port=server_port)['floating_ip_address']
|
||||||
|
self.ping_ip_address(server['fip'])
|
||||||
|
server['ssh_client'] = self._create_ssh_client(server['fip'])
|
||||||
|
server['hv_base_name'] = server[
|
||||||
|
'OS-EXT-SRV-ATTR:hypervisor_hostname'].split('.')[0]
|
||||||
|
server['hv_ssh_client'] = self.find_node_client(
|
||||||
|
server['hv_base_name'])
|
||||||
|
return server
|
||||||
|
|
||||||
|
# Wrapper function for common creation of logs
|
||||||
|
# Returns dictionary of log details.
|
||||||
|
def _create_log(self,
|
||||||
|
name='',
|
||||||
|
resource_id='',
|
||||||
|
event='ALL',
|
||||||
|
description='',
|
||||||
|
enabled=True,
|
||||||
|
add_cleanup=True):
|
||||||
|
_log = self.create_log(
|
||||||
|
name=name or data_utils.rand_name('{}-test-log'.format(
|
||||||
|
self.__class__.__name__)),
|
||||||
|
description=description,
|
||||||
|
resource_id=resource_id or self.secgrp['id'],
|
||||||
|
event=event,
|
||||||
|
enabled=enabled)
|
||||||
|
if add_cleanup:
|
||||||
|
self.addCleanup(self.admin_client.delete_log, _log['id'])
|
||||||
|
return _log
|
||||||
|
|
||||||
|
def verify_meter_and_band_amounts(self, meter_range, meter_band_range):
|
||||||
|
meter_count = int(self.master_node_client.exec_command(
|
||||||
|
self.nbctl + ' list meter | grep _uuid | wc -l'))
|
||||||
|
meter_band_count = int(self.master_node_client.exec_command(
|
||||||
|
self.nbctl + ' list meter-band | grep _uuid | wc -l'))
|
||||||
|
self.assertIn(
|
||||||
|
meter_count, meter_range,
|
||||||
|
"Log meter count in NB '{}' not as expected '{}'".format(
|
||||||
|
meter_count, meter_range))
|
||||||
|
self.assertIn(
|
||||||
|
meter_band_count, meter_band_range,
|
||||||
|
"Log meter band count in NB '{}' not as expected '{}'".format(
|
||||||
|
meter_band_count, meter_band_range))
|
||||||
|
|
||||||
|
# Logging tests for both stateful/stateless security group classes
|
||||||
|
|
||||||
|
def _test_log_commands(self):
|
||||||
|
"""Test verifies OpenStack log CLI commands run successfully,
|
||||||
|
and return the expected output.
|
||||||
|
"""
|
||||||
|
prefix = self.get_osp_cmd_prefix()
|
||||||
|
log_name_1 = data_utils.rand_name('cli-test-log-1')
|
||||||
|
log_name_2 = data_utils.rand_name('cli-test-log-2')
|
||||||
|
cmds = [
|
||||||
|
# 1) verify that logging extension is enabled
|
||||||
|
'openstack extension list --fit-width | grep logging',
|
||||||
|
# 2) make sure logs are enabled for security groups
|
||||||
|
'openstack network loggable resources list',
|
||||||
|
# 3) perform logging for a specific security group
|
||||||
|
('openstack network log create --resource-type security_group '
|
||||||
|
'--resource {} --event ALL {}').format(
|
||||||
|
self.secgrp['id'], log_name_1),
|
||||||
|
# 4) show details of the first logging object
|
||||||
|
'openstack network log show {}'.format(log_name_1),
|
||||||
|
# 5) perform logging of accept events for all security groups
|
||||||
|
('openstack network log create --resource-type security_group '
|
||||||
|
'--event ACCEPT {}').format(log_name_2),
|
||||||
|
# 6) list existing logging objects
|
||||||
|
'openstack network log list',
|
||||||
|
# 7) delete both new logging objects
|
||||||
|
'openstack network log delete {} {}'.format(
|
||||||
|
log_name_1, log_name_2)]
|
||||||
|
stdout_patterns = [
|
||||||
|
# 1) verify that logging extension is enabled in output
|
||||||
|
'Logging API Extension',
|
||||||
|
# 2) check logs are enabled for security groups in output
|
||||||
|
'security_group',
|
||||||
|
# 3) check create log per SG command outputs enabled log, for all
|
||||||
|
# events, with desired name
|
||||||
|
'|.*Enabled.*|.*True.*|.*Event.*|.*ALL.*|.*Name.*|.*{}'.format(
|
||||||
|
log_name_1),
|
||||||
|
# 4) check show log per SG command outputs enabled log, for all
|
||||||
|
# events, with desired name
|
||||||
|
'|.*Enabled.*|.*True.*|.*Event.*|.*ALL.*|.*Name.*|.*{}'.format(
|
||||||
|
log_name_1),
|
||||||
|
# 5) check create log for all SGs command has enabled log,
|
||||||
|
# for accept events, with desired name, no resource attached
|
||||||
|
('|.*Enabled.*|.*True.*|.*Event.*|.*ACCEPT.*|.*Name.*|.*{}.*'
|
||||||
|
'|.*Resource.*|.*None.*|.*Target').format(log_name_2),
|
||||||
|
# 6) both premade log names in output
|
||||||
|
'True.*|.*{}.*|.*True.*|.*{}'.format(log_name_1, log_name_2),
|
||||||
|
# 7) output not expected
|
||||||
|
'']
|
||||||
|
# NOTE(mblue): devstack: Extensions list not supported by Identity API
|
||||||
|
if WB_CONF.openstack_type == 'devstack':
|
||||||
|
cmds.pop(0)
|
||||||
|
stdout_patterns.pop(0)
|
||||||
|
for cmd, ptn in zip(cmds, stdout_patterns):
|
||||||
|
self.validate_command('bash -c "' + prefix + cmd + '"', ptn)
|
||||||
|
|
||||||
|
def _test_only_dropped_traffic_logged(self):
|
||||||
|
"""This scenario verifies that only the log entries of dropped traffic
|
||||||
|
exist when only the "drop" event is supposed to be logged
|
||||||
|
('--event DROP' option).
|
||||||
|
Traffic of "accept" events is generated but not supposed to be logged
|
||||||
|
(Steps 1 - 3 and 8 - 12).
|
||||||
|
|
||||||
|
This scenario also verifies rate and burst limit configuration changes
|
||||||
|
properly in overcloud nodes
|
||||||
|
(Steps 4 - 7).
|
||||||
|
"""
|
||||||
|
vm_a = self._create_server()
|
||||||
|
# 1) enable logging only for "drop" events
|
||||||
|
self._create_log(event='DROP')
|
||||||
|
# 2) attempt ssh to VM from undercloud ("drop" event),
|
||||||
|
# this ensures an initial log entry exists, with needed count value.
|
||||||
|
self.try_ssh_traffic(vm_a['ssh_client'])
|
||||||
|
# 3) tracks count value of last log entry, from hypervisor,
|
||||||
|
# before sending test traffic to be logged.
|
||||||
|
self.start_track_log(vm_a['hv_ssh_client'])
|
||||||
|
# 4) configure higher rate and burst limits on all controllers
|
||||||
|
# default preset (min possible): rate limit 100, burst limit 25
|
||||||
|
burst_val = random.randint(30, 150)
|
||||||
|
rate_val = random.randint(120, 300)
|
||||||
|
self.set_service_setting(file=self.ML2_CONF_FILE,
|
||||||
|
section='network_log',
|
||||||
|
param='burst_limit',
|
||||||
|
value=burst_val)
|
||||||
|
self.set_service_setting(file=self.ML2_CONF_FILE,
|
||||||
|
section='network_log',
|
||||||
|
param='rate_limit',
|
||||||
|
value=rate_val)
|
||||||
|
# 5) restart neutron api on all controllers simultaneously
|
||||||
|
if not WB_CONF.openstack_type == 'podified':
|
||||||
|
if WB_CONF.openstack_type == 'devstack':
|
||||||
|
service_ptn = 'q svc'
|
||||||
|
else:
|
||||||
|
service_ptn = 'neutron api'
|
||||||
|
for node in self.nodes:
|
||||||
|
if node['is_controller']:
|
||||||
|
# NOTE(mblue): if reset fails on multinode, consider
|
||||||
|
# wait_until_active=False for a more simultaneous reset
|
||||||
|
self.reset_node_service(service_ptn, node['client'])
|
||||||
|
# 6) verify rate/burst limits set on master/leader controller meter
|
||||||
|
wait_err = ('Timed out: burst/rate limits not configured properly '
|
||||||
|
'on master/leader controller.')
|
||||||
|
common_utils.wait_until_true(
|
||||||
|
lambda: self.validate_command(
|
||||||
|
'{} list meter-band'.format(self.nbctl),
|
||||||
|
pattern=r'burst_size[ \t]+:[ \t]+{}'.format(burst_val),
|
||||||
|
ssh_client=self.master_node_client,
|
||||||
|
ret_bool_pattern=True),
|
||||||
|
timeout=180,
|
||||||
|
sleep=10,
|
||||||
|
exception=RuntimeError(wait_err))
|
||||||
|
# extra command call needed due to regex limitation
|
||||||
|
self.validate_command(
|
||||||
|
'{} list meter-band'.format(self.nbctl),
|
||||||
|
pattern=r'rate[ \t]+:[ \t]+{}'.format(rate_val),
|
||||||
|
ssh_client=self.master_node_client)
|
||||||
|
# 7) verify openvswitch meter configuration on compute of VM
|
||||||
|
self.validate_command(
|
||||||
|
'sudo ovs-ofctl dump-meters br-int -O OpenFlow15',
|
||||||
|
pattern='type=drop rate={} burst_size={}'.format(
|
||||||
|
rate_val, burst_val),
|
||||||
|
ssh_client=vm_a['hv_ssh_client'])
|
||||||
|
# 8) ping VM from undercloud (supposed to pass but not be logged)
|
||||||
|
self.ping_ip_address(vm_a['fip'])
|
||||||
|
# 9) ssh VM from undercloud (supposed to drop traffic and be logged)
|
||||||
|
self.try_ssh_traffic(vm_a['ssh_client'])
|
||||||
|
# 10) get last log entry count value, and relevant log entries
|
||||||
|
# from compute, after sending traffic to be logged
|
||||||
|
self.retrieve_tracked_log(vm_a['hv_ssh_client'])
|
||||||
|
# 11) verify ICMP isn't logged at all (allowed "accept" event traffic)
|
||||||
|
self.check_log_icmp(
|
||||||
|
should_log=False, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
# 12) verify ssh attempts are logged (disallowed "drop" event traffic)
|
||||||
|
self.check_log_ssh(
|
||||||
|
should_log=True, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
|
||||||
|
def _test_only_accepted_traffic_logged(self):
|
||||||
|
"""This test verifies that only the log entries of allowed traffic
|
||||||
|
exist when only the "allow" event is supposed to be logged
|
||||||
|
(‘--event ACCEPT’ option).
|
||||||
|
Traffic of "drop" events is generated but not supposed to be logged.
|
||||||
|
(Steps 1 - 8)
|
||||||
|
|
||||||
|
This scenario also verifies that both directions of traffic are logged
|
||||||
|
by same SGR, checking ICMP echo request and reply in logs.
|
||||||
|
(Step 7)
|
||||||
|
|
||||||
|
This scenario also verifies that when adding SGRs ingress
|
||||||
|
for dns and ssh traffic, single meter and meter band exist in NB,
|
||||||
|
and all SGRs/ACLs are linked to same meter in NB.
|
||||||
|
(Step 9 - 12)
|
||||||
|
|
||||||
|
This scenario also verifies configured rate and burst limits
|
||||||
|
are enforced properly, when excessive traffic amount matches.
|
||||||
|
(Steps 13 - 17).
|
||||||
|
|
||||||
|
The scenario verifies no duplicate entries on same/different computes.
|
||||||
|
Test runs twice, once 2 VMs on same compute,
|
||||||
|
once on different computes, same network and subnet used for VMs.
|
||||||
|
|
||||||
|
Test sends a constant amount of ICMP requests from the first VM to
|
||||||
|
the second VM locally in same subnet.
|
||||||
|
|
||||||
|
Afterwards verifies a constant amount of log entries was written
|
||||||
|
for the ICMP traffic, in same/different compute, checking there
|
||||||
|
are no duplicated log entries.
|
||||||
|
|
||||||
|
Duplicate entries checked using from-lport/to-lport and request/reply,
|
||||||
|
each of 4 possible options combination should have constant amount
|
||||||
|
(Only in different computes test, amount is split in specific way
|
||||||
|
described in later comments of test code).
|
||||||
|
(Step 18 - 20)
|
||||||
|
"""
|
||||||
|
vm_a = self._create_server()
|
||||||
|
# 1) enable logging only for "accept" events
|
||||||
|
self._create_log(event='ACCEPT')
|
||||||
|
# 2) ping VM from undercloud ("accept" event),
|
||||||
|
# this ensures an initial log entry exists, with needed count value.
|
||||||
|
self.ping_ip_address(vm_a['fip'])
|
||||||
|
# 3) tracks count value of last log entry, from hypervisor,
|
||||||
|
# before sending test traffic to be logged.
|
||||||
|
self.start_track_log(vm_a['hv_ssh_client'])
|
||||||
|
# 4) ping VM from undercloud (supposed to pass and be logged)
|
||||||
|
self.ping_ip_address(vm_a['fip'])
|
||||||
|
# 5) ssh VM from undercloud (supposed to drop traffic, not be logged)
|
||||||
|
self.try_ssh_traffic(vm_a['ssh_client'])
|
||||||
|
# 6) get last log entry count value, and relevant log entries
|
||||||
|
# from compute, after sending traffic to be logged
|
||||||
|
self.retrieve_tracked_log(vm_a['hv_ssh_client'])
|
||||||
|
# 7) verify ICMP is logged in both directions ("accept" traffic)
|
||||||
|
self.check_log_icmp(
|
||||||
|
should_log=True, hypervisor_ssh=vm_a['hv_ssh_client'],
|
||||||
|
both_directions=True)
|
||||||
|
# 8) verify ssh attempts aren't logged ("drop" event traffic)
|
||||||
|
self.check_log_ssh(
|
||||||
|
should_log=False, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
# 9) add temporary SGR allowing ingress tcp port 22 (ssh traffic)
|
||||||
|
ssh_sgr_in = self.create_security_group_rule(
|
||||||
|
security_group_id=self.secgrp['id'],
|
||||||
|
protocol='tcp',
|
||||||
|
direction='ingress',
|
||||||
|
port_range_min=22,
|
||||||
|
port_range_max=22)
|
||||||
|
self.addCleanup(
|
||||||
|
self.admin_client.delete_security_group_rule, ssh_sgr_in['id'])
|
||||||
|
# 10) add temporary SGR allowing ingress udp port 53 (dns traffic)
|
||||||
|
dns_sgr = self.create_security_group_rule(
|
||||||
|
security_group_id=self.secgrp['id'],
|
||||||
|
protocol='udp',
|
||||||
|
direction='ingress',
|
||||||
|
port_range_min=53,
|
||||||
|
port_range_max=53)
|
||||||
|
self.addCleanup(
|
||||||
|
self.admin_client.delete_security_group_rule, dns_sgr['id'])
|
||||||
|
# fetch updated ingress SGRs in logging SG
|
||||||
|
sg_rules = self.admin_client.show_security_group(
|
||||||
|
self.secgrp['id'])['security_group']['security_group_rules']
|
||||||
|
sg_rules_ingress = [rule for rule in sg_rules
|
||||||
|
if rule['direction'] == 'ingress']
|
||||||
|
# 11) verify single meter and meter band quantity in NB
|
||||||
|
self.verify_meter_and_band_amounts((1, 2), (1, 2))
|
||||||
|
# 12) verify same logging SGRs/ACLs amount linked to relevant meter
|
||||||
|
# NOTE(mblue): devstack ACLs for stateless SG with another meter name
|
||||||
|
if WB_CONF.openstack_type == 'devstack' and \
|
||||||
|
not self.is_secgrp_stateful:
|
||||||
|
meter_postfix = '.log_stateless"'
|
||||||
|
else:
|
||||||
|
meter_postfix = '.log"'
|
||||||
|
log_acls_output = self.master_node_client.exec_command(
|
||||||
|
self.nbctl + ' find acl log=true | grep -E '
|
||||||
|
"'meter.*:.*(log_meter|test_log{})'".format(meter_postfix))
|
||||||
|
# needed SGR amount may change due to stateful/stateless setting
|
||||||
|
self.assertEqual(len(sg_rules_ingress),
|
||||||
|
len(log_acls_output.splitlines()),
|
||||||
|
"Not all logging SGRs/ACLs linked to relevant meter")
|
||||||
|
# 13) fetch current configured rate and burst limits
|
||||||
|
burst_limit = int(self.validate_command(
|
||||||
|
'sudo crudini --get {} network_log burst_limit'.format(
|
||||||
|
self.ML2_CONF_FILE),
|
||||||
|
ssh_client=self.master_node_client))
|
||||||
|
rate_limit = int(self.validate_command(
|
||||||
|
'sudo crudini --get {} network_log rate_limit'.format(
|
||||||
|
self.ML2_CONF_FILE),
|
||||||
|
ssh_client=self.master_node_client))
|
||||||
|
# 14) send excessive ICMP requests to VM for 1 second
|
||||||
|
self.start_track_log(vm_a['hv_ssh_client'])
|
||||||
|
self.validate_command(
|
||||||
|
'sudo ping {} -i 0.002 -c 500 | tail -n4'.format(vm_a['fip']),
|
||||||
|
pattern=r' 0% packet loss')
|
||||||
|
self.retrieve_tracked_log(vm_a['hv_ssh_client'])
|
||||||
|
# 15) verify log entries amount equals to:
|
||||||
|
# rate limit + burst limit (up to 10% offset allowed)
|
||||||
|
limits_fail_msg = "rate or burst log limits not enforced correctly"
|
||||||
|
quick_test_target = burst_limit + rate_limit
|
||||||
|
quick_test_logs_amount = self._hypervisors_counts[
|
||||||
|
vm_a['hv_ssh_client'].host]['test_logs_amount']
|
||||||
|
self.assertIn(quick_test_logs_amount,
|
||||||
|
range(round(0.9 * quick_test_target),
|
||||||
|
round(1.1 * quick_test_target)),
|
||||||
|
limits_fail_msg)
|
||||||
|
# 16) send excessive ICMP requests to VM for 60 seconds
|
||||||
|
self.start_track_log(vm_a['hv_ssh_client'])
|
||||||
|
self.validate_command(
|
||||||
|
'sudo ping {} -i 0.005 -c 12000 | tail -n4'.format(vm_a['fip']),
|
||||||
|
pattern=r' 0% packet loss',
|
||||||
|
timeout=70)
|
||||||
|
self.retrieve_tracked_log(vm_a['hv_ssh_client'])
|
||||||
|
# 17) verify log entries amount equals to:
|
||||||
|
# rate limit * 60 + burst limit (up to 2% offset allowed)
|
||||||
|
long_test_target = rate_limit * 60 + burst_limit
|
||||||
|
long_test_logs_amount = self._hypervisors_counts[
|
||||||
|
vm_a['hv_ssh_client'].host]['test_logs_amount']
|
||||||
|
self.assertIn(long_test_logs_amount,
|
||||||
|
range(round(0.98 * long_test_target),
|
||||||
|
round(1.02 * long_test_target)),
|
||||||
|
limits_fail_msg)
|
||||||
|
|
||||||
|
# NOTE(mblue): skip multi-compute test when setup is single node
|
||||||
|
for vms_same_compute in (True, False):
|
||||||
|
if self.is_setup_single_node() and not vms_same_compute:
|
||||||
|
continue
|
||||||
|
self._verify_duplicate_entries(vms_same_compute, vm_a)
|
||||||
|
|
||||||
|
def _verify_duplicate_entries(self, same_compute, vm_a):
|
||||||
|
# 18) create second VM on the same/different compute node
|
||||||
|
compute_hint_str = 'same' if same_compute else 'different'
|
||||||
|
vm_b = self._create_server(
|
||||||
|
scheduler_hints={'{}_host'.format(compute_hint_str): vm_a['id']})
|
||||||
|
vm_b_internal_ip = list(vm_b['addresses'].values())[0][0]['addr']
|
||||||
|
time.sleep(5)
|
||||||
|
# 19) track logs during ping, 10 times from VM 1 to VM 2
|
||||||
|
# (log file located on compute)
|
||||||
|
self.start_track_log(vm_a['hv_ssh_client'])
|
||||||
|
if not same_compute:
|
||||||
|
self.start_track_log(vm_b['hv_ssh_client'])
|
||||||
|
ping_amount = 10
|
||||||
|
self.check_remote_connectivity(
|
||||||
|
vm_a['ssh_client'], vm_b_internal_ip, ping_count=ping_amount)
|
||||||
|
self.retrieve_tracked_log(vm_a['hv_ssh_client'])
|
||||||
|
if not same_compute:
|
||||||
|
self.retrieve_tracked_log(vm_b['hv_ssh_client'])
|
||||||
|
# 20) verify each possible direction and type logged as ping amount
|
||||||
|
icmp_types = ('0', '8') # reply/request
|
||||||
|
directions = ('to-lport', 'from-lport') # to/from logical switch
|
||||||
|
checked_options = [(d, t) for d in directions for t in icmp_types]
|
||||||
|
entries_list = [self._hypervisors_counts[
|
||||||
|
vm_a['hv_ssh_client'].host]['tested_logs'].splitlines()]
|
||||||
|
if not same_compute:
|
||||||
|
entries_list.append(self._hypervisors_counts[
|
||||||
|
vm_b['hv_ssh_client'].host]['tested_logs'].splitlines())
|
||||||
|
err_msg = (
|
||||||
|
'\nlog entries amount of ICMP type "{}", direction "{}", '
|
||||||
|
'not according to ping amount, which is {}.\n'
|
||||||
|
'Notice code comments, tempest.log for further debugging.\n'
|
||||||
|
'These are the log_acl entries captured while ping from '
|
||||||
|
'VM 1 to VM 2:\n'
|
||||||
|
'\n\n{}\n\n')
|
||||||
|
for i, entries in enumerate(entries_list):
|
||||||
|
# different computes case: each compute holds half logs like so:
|
||||||
|
# VM A's compute -> request with to-lport, reply with from-lport.
|
||||||
|
# (compute of client VM)
|
||||||
|
# VM B's compute -> request with from-lport, reply with to-lport.
|
||||||
|
# (compute of server VM)
|
||||||
|
if not same_compute:
|
||||||
|
checked_options_filt = \
|
||||||
|
checked_options[::3] if i == 0 else checked_options[1:3]
|
||||||
|
else:
|
||||||
|
checked_options_filt = checked_options
|
||||||
|
for direction, icmp_type in checked_options_filt:
|
||||||
|
entries_with_opts_count = len([
|
||||||
|
ent for ent in entries if
|
||||||
|
'direction={}'.format(direction) in ent and
|
||||||
|
'icmp_type={}'.format(icmp_type) in ent])
|
||||||
|
LOG.debug(
|
||||||
|
('same_computes=%s, hv=%d, direction=%s, icmp_type=%s '
|
||||||
|
'-> entries count %d'),
|
||||||
|
str(same_compute), i, direction, icmp_type,
|
||||||
|
entries_with_opts_count)
|
||||||
|
self.assertEqual(
|
||||||
|
entries_with_opts_count,
|
||||||
|
ping_amount,
|
||||||
|
err_msg.format(
|
||||||
|
icmp_type, direction, ping_amount, pformat(entries)))
|
||||||
|
|
||||||
|
def _test_dropped_and_accepted_traffic_logged(self):
|
||||||
|
"""This scenario verifies that the log entries of both dropped and
|
||||||
|
accepted events exist when both "drop" and "accept" events are
|
||||||
|
supposed to be logged (‘--event ALL’ option).
|
||||||
|
|
||||||
|
This scenario also verifies indications of a few protocols are logged:
|
||||||
|
using TCP, UDP, SSH and ICMP traffic
|
||||||
|
(Steps 1 - 11).
|
||||||
|
|
||||||
|
This scenario also verifies log file rotation done when max size
|
||||||
|
configured per log file reached
|
||||||
|
(Steps 12 - 16).
|
||||||
|
|
||||||
|
This scenario verifies allowed and unallowed traffic (ICMP and SSH/UDP)
|
||||||
|
aren't logged, after log disabled, also after log deleted.
|
||||||
|
(Steps 18 - 23).
|
||||||
|
|
||||||
|
Northbound DB checks used to verify ACL log value enabled/disabled,
|
||||||
|
and meter, meter band amounts when log exists and removed.
|
||||||
|
(17, 19, 24).
|
||||||
|
"""
|
||||||
|
vm_a = self._create_server()
|
||||||
|
if WB_CONF.openstack_type == 'devstack':
|
||||||
|
rotate_prefix = 'sudo '
|
||||||
|
else:
|
||||||
|
rotate_prefix = 'sudo podman exec logrotate_crond '
|
||||||
|
# 1) enable logging for "all" events
|
||||||
|
local_log = self._create_log(event='ALL', add_cleanup=False)
|
||||||
|
# 2) attempt ssh to VM from undercloud ("drop" event),
|
||||||
|
# this ensures an initial log entry exists, with needed count value.
|
||||||
|
self.try_ssh_traffic(vm_a['ssh_client'])
|
||||||
|
# 3) tracks count value of last log entry, from hypervisor,
|
||||||
|
# before sending test traffic to be logged.
|
||||||
|
self.start_track_log(vm_a['hv_ssh_client'])
|
||||||
|
# 4) ping VM from undercloud (supposed to pass and be logged)
|
||||||
|
self.ping_ip_address(vm_a['fip'])
|
||||||
|
# 5) ssh VM from undercloud (supposed to drop traffic and be logged)
|
||||||
|
self.try_ssh_traffic(vm_a['ssh_client'])
|
||||||
|
# 6) send dropped TCP and UDP traffic port 9999
|
||||||
|
self.try_tcp_traffic(vm_a['fip'])
|
||||||
|
self.try_udp_traffic(vm_a['fip'])
|
||||||
|
# 7) get last log entry count value, and relevant log entries
|
||||||
|
# from compute, after sending traffic to be logged
|
||||||
|
self.retrieve_tracked_log(vm_a['hv_ssh_client'])
|
||||||
|
# 8) verify ICMP is logged (allowed "accept" event traffic)
|
||||||
|
self.check_log_icmp(
|
||||||
|
should_log=True, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
# 9) verify TCP traffic with port 9999 is indicated in logs
|
||||||
|
self.check_log_tcp(
|
||||||
|
should_log=True, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
# 10) verify UDP traffic with port 9999 is indicated in logs
|
||||||
|
self.check_log_udp(
|
||||||
|
should_log=True, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
# 11) verify ssh attempts are logged (disallowed "drop" event traffic)
|
||||||
|
self.check_log_ssh(
|
||||||
|
should_log=True, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
# 12) fetch 'maxsize' value which configures log rotation threshold
|
||||||
|
maxsize_si = self.validate_command(
|
||||||
|
rotate_prefix + 'grep maxsize /etc/logrotate{}.conf; true'.format(
|
||||||
|
self.rotate_service_fix),
|
||||||
|
ssh_client=vm_a['hv_ssh_client']).rstrip().split(' ')[-1]
|
||||||
|
# if setup type supports log rotation due to size, then test
|
||||||
|
if maxsize_si:
|
||||||
|
# convert to bytes without SI prefixes k/M/G (logrotate SI options)
|
||||||
|
try:
|
||||||
|
power = 10 * ('kMG'.index(maxsize_si[-1]) + 1)
|
||||||
|
maxsize_num = maxsize_si[:-1]
|
||||||
|
maxsize = int(maxsize_num) * 2 ** power
|
||||||
|
# no SI symbol
|
||||||
|
except ValueError:
|
||||||
|
maxsize = int(maxsize_si)
|
||||||
|
overflow_size = maxsize + 1
|
||||||
|
# 13) force check if log rotation needed (before test start)
|
||||||
|
self.validate_command(
|
||||||
|
rotate_prefix + self.ROTATION_CHECK_CMD,
|
||||||
|
ssh_client=vm_a['hv_ssh_client'])
|
||||||
|
# current active log file size in bytes (before size overflow)
|
||||||
|
active_log_pre_overflow = int(self.validate_command(
|
||||||
|
'sudo wc -c {}'.format(self.SG_LOG_FILE),
|
||||||
|
ssh_client=vm_a['hv_ssh_client']).split()[0])
|
||||||
|
# 14) adding bytes into active log file to pass 'maxsize' value
|
||||||
|
self.validate_command(('''python3 -c "print('A'*{})" | '''
|
||||||
|
'sudo tee -a {} 1> /dev/null').format(
|
||||||
|
overflow_size, self.SG_LOG_FILE),
|
||||||
|
ssh_client=vm_a['hv_ssh_client'])
|
||||||
|
# 15) force check if log rotation is needed
|
||||||
|
self.validate_command(
|
||||||
|
rotate_prefix + self.ROTATION_CHECK_CMD,
|
||||||
|
ssh_client=vm_a['hv_ssh_client'])
|
||||||
|
# current active log file size in bytes (after size overflow)
|
||||||
|
active_log_post_overflow = int(self.validate_command(
|
||||||
|
'sudo wc -c {}'.format(self.SG_LOG_FILE),
|
||||||
|
ssh_client=vm_a['hv_ssh_client']).split()[0])
|
||||||
|
# new rotated log file size in bytes (after size overflow)
|
||||||
|
rotated_log_post_overflow = int(self.validate_command(
|
||||||
|
'sudo wc -c {}'.format(self.SG_LOG_FILE + '.1'),
|
||||||
|
ssh_client=vm_a['hv_ssh_client']).split()[0])
|
||||||
|
# 16) verify log rotation succesful:
|
||||||
|
# - current new active log has a small size (below 'offset').
|
||||||
|
# - current rotated log similar to previous active log size
|
||||||
|
# (up until additional 'offset' bytes of logs allowed).
|
||||||
|
offset = 1500
|
||||||
|
self.assertLess(active_log_post_overflow, offset,
|
||||||
|
"Active log after rotation too big")
|
||||||
|
self.assertIn(
|
||||||
|
rotated_log_post_overflow,
|
||||||
|
range(active_log_pre_overflow + overflow_size,
|
||||||
|
active_log_pre_overflow + overflow_size + offset),
|
||||||
|
"Rotated log size much different than expected")
|
||||||
|
# this ensures an initial DROP log entry exists (after rotation)
|
||||||
|
self.try_ssh_traffic(vm_a['ssh_client'])
|
||||||
|
# 17) verify single meter and meter band in NBDB
|
||||||
|
self.verify_meter_and_band_amounts((1, 2), (1, 2))
|
||||||
|
# 18) test no logging done, after log disabled, and after log deleted
|
||||||
|
icmp_acl_cmd = self.nbctl + \
|
||||||
|
(r' find acl "external_ids:\"neutron:security_group_rule_id\""='
|
||||||
|
'"{}"'.format(self.icmp_rule['id']))
|
||||||
|
for _request_op in range(2):
|
||||||
|
if _request_op == 0:
|
||||||
|
# 19) verify log value of ACL in NB, before and after disable
|
||||||
|
icmp_acl_output = self.master_node_client.exec_command(
|
||||||
|
icmp_acl_cmd)
|
||||||
|
self.assertRegex(
|
||||||
|
icmp_acl_output, r'log[ \t]*:[ \t]*true',
|
||||||
|
'ACL log value in NB not true, when SGR enabled')
|
||||||
|
self.admin_client.update_log(local_log['id'], enabled=False)
|
||||||
|
icmp_acl_output = self.master_node_client.exec_command(
|
||||||
|
icmp_acl_cmd)
|
||||||
|
self.assertRegex(
|
||||||
|
icmp_acl_output, r'log[ \t]*:[ \t]*false',
|
||||||
|
'ACL log value in NB not false, when SGR disabled')
|
||||||
|
else:
|
||||||
|
self.admin_client.delete_log(local_log['id'])
|
||||||
|
# 20) track only logging entries after disable/deletion
|
||||||
|
self.start_track_log(vm_a['hv_ssh_client'])
|
||||||
|
# 21) send disallowed SSH/UDP traffic, and allowed ICMP traffic
|
||||||
|
self.try_ssh_traffic(vm_a['ssh_client'])
|
||||||
|
self.try_udp_traffic(vm_a['fip'])
|
||||||
|
self.ping_ip_address(vm_a['fip'])
|
||||||
|
# 22) stop tracking logs
|
||||||
|
self.retrieve_tracked_log(vm_a['hv_ssh_client'])
|
||||||
|
# 23) verify no SSH/UDP/ICMP traffic logged
|
||||||
|
self.check_log_ssh(
|
||||||
|
should_log=False, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
self.check_log_udp(
|
||||||
|
should_log=False, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
self.check_log_icmp(
|
||||||
|
should_log=False, hypervisor_ssh=vm_a['hv_ssh_client'])
|
||||||
|
# 24) verify meter / meter band don't exist in NB after log deletion
|
||||||
|
self.verify_meter_and_band_amounts((0,), (0,))
|
||||||
|
|
||||||
|
|
||||||
|
class StatefulSecGroupLoggingTest(BaseSecGroupLoggingTest):
|
||||||
|
is_secgrp_stateful = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resource_setup(cls):
|
||||||
|
super(StatefulSecGroupLoggingTest, cls).resource_setup()
|
||||||
|
cls._common_resource_setup()
|
||||||
|
|
||||||
|
@decorators.idempotent_id('ad5f67e0-48d2-4af5-8bc2-b0a8973d4fba')
|
||||||
|
def test_log_commands(self):
|
||||||
|
self._test_log_commands()
|
||||||
|
|
||||||
|
@decorators.idempotent_id('1cb767a8-74c3-42a4-a705-c9f9622f59c9')
|
||||||
|
def test_only_dropped_traffic_logged(self):
|
||||||
|
self._test_only_dropped_traffic_logged()
|
||||||
|
|
||||||
|
@decorators.idempotent_id('d1b4cea8-e2cb-4d90-a135-ea41a5fb212a')
|
||||||
|
def test_only_accepted_traffic_logged(self):
|
||||||
|
self._test_only_accepted_traffic_logged()
|
||||||
|
|
||||||
|
@decorators.idempotent_id('003f7f7c-f8b8-4bfe-a24a-12e1c2a14cb9')
|
||||||
|
def test_dropped_and_accepted_traffic_logged(self):
|
||||||
|
self._test_dropped_and_accepted_traffic_logged()
|
||||||
|
|
||||||
|
|
||||||
|
class StatelessSecGroupLoggingTest(BaseSecGroupLoggingTest):
|
||||||
|
required_extensions = BaseSecGroupLoggingTest.required_extensions + \
|
||||||
|
['stateful-security-group']
|
||||||
|
is_secgrp_stateful = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def resource_setup(cls):
|
||||||
|
super(StatelessSecGroupLoggingTest, cls).resource_setup()
|
||||||
|
cls._common_resource_setup()
|
||||||
|
|
||||||
|
@decorators.idempotent_id('65319597-a504-4f29-947b-ae57f4ac351c')
|
||||||
|
def test_log_commands(self):
|
||||||
|
self._test_log_commands()
|
||||||
|
|
||||||
|
@decorators.idempotent_id('7bab2764-2aac-49da-bd0f-161128695bfb')
|
||||||
|
def test_only_dropped_traffic_logged(self):
|
||||||
|
self._test_only_dropped_traffic_logged()
|
||||||
|
|
||||||
|
@decorators.idempotent_id('2efc5a0c-859c-4a35-b658-52d323c46fef')
|
||||||
|
def test_only_accepted_traffic_logged(self):
|
||||||
|
self._test_only_accepted_traffic_logged()
|
||||||
|
|
||||||
|
@decorators.idempotent_id('e26ccbf1-4b4a-4812-9e6f-9b59cf196540')
|
||||||
|
def test_dropped_and_accepted_traffic_logged(self):
|
||||||
|
self._test_dropped_and_accepted_traffic_logged()
|
|
@ -101,6 +101,7 @@
|
||||||
- l3-ha
|
- l3-ha
|
||||||
- l3-ndp-proxy
|
- l3-ndp-proxy
|
||||||
- l3_agent_scheduler
|
- l3_agent_scheduler
|
||||||
|
- logging
|
||||||
- metering
|
- metering
|
||||||
- multi-provider
|
- multi-provider
|
||||||
- net-mtu
|
- net-mtu
|
||||||
|
|
Loading…
Reference in New Issue