From 3c873d1bbe2cf2511956b52657de5ea3a5dc33fa Mon Sep 17 00:00:00 2001 From: Maor Blaustein Date: Tue, 19 Mar 2024 12:54:42 +0200 Subject: [PATCH] Migrate internal dns tests Changes from original tests: - Adjust migrated imports and configuration. - Adjusted 'BaseTempestTestCaseAdvanced' class. - Added node_power_change option. - Spared 'future' module dependency. - Adjusted find_node_client method. - Adjusted compute ovn-controller service restart for podified/devstack/other. - Replaced test UUIDs with unique ones. Change-Id: I24e0b1dd71f49d484ef8d33da470b8df4b12d2e7 --- whitebox_neutron_tempest_plugin/config.py | 4 + .../tests/scenario/base.py | 35 ++ .../tests/scenario/test_internal_dns.py | 376 ++++++++++++++++++ 3 files changed, 415 insertions(+) create mode 100644 whitebox_neutron_tempest_plugin/tests/scenario/test_internal_dns.py diff --git a/whitebox_neutron_tempest_plugin/config.py b/whitebox_neutron_tempest_plugin/config.py index c14c566..c43e344 100644 --- a/whitebox_neutron_tempest_plugin/config.py +++ b/whitebox_neutron_tempest_plugin/config.py @@ -22,6 +22,10 @@ whitebox_neutron_plugin_options = cfg.OptGroup( ) WhiteboxNeutronPluginOptions = [ + cfg.BoolOpt('node_power_change', + default=True, + help='Whether to power off/on nodes, ' + 'such as controller/compute.'), cfg.StrOpt('openstack_type', default='devstack', help='Type of openstack deployment, ' diff --git a/whitebox_neutron_tempest_plugin/tests/scenario/base.py b/whitebox_neutron_tempest_plugin/tests/scenario/base.py index 5d8e46d..a7a139b 100644 --- a/whitebox_neutron_tempest_plugin/tests/scenario/base.py +++ b/whitebox_neutron_tempest_plugin/tests/scenario/base.py @@ -132,6 +132,12 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase): return subnet['cidr'] return None + def find_node_client(self, node_name): + for node in self.nodes: + if node['name'] == node_name: + return node['client'] + return None + @staticmethod def _get_local_ip_from_network(network): host_ip_addresses = [ifaddresses(iface)[AF_INET][0]['addr'] @@ -471,6 +477,35 @@ class BaseTempestWhiteboxTestCase(base.BaseTempestTestCase): return sender, receiver +class BaseTempestTestCaseAdvanced(BaseTempestWhiteboxTestCase): + """Base class skips test suites unless advanced image is available, + also defines handy test settings for advanced image use. + """ + + @classmethod + def skip_checks(cls): + super(BaseTempestTestCaseAdvanced, cls).skip_checks() + advanced_image_available = ( + CONF.neutron_plugin_options.advanced_image_ref or + CONF.neutron_plugin_options.default_image_is_advanced) + if not advanced_image_available: + skip_reason = "This test requires advanced image and tools" + raise cls.skipException(skip_reason) + + @classmethod + def resource_setup(cls): + super(BaseTempestTestCaseAdvanced, cls).resource_setup() + if 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 + + class TrafficFlowTest(BaseTempestWhiteboxTestCase): force_tenant_isolation = False diff --git a/whitebox_neutron_tempest_plugin/tests/scenario/test_internal_dns.py b/whitebox_neutron_tempest_plugin/tests/scenario/test_internal_dns.py new file mode 100644 index 0000000..6ed98bf --- /dev/null +++ b/whitebox_neutron_tempest_plugin/tests/scenario/test_internal_dns.py @@ -0,0 +1,376 @@ +# 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. +import re + +from neutron_lib import constants as lib_constants +from neutron_tempest_plugin.common import ssh +from neutron_tempest_plugin.common import utils as common_utils +from neutron_tempest_plugin import config +from oslo_log import log +from tempest.common import utils +from tempest.lib.common.utils import data_utils +from tempest.lib import decorators +from tempest.lib.exceptions import SSHExecCommandFailed +# TODO(mblue): implement alternative for next gen +# from tempest_helper_plugin.common.utils.linux import node_power + +from whitebox_neutron_tempest_plugin.tests.scenario import base + +CONF = config.CONF +WB_CONF = CONF.whitebox_neutron_plugin_options +LOG = log.getLogger(__name__) + + +class InternalDNSBaseCommon(base.BaseTempestWhiteboxTestCase): + """Common base class of resources and functionalities for test classes.""" + + port_error_msg = ('Openstack command returned incorrect' + ' hostname value in port.') + ssh_error_msg = ('Remote shell command returned incorrect hostname value' + " (command: 'cat /etc/hostname').") + ssh_hostname_cmd = 'cat /etc/hostname' + + @staticmethod + def _rand_name(name): + """'data_utils.rand_name' wrapper, show name related to test suite.""" + return data_utils.rand_name('internal-dns-test-{}'.format(name)) + + @classmethod + def resource_setup(cls): + super(InternalDNSBaseCommon, cls).resource_setup() + # setup reusable resources for entire test suite + cls.keypair = cls.create_keypair( + name=cls._rand_name('shared-keypair')) + cls.secgroup = cls.create_security_group( + name=cls._rand_name('shared-secgroup')) + cls.security_groups.append(cls.secgroup) + cls.create_loginable_secgroup_rule( + secgroup_id=cls.secgroup['id']) + cls.create_pingable_secgroup_rule( + secgroup_id=cls.secgroup['id']) + cls.network = cls.create_network(name=cls._rand_name('shared-network')) + cls.subnet = cls.create_subnet( + cls.network, name=cls._rand_name('shared-subnet')) + cls.router = cls.create_router_by_client() + cls.create_router_interface(cls.router['id'], cls.subnet['id']) + cls.vm_kwargs = { + 'flavor_ref': cls.flavor_ref, + 'image_ref': cls.image_ref, + 'key_name': cls.keypair['name'], + 'security_groups': [{'name': cls.secgroup['name']}] + } + + def _create_ssh_client(self, ip_addr): + return ssh.Client(ip_addr, + self.username, + pkey=self.keypair['private_key']) + + def _validate_port_dns_details(self, expected_hostname, checked_port, + raise_exception=True): + """Validates reused objects for correct dns values in tests.""" + result = True + dns_details = checked_port['dns_assignment'][0] + try: + self.assertEqual(expected_hostname, checked_port['dns_name'], + self.port_error_msg) + self.assertEqual(expected_hostname, dns_details['hostname'], + self.port_error_msg) + self.assertIn(expected_hostname, dns_details['fqdn'], + self.port_error_msg) + # returns boolean instead of raising assert exception when needed + except AssertionError: + if raise_exception: + raise + result = False + return result + + def _validate_ssh_dns_details(self, expected_hostname, ssh_client, + raise_exception=True): + """Validates correct dns values returned from ssh command in tests.""" + ssh_output = ssh_client.exec_command(self.ssh_hostname_cmd) + result = expected_hostname in ssh_output + if raise_exception and not result: + self.fail(self.ssh_error_msg) + return result + + def _dns_common_validations(self, vm_name, dns_port, vm_client): + """Validate hostname (dns-name) using API, and guest VM.""" + # retry to get ssh connection until VM is up and working (with timeout) + try: + common_utils.wait_until_true( + lambda: self._validate_ssh_dns_details(vm_name, + vm_client, + raise_exception=False), + timeout=120, + sleep=10) + except common_utils.WaitTimeout: + self.fail(self.ssh_error_msg) + # validate dns port hostname from API + self._validate_port_dns_details(vm_name, dns_port) + + def _common_create_and_update_port_with_dns_name(self): + """Helper function that creates and updates a port with correct + internal dns-name (hostname), without any validations afterwards. + """ + + # 1) Create a port with wrong dns-name (not as VM name). + # 2) Verify that wrong port initial dns-name. + # was queried from openstack API. + # 3) Update the port with correct dns-name (as VM name). + # 4) Boot a VM with corrected predefined port. + + # NOTE: VM's hostname has to be the same as VM's name + # when a VM is created, it is a known limitation. + # Therefore VM's dns-name/hostname is checked to be as VM's name. + + vm_correct_name = self._rand_name('vm') + vm_wrong_name = self._rand_name('bazinga') + # create port with wrong dns-name (not as VM name) + dns_port = self.create_port(self.network, + dns_name=vm_wrong_name, + security_groups=[self.secgroup['id']], + name=self._rand_name('port')) + # validate dns port with wrong initial hostname from API + self._validate_port_dns_details(vm_wrong_name, dns_port) + # update port with correct dns-name (as VM name) + dns_port = self.update_port(dns_port, dns_name=vm_correct_name) + # create VM with correct predefined dns-name on port + vm_1 = self.create_server(name=vm_correct_name, + networks=[{'port': dns_port['id']}], + **self.vm_kwargs) + vm_1['fip'] = self.create_floatingip(port=dns_port) + vm_1['ssh_client'] = self._create_ssh_client( + vm_1['fip']['floating_ip_address']) + # return parameters required for validations + return (vm_correct_name, dns_port, vm_1) + + +class InternalDNSBaseOvn(base.BaseTempestTestCaseOvn, + InternalDNSBaseCommon): + """Ovn base class of resources and functionalities for test class.""" + + ovn_db_hostname_cmd = '{} list dns' + ovn_db_error_msg = ('Incorrect hostname/ip values in NBDB, ' + 'or failed to reach OVN NBDB.') + + def _get_router_and_nodes_info(self): + self.router_port = self.os_admin.network_client.list_ports( + device_id=self.router['id'], + device_owner=lib_constants.DEVICE_OWNER_ROUTER_GW)['ports'][0] + self.router_gateway_chassis = self.get_router_gateway_chassis( + self.router_port['id']) + self.discover_nodes() + + def _validate_dns_ovn_nbdb(self, expected_hostname, local_ip): + """Validates correct dns values exist in OVN NBDB, + if so, then returns True, otherwise returns False. + """ + # optional quotation marks for OSP 13 + dns_pattern = '.*"?{}"?="?{}"?.*'.format(expected_hostname, local_ip) + try: + db_dns_entries = self.run_on_master_controller( + self.ovn_db_hostname_cmd.format(self.nbctl)).replace('\n', '') + except SSHExecCommandFailed as err: + LOG.warning(err) + return False + result = re.match(dns_pattern, db_dns_entries) + if not result: + err = "{}:\n'{}' regex not found in string '{}'".format( + self.ovn_db_error_msg, dns_pattern, db_dns_entries) + LOG.warning(err) + return False + return True + + def _dns_all_validations(self, vm_name, dns_port, vm_client): + """Validate hostname (dns-name) using API, guest VM, and OVN NBDB.""" + # validate dns port hostname using API and check on guest VM + self._dns_common_validations(vm_name, dns_port, vm_client) + # validate dns-name details in OVN NBDB + try: + common_utils.wait_until_true( + lambda: self._validate_dns_ovn_nbdb( + vm_name, + dns_port['fixed_ips'][0]['ip_address']), + timeout=120, + sleep=10) + except common_utils.WaitTimeout: + self.fail(self.ovn_db_error_msg) + + +class InternalDNSTestOvn(InternalDNSBaseOvn): + """Tests internal DNS capabilities on OVN setups.""" + + @utils.requires_ext(extension="dns-integration", service="network") + @decorators.idempotent_id('6349ce8c-bc10-485a-a21b-da073241420e') + def test_ovn_create_and_update_port_with_dns_name(self): + """Test creation of port with correct internal dns-name (hostname).""" + + # 1) Create resources: network, subnet, etc. + # 2) Create a port with wrong dns-name (not as VM name). + # 3) Verify that wrong port initial dns-name. + # was queried from openstack API. + # 4) Update the port with correct dns-name (as VM name). + # 5) Boot a VM with corrected predefined port. + # 6) Verify that correct port dns-name + # was queried from openstack API. + # 7) Validate hostname configured on VM is same as VM's name. + # 8) Validate hostname configured correctly in OVN NBDB. + + # NOTE: VM's hostname has to be the same as VM's name + # when a VM is created, it is a known limitation. + # Therefore VM's dns-name/hostname is checked to be as VM's name. + + # all test steps 2 - 5 (inclusively) + vm_name, dns_port, vm_1 = \ + self._common_create_and_update_port_with_dns_name() + # validate hostname (dns-name) using API, guest VM, and OVN NBDB + self._dns_all_validations(vm_name, dns_port, vm_1['ssh_client']) + + +class InternalDNSInterruptionsTestOvn(InternalDNSBaseOvn): + """Tests internal DNS capabilities on OVN setups, + with interruptions in overcloud. + """ + + @utils.requires_ext(extension="dns-integration", service="network") + @decorators.idempotent_id('bf11667e-34f8-4ac4-886b-45e099fdbffa') + def test_dns_name_after_ovn_controller_restart(self): + """Tests that OpenStack port, guest VM and OVN NB database + have correct dns-name (hostname) set, after controller service + restart on compute node. + """ + + # 1) Create resources: network, subnet, etc. + # 2) Create a port with dns-name. + # 3) Boot a guest VM with predefined port. + # 4) Restart ovn controller service on compute which runs guest VM. + # 5) Validate hostname configured on VM is the same as VM's name. + # 6) Verify that the correct port dns-name (as VM name) + # was queried from openstack API. + # 7) Validate dns-name details in OVN NB database. + + # NOTE: VM's hostname has to be the same as VM's name + # when a VM is created, it is a known limitation. + # Therefore VM's dns-name/hostname is checked to be as VM's name. + + vm_name = self._rand_name('vm') + # create port with dns-name (as VM name) + dns_port = self.create_port(self.network, + dns_name=vm_name, + security_groups=[self.secgroup['id']], + name=self._rand_name('port')) + # create VM with predefined dns-name on port + vm_1 = self.create_server(name=vm_name, + networks=[{'port': dns_port['id']}], + **self.vm_kwargs) + vm_1['fip'] = self.create_floatingip(port=dns_port) + # restart controller service on compute which runs guest VM + self.discover_nodes() + compute_hostname = self.get_host_for_server( + vm_1['server']['id']).partition('.')[0] + compute_client = self.find_node_client(compute_hostname) + if WB_CONF.openstack_type == 'devstack': + service_prefix = 'devstack@' + mid_char = '-' + elif WB_CONF.openstack_type == 'podified': + service_prefix = 'edpm_' + mid_char = '_' + else: + service_prefix = '' + mid_char = '_' + compute_client.exec_command( + "sudo systemctl restart {}ovn{}controller.service".format( + service_prefix, mid_char)) + # validate hostname configured on VM is same as VM's name + vm_1['ssh_client'] = self._create_ssh_client( + vm_1['fip']['floating_ip_address']) + # validate hostname (dns-name) using API, guest VM, and OVN NBDB + self._dns_all_validations(vm_name, dns_port, vm_1['ssh_client']) + + +class InternalDNSInterruptionsAdvancedTestOvn( + InternalDNSBaseOvn, + base.BaseTempestTestCaseAdvanced): + """Tests internal DNS capabilities with interruptions in overcloud, + on advanced image only. + """ + + @classmethod + def skip_checks(cls): + super(InternalDNSInterruptionsAdvancedTestOvn, cls).skip_checks() + if WB_CONF.openstack_type == 'devstack': + cls.skipException( + "Devstack doesn't support powering nodes on/off, " + "skipping tests") + if not WB_CONF.node_power_change: + cls.skipException( + "node_power_change is not enabled, skipping tests") + + @decorators.attr(type='slow') + @utils.requires_ext(extension="dns-integration", service="network") + @decorators.idempotent_id('e6c5dbea-d704-4cda-bb92-a5bfd0aa1bb2') + def test_ovn_dns_name_after_networker_reboot(self): + """Tests that OpenStack port, guest VM and OVN NB database have correct + dns-name (hostname) when master networker node is turned off and on. + """ + + # 1) Create resources: network, subnet, etc. + # 2) Create a port with dns-name. + # 3) Boot a VM with predefined port. + # 4) Soft shutdown master networker node. + # 5) Validate hostname (dns-name) using API, guest VM, + # and OVN NBDB when networker node is off. + # 6) Turn on previous master networker node, wait until it is working. + # 7) Validate hostname (dns-name) using API, guest VM, + # and OVN NBDB when networker node is on. + + # NOTE: VM's hostname has to be the same as VM's name + # when a VM is created, it is a known limitation. + # Therefore VM's dns-name/hostname is checked to be as VM's name. + + # TODO(mblue): implement alternative for next gen + # ensures overcloud nodes are up for next tests + self.skipException("Powering nodes not supported yet (TODO)") + # self.addCleanup(self.ensure_overcloud_nodes_active) + # create port with dns-name (as VM name) + vm_name = self._rand_name('vm') + dns_port = self.create_port(self.network, + dns_name=vm_name, + security_groups=[self.secgroup['id']], + name=self._rand_name('port')) + # create VM with predefined dns-name on port + vm_1 = self.create_server(name=vm_name, + networks=[{'port': dns_port['id']}], + **self.vm_kwargs) + vm_1['fip'] = self.create_floatingip(port=dns_port) + vm_1['ssh_client'] = self._create_ssh_client( + vm_1['fip']['floating_ip_address']) + self._get_router_and_nodes_info() + # TODO(mblue): implement alternative for next gen + # node_id = self.hosts_info.get_overcloud_node_id( + # self.router_gateway_chassis) + # soft shutdown master networker node + # TODO(mblue): implement alternative for next gen + # node_power.power_off(node_id) + # validate hostname (dns-name) using API, guest VM, + # and OVN NBDB when networker node is off and on + self._dns_all_validations(vm_name, dns_port, vm_1['ssh_client']) + # turn on networker node, wait until it is up and working + # TODO(mblue): implement alternative for next gen + # node_power.power_on( + # node_id, + # services_clients=[self.os_admin.network_client], + # agents_clients=[self.os_admin.compute.ServicesClient()]) + self._dns_all_validations(vm_name, dns_port, vm_1['ssh_client'])