diff --git a/tobiko/openstack/neutron/__init__.py b/tobiko/openstack/neutron/__init__.py index 8f4b19bc1..dba2e872e 100644 --- a/tobiko/openstack/neutron/__init__.py +++ b/tobiko/openstack/neutron/__init__.py @@ -28,6 +28,8 @@ OVN_CONTROLLER = _agent.OVN_CONTROLLER OVN_METADATA_AGENT = _agent.OVN_METADATA_AGENT AgentNotFoundOnHost = _agent.AgentNotFoundOnHost skip_if_missing_networking_agents = _agent.skip_if_missing_networking_agents +skip_unless_is_ovn = _agent.skip_unless_is_ovn +skip_unless_is_ovs = _agent.skip_unless_is_ovs list_networking_agents = _agent.list_networking_agents diff --git a/tobiko/openstack/neutron/_agent.py b/tobiko/openstack/neutron/_agent.py index 3e3f0fc3d..dfca35a05 100644 --- a/tobiko/openstack/neutron/_agent.py +++ b/tobiko/openstack/neutron/_agent.py @@ -69,3 +69,13 @@ def skip_if_missing_networking_agents(binary: typing.Optional[str] = None, ', '.join("{!s}={!r}".format(k, v) for k, v in params.items())) return tobiko.skip_if(message, missing_networking_agents, count=count, **params) + + +def skip_unless_is_ovn(): + '''Skip the test if ovn_controller agent does not exist''' + return skip_if_missing_networking_agents(OVN_CONTROLLER) + + +def skip_unless_is_ovs(): + '''Skip the test if openvswitch agent does not exist''' + return skip_if_missing_networking_agents(OPENVSWITCH_AGENT) diff --git a/tobiko/shell/files/__init__.py b/tobiko/shell/files/__init__.py new file mode 100644 index 000000000..2d08d8bde --- /dev/null +++ b/tobiko/shell/files/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) 2020 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 __future__ import absolute_import + +from tobiko.shell.files import _exception +from tobiko.shell.files import _logs + + +LogParserError = _exception.LogParserError +LogFileNotFound = _exception.LogFileNotFound + +LogFile = _logs.LogFile +ClusterLogFile = _logs.ClusterLogFile diff --git a/tobiko/shell/files/_exception.py b/tobiko/shell/files/_exception.py new file mode 100644 index 000000000..a931e05e4 --- /dev/null +++ b/tobiko/shell/files/_exception.py @@ -0,0 +1,26 @@ +# Copyright (c) 2020 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 __future__ import absolute_import + +import tobiko + + +class LogParserError(tobiko.TobikoException): + pass + + +class LogFileNotFound(LogParserError): + message = 'File {filename} was not found on {host}' diff --git a/tobiko/shell/files/_logs.py b/tobiko/shell/files/_logs.py new file mode 100644 index 000000000..7d719ad19 --- /dev/null +++ b/tobiko/shell/files/_logs.py @@ -0,0 +1,110 @@ +# Copyright (c) 2020 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 __future__ import absolute_import + +import os + +from tobiko.shell import files +from tobiko.shell import sh +from tobiko.openstack import topology + + +class LogFile(object): + + def __init__(self, hostname, filename): + self.filename = filename + self.host = topology.get_openstack_node(hostname=hostname) + self._list_logfiles() + self.cmd = '' + self.found = [] + + def find(self, regex): + self._list_logfiles() + self.cmd = f"zgrep -Eh {regex}" + self.found = sh.execute(f'{self.cmd} {" ".join(self.logfiles)}', + ssh_client=self.host.ssh_client, + expect_exit_status=None, + sudo=True).stdout.split('\n') + try: + self.found.remove('') + except ValueError: + pass + return self.found + + def find_new(self): + self._list_logfiles() + if not self.cmd: + err_msg = 'find_new() method can be only executed after find()' + raise files.LogParserError(message=err_msg) + tmp = sh.execute(f'{self.cmd} {" ".join(self.logfiles)}', + ssh_client=self.host.ssh_client, + expect_exit_status=None, + sudo=True).stdout.split('\n') + found = [] + for log_string in tmp: + if log_string not in self.found and log_string != '': + found.append(log_string) + self.found.append(log_string) + return found + + def _list_logfiles(self): + file_path, file_name = os.path.split(self.filename) + result = sh.execute(f'find {file_path} -name {file_name}*', + ssh_client=self.host.ssh_client, + expect_exit_status=None, + sudo=True) + self.logfiles = set(result.stdout.split('\n')) + if '' in self.logfiles: + self.logfiles.remove('') + if self.logfiles == []: + raise files.LogFileNotFound(filename=str(self.filename), + host=str(self.host.name)) + + +class ClusterLogFile(object): + + def __init__(self, filename): + self.filename = filename + self.hostnames = [] + self.logfiles = [] + + def add_host(self, hostname): + if hostname in self.hostnames: + return + self.hostnames.append(hostname) + self.logfiles.append(LogFile(hostname, self.filename)) + + def add_group(self, group): + for host in topology.list_openstack_nodes(group=group): + self.add_host(host.name) + + def find(self, regex): + for logfile in self.logfiles: + logfile.find(regex) + return self.found + + def find_new(self): + new_lines = [] + for logfile in self.logfiles: + new_lines += logfile.find_new() + return new_lines + + @property + def found(self): + found = [] + for logfile in self.logfiles: + found += logfile.found + return found diff --git a/tobiko/shell/files/config.py b/tobiko/shell/files/config.py new file mode 100644 index 000000000..c7c83529a --- /dev/null +++ b/tobiko/shell/files/config.py @@ -0,0 +1,16 @@ +# Copyright (c) 2020 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 __future__ import absolute_import diff --git a/tobiko/tests/scenario/neutron/test_port.py b/tobiko/tests/scenario/neutron/test_port.py index b931be9de..58ebb20e4 100644 --- a/tobiko/tests/scenario/neutron/test_port.py +++ b/tobiko/tests/scenario/neutron/test_port.py @@ -18,9 +18,11 @@ import netaddr import testtools import tobiko +from tobiko.shell import files from tobiko.shell import ping from tobiko.shell import ip from tobiko.openstack import neutron +from tobiko.openstack import nova from tobiko.openstack import stacks @@ -98,3 +100,32 @@ class CentosServerL3HAPortTestWith(PortTest): class UbuntuServerL3HAPortTestWith(PortTest): #: Resources stack with floating IP and Nova server stack = tobiko.required_setup_fixture(stacks.L3haUbuntuServerStackFixture) + + +class PortLogsStack(stacks.CirrosServerStackFixture): + pass + + +@neutron.skip_unless_is_ovs() +class PortLogs(testtools.TestCase): + + stack = tobiko.required_setup_fixture(PortLogsStack) + + def test_nova_port_notification(self): + expected_logfile = '/var/log/containers/neutron/server.log' + logfile = files.ClusterLogFile(expected_logfile) + try: + logfile.add_group('controller') + except files.LogFileNotFound as ex: + tobiko.skip(str(ex)) + logfile.find(f'Nova.+event.+response.*{self.stack.server_id}') + nova.shutoff_server(self.stack.server_id) + nova.activate_server(self.stack.server_id) + new_events = logfile.find_new() + self.assertEqual(len(new_events), 2) + self.assertTrue( + any('network-vif-unplugged' in event for event in new_events)) + self.assertTrue( + any('network-vif-plugged' in event for event in new_events)) + self.assertTrue( + all(self.stack.port_id in event for event in new_events))