diff --git a/tobiko/openstack/neutron/config.py b/tobiko/openstack/neutron/config.py index 88f5ad03d..a8ec19473 100644 --- a/tobiko/openstack/neutron/config.py +++ b/tobiko/openstack/neutron/config.py @@ -65,15 +65,9 @@ OPTIONS = [ cfg.IntOpt('dscp_mark', default=40, help="The DSCP marking value for the QoS Policy Rule"), - cfg.StrOpt('trunk_subport_segmentation_type', - default='vlan', - help="Trunk subport segmentation type"), - cfg.IntOpt('trunk_subport_segmentation_id', + cfg.IntOpt('vlan_id', default=101, - help="Trunk subport segmentation ID"), - cfg.StrOpt('trunk_subport_subnet_cidr', - default='192.168.101.0/24', - help="The CIDR block for trunk subport subnet") + help="VLAN trunk subport segmentation ID"), ] diff --git a/tobiko/openstack/stacks/__init__.py b/tobiko/openstack/stacks/__init__.py index 6bbe7444e..33b3df860 100644 --- a/tobiko/openstack/stacks/__init__.py +++ b/tobiko/openstack/stacks/__init__.py @@ -25,12 +25,13 @@ from tobiko.openstack.stacks import _nova from tobiko.openstack.stacks import _octavia from tobiko.openstack.stacks import _qos from tobiko.openstack.stacks import _ubuntu +from tobiko.openstack.stacks import _vlan + CentosFlavorStackFixture = _centos.CentosFlavorStackFixture CentosImageFixture = _centos.CentosImageFixture CentosServerStackFixture = _centos.CentosServerStackFixture Centos7ServerStackFixture = _centos.Centos7ServerStackFixture -CentosTrunkServerStackFixture = _centos.CentosTrunkServerStackFixture CirrosFlavorStackFixture = _cirros.CirrosFlavorStackFixture CirrosImageFixture = _cirros.CirrosImageFixture @@ -52,7 +53,6 @@ FedoraServerStackFixture = _fedora.FedoraServerStackFixture RedHatFlavorStackFixture = _redhat.RedHatFlavorStackFixture RhelImageFixture = _redhat.RhelImageFixture RedHatServerStackFixture = _redhat.RedHatServerStackFixture -RedHatTrunkServerStackFixture = _redhat.RedHatTrunkServerStackFixture L3haNetworkStackFixture = _l3ha.L3haNetworkStackFixture L3haServerStackFixture = _l3ha.L3haServerStackFixture @@ -80,6 +80,16 @@ ServerGroupStackFixture = _nova.ServerGroupStackFixture AffinityServerGroupStackFixture = _nova.AffinityServerGroupStackFixture AntiAffinityServerGroupStackFixture = _nova.AntiAffinityServerGroupStackFixture +OctaviaLoadbalancerStackFixture = _octavia.OctaviaLoadbalancerStackFixture +OctaviaListenerStackFixture = _octavia.OctaviaListenerStackFixture +OctaviaPoolStackFixture = _octavia.OctaviaPoolStackFixture +OctaviaMemberServerStackFixture = _octavia.OctaviaMemberServerStackFixture +OctaviaServerStackFixture = _octavia.OctaviaServerStackFixture +OctaviaClientServerStackFixture = _octavia.OctaviaClientServerStackFixture +OctaviaOtherServerStackFixture = _octavia.OctaviaOtherServerStackFixture +OctaviaOtherMemberServerStackFixture = ( + _octavia.OctaviaOtherMemberServerStackFixture) + QosNetworkStackFixture = _qos.QosNetworkStackFixture QosPolicyStackFixture = _qos.QosPolicyStackFixture QosServerStackFixture = _qos.QosServerStackFixture @@ -91,12 +101,5 @@ UbuntuServerStackFixture = _ubuntu.UbuntuServerStackFixture UbuntuMinimalServerStackFixture = _ubuntu.UbuntuMinimalServerStackFixture UbuntuExternalServerStackFixture = _ubuntu.UbuntuExternalServerStackFixture -OctaviaLoadbalancerStackFixture = _octavia.OctaviaLoadbalancerStackFixture -OctaviaListenerStackFixture = _octavia.OctaviaListenerStackFixture -OctaviaPoolStackFixture = _octavia.OctaviaPoolStackFixture -OctaviaMemberServerStackFixture = _octavia.OctaviaMemberServerStackFixture -OctaviaServerStackFixture = _octavia.OctaviaServerStackFixture -OctaviaClientServerStackFixture = _octavia.OctaviaClientServerStackFixture -OctaviaOtherServerStackFixture = _octavia.OctaviaOtherServerStackFixture -OctaviaOtherMemberServerStackFixture = ( - _octavia.OctaviaOtherMemberServerStackFixture) +VlanNetworkStackFixture = _vlan.VlanNetworkStackFixture +VlanProxyServerStackFixture = _vlan.VlanProxyServerStackFixture diff --git a/tobiko/openstack/stacks/_centos.py b/tobiko/openstack/stacks/_centos.py index a79c5cdfc..80f19b48c 100644 --- a/tobiko/openstack/stacks/_centos.py +++ b/tobiko/openstack/stacks/_centos.py @@ -71,27 +71,3 @@ class Centos7ServerStackFixture(CentosServerStackFixture): #: Glance image used to create a Nova server instance image_fixture = tobiko.required_setup_fixture(Centos7ImageFixture) - - -class CentosTrunkServerStackFixture( - CentosServerStackFixture, _nova.TrunkServerStackFixture): - - interface = 'eth0' - - @property - def user_data(self): - return ("#cloud-config\n" - "write_files:\n" - "- path: {path}/ifcfg-{interface}.{index}\n" - " owner: \"root\"\n" - " permissions: \"777\"\n" - " content: |\n" - " DEVICE=\"{interface}.{index}\"\n" - " BOOTPROTO=\"none\"\n" - " ONBOOT=\"yes\"\n" - " VLAN=\"yes\"\n" - " PERSISTENT_DHCLIENT=\"no\"\n" - "runcmd:\n" - "- [ sh, -c , \"systemctl restart NetworkManager\" ]".format( - path='/etc/sysconfig/network-scripts/', - interface=self.interface, index=self.trunk_subport_vlan)) diff --git a/tobiko/openstack/stacks/_cirros.py b/tobiko/openstack/stacks/_cirros.py index f586c1ac3..949a69a81 100644 --- a/tobiko/openstack/stacks/_cirros.py +++ b/tobiko/openstack/stacks/_cirros.py @@ -13,7 +13,6 @@ # under the License. from __future__ import absolute_import -import tobiko from tobiko import config from tobiko.openstack import glance from tobiko.openstack.stacks import _nova diff --git a/tobiko/openstack/stacks/_nova.py b/tobiko/openstack/stacks/_nova.py index 882ece43a..d5ce2a6ae 100644 --- a/tobiko/openstack/stacks/_nova.py +++ b/tobiko/openstack/stacks/_nova.py @@ -218,6 +218,8 @@ class ServerStackFixture(heat.HeatStackFixture, abc.ABC): def fixed_ipv6(self): return self.find_fixed_ip(ip_version=6) + has_vlan = False + #: Schedule on different host that this Nova server instance ID different_host = None @@ -355,6 +357,12 @@ class ServerStackFixture(heat.HeatStackFixture, abc.ABC): requirements['cores'] += (self.flavor_stack.vcpus or 1) return requirements + @property + def neutron_required_quota_set(self) -> typing.Dict[str, int]: + requirements = super().neutron_required_quota_set + requirements['port'] += 1 + return requirements + def assert_is_reachable(self): ping.assert_reachable_hosts([self.ip_address]) @@ -535,11 +543,3 @@ class AntiAffinityServerGroupStackFixture(tobiko.SharedFixture): @property def scheduler_group(self): return self.server_group_stack.anti_affinity_server_group_id - - -@neutron.skip_if_missing_networking_extensions('trunk') -class TrunkServerStackFixture(CloudInitServerStackFixture): - has_trunk = True - segmentation_id = CONF.tobiko.neutron.trunk_subport_segmentation_id - segmentation_type = CONF.tobiko.neutron.trunk_subport_segmentation_type - trunk_subport_cidr = CONF.tobiko.neutron.trunk_subport_subnet_cidr diff --git a/tobiko/openstack/stacks/_redhat.py b/tobiko/openstack/stacks/_redhat.py index e4fe77f03..e46817114 100644 --- a/tobiko/openstack/stacks/_redhat.py +++ b/tobiko/openstack/stacks/_redhat.py @@ -86,8 +86,3 @@ class RedHatServerStackFixture(_centos.CentosServerStackFixture): #: Flavor used to create a Nova server instance flavor_stack = tobiko.required_setup_fixture(RedHatFlavorStackFixture) - - -class RedHatTrunkServerStackFixture( - RedHatServerStackFixture, _centos.CentosTrunkServerStackFixture): - pass diff --git a/tobiko/openstack/stacks/_ubuntu.py b/tobiko/openstack/stacks/_ubuntu.py index 9d3976a67..0c23fa269 100644 --- a/tobiko/openstack/stacks/_ubuntu.py +++ b/tobiko/openstack/stacks/_ubuntu.py @@ -15,10 +15,13 @@ from __future__ import absolute_import import typing +import yaml + import tobiko from tobiko import config from tobiko.openstack import glance from tobiko.openstack.stacks import _nova +from tobiko.openstack.stacks import _vlan CONF = config.CONF @@ -71,6 +74,7 @@ class UbuntuImageFixture(UbuntuMinimalImageFixture, - ping - ncat - nginx + - vlan The image will also have below running services: - nginx HTTP server listening on TCP port 80 @@ -86,8 +90,10 @@ class UbuntuImageFixture(UbuntuMinimalImageFixture, def install_packages(self) -> typing.List[str]: return super().install_packages + ['iperf3', 'iputils-ping', + 'nano', 'ncat', - 'nginx'] + 'nginx', + 'vlan'] # port of running HTTP server http_port = 80 @@ -103,8 +109,44 @@ class UbuntuImageFixture(UbuntuMinimalImageFixture, '> /etc/systemd/system/iperf3-server@.service') run_commands.append( f"systemctl enable iperf3-server@{self.iperf3_port}") + run_commands.append('echo "8021q" >> /etc/modules') return run_commands + @property + def ethernet_device(self) -> str: + return 'ens3' + + @property + def vlan_id(self) -> int: + return tobiko.tobiko_config().neutron.vlan_id + + @property + def vlan_device(self) -> str: + return f'vlan{self.vlan_id}' + + @property + def vlan_config(self) -> typing.Dict[str, typing.Any]: + return { + 'network': { + 'version': 2, + 'vlans': { + self.vlan_device: { + 'link': self.ethernet_device, + 'dhcp4': True, + 'dhcp6': True, + 'id': self.vlan_id, + } + } + } + } + + @property + def write_files(self) -> typing.Dict[str, str]: + write_files = super().write_files + write_files['/etc/netplan/75-tobiko-vlan.yaml'] = yaml.dump( + self.vlan_config) + return write_files + class UbuntuFlavorStackFixture(_nova.FlavorStackFixture): ram = 128 @@ -120,7 +162,8 @@ class UbuntuMinimalServerStackFixture(_nova.CloudInitServerStackFixture): flavor_stack = tobiko.required_setup_fixture(UbuntuFlavorStackFixture) -class UbuntuServerStackFixture(UbuntuMinimalServerStackFixture): +class UbuntuServerStackFixture(UbuntuMinimalServerStackFixture, + _vlan.VlanServerStackFixture): """Ubuntu server running an HTTP server The server has additional commands compared to the minimal one: @@ -140,6 +183,14 @@ class UbuntuServerStackFixture(UbuntuMinimalServerStackFixture): def iperf3_port(self) -> int: return self.image_fixture.iperf3_port + @property + def vlan_id(self) -> int: + return self.image_fixture.vlan_id + + @property + def vlan_device(self) -> str: + return self.image_fixture.vlan_device + class UbuntuExternalServerStackFixture(UbuntuServerStackFixture, _nova.ExternalServerStackFixture): diff --git a/tobiko/openstack/stacks/_vlan.py b/tobiko/openstack/stacks/_vlan.py new file mode 100644 index 000000000..3c72f6c1f --- /dev/null +++ b/tobiko/openstack/stacks/_vlan.py @@ -0,0 +1,108 @@ +# Copyright (c) 2021 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 abc +import typing + +import netaddr + +import tobiko +from tobiko import config +from tobiko.shell import ping +from tobiko.shell import ssh +from tobiko.openstack import neutron +from tobiko.openstack.stacks import _cirros +from tobiko.openstack.stacks import _neutron +from tobiko.openstack.stacks import _nova + + +CONF = config.CONF + + +class VlanNetworkStackFixture(_neutron.NetworkStackFixture): + pass + + +class VlanProxyServerStackFixture(_cirros.CirrosServerStackFixture): + network_stack = tobiko.required_fixture(VlanNetworkStackFixture) + + +@neutron.skip_if_missing_networking_extensions('trunk') +class VlanServerStackFixture(_nova.ServerStackFixture, abc.ABC): + + has_vlan = True + + #: stack with the newtwork where the trunk support is attached + vlan_network_stack = tobiko.required_fixture(VlanNetworkStackFixture) + + @property + def vlan_id(self) -> int: + return CONF.tobiko.neutron.vlan_id + + @property + def vlan_network(self) -> str: + return self.vlan_network_stack.network_id + + @property + def vlan_fixed_ipv4(self): + return self.find_vlan_fixed_ip(ip_version=4) + + @property + def vlan_fixed_ipv6(self): + return self.find_vlan_fixed_ip(ip_version=6) + + def find_vlan_fixed_ip(self, + ip_version: int = None, + unique=False) -> netaddr.IPAddress: + vlan_ips = self.list_vlan_fixed_ips(ip_version=ip_version) + if unique: + return vlan_ips.unique + else: + return vlan_ips.first + + def list_vlan_fixed_ips(self, + ip_version: int = None) \ + -> tobiko.Selection[netaddr.IPAddress]: + fixed_ips: tobiko.Selection[netaddr.IPAddress] = tobiko.Selection( + netaddr.IPAddress(fixed_ip['ip_address']) + for fixed_ip in self.vlan_fixed_ips) + if ip_version is not None: + fixed_ips = fixed_ips.with_attributes(version=ip_version) + return fixed_ips + + @property + def vlan_ssh_proxy_client(self) -> ssh.SSHClientType: + return tobiko.setup_fixture(VlanProxyServerStackFixture).ssh_client + + def assert_vlan_is_reachable(self, + ip_version: int = None): + fixed_ips = self.list_vlan_fixed_ips(ip_version=ip_version) + ping.assert_reachable_hosts(fixed_ips, + ssh_client=self.vlan_ssh_proxy_client) + + def assert_vlan_is_unreachable(self, + ip_version: int = None): + fixed_ips = self.list_vlan_fixed_ips(ip_version=ip_version) + ping.assert_unreachable_hosts(fixed_ips, + ssh_client=self.vlan_ssh_proxy_client) + + @property + def neutron_required_quota_set(self) -> typing.Dict[str, int]: + requirements = super().neutron_required_quota_set + requirements['port'] += 1 + requirements['trunk'] += 1 + return requirements diff --git a/tobiko/openstack/stacks/nova/server.yaml b/tobiko/openstack/stacks/nova/server.yaml index 4a1d68285..94543b0cc 100644 --- a/tobiko/openstack/stacks/nova/server.yaml +++ b/tobiko/openstack/stacks/nova/server.yaml @@ -72,23 +72,19 @@ parameters: default: '' description: Optional user_data to be passed to the server - has_trunk: + has_vlan: type: boolean default: false - trunk_subport_cidr: + vlan_network: type: string + description: Network ID where vlan trunk support is attached default: '' - segmentation_type: - type: string - description: Segmentation type for trunk subport - default: '' - - segmentation_id: + vlan_id: type: number - description: Segmentation ID for trunk subport - default: 0 + description: Segmentation ID for vlan trunk subport + default: 101 conditions: @@ -98,8 +94,8 @@ conditions: use_extra_dhcp_opts: get_param: use_extra_dhcp_opts - has_trunk: - get_param: has_trunk + has_vlan: + get_param: has_vlan resources: @@ -154,34 +150,26 @@ resources: floating_network: {get_param: floating_network} port_id: {get_resource: port} - trunk_subport_network: - type: OS::Neutron::Net - condition: has_trunk - - trunk_subport_subnet: - type: OS::Neutron::Subnet - condition: has_trunk - properties: - network: {get_resource: trunk_subport_network} - cidr: {get_param: trunk_subport_cidr} - - trunk_subport: + vlan_port: type: OS::Neutron::Port - condition: has_trunk + description: Vlan trunk subport + condition: has_vlan properties: - network: {get_resource: trunk_subport_network} + network: {get_param: vlan_network} mac_address: {get_attr: [port, mac_address]} + port_security_enabled: {get_param: port_security_enabled} + security_groups: {get_param: security_groups} trunk: type: OS::Neutron::Trunk - description: Trunk port connected to the server - condition: has_trunk + description: Trunk connected to the server port + condition: has_vlan properties: port: {get_resource: port} sub_ports: - - {port: {get_resource: trunk_subport}, - segmentation_type: {get_param: segmentation_type}, - segmentation_id: {get_param: segmentation_id}} + - port: {get_resource: vlan_port} + segmentation_type: vlan + segmentation_id: {get_param: vlan_id} outputs: @@ -208,3 +196,12 @@ outputs: port_id: value: {get_resource: port} + + vlan_port_id: + value: {get_resource: vlan_port} + condition: has_vlan + + vlan_fixed_ips: + description: fixed IP addresses of server vlan port + value: {get_attr: [vlan_port, fixed_ips]} + condition: has_vlan diff --git a/tobiko/shell/ip.py b/tobiko/shell/ip.py index 6992a762a..9b21dd10c 100644 --- a/tobiko/shell/ip.py +++ b/tobiko/shell/ip.py @@ -76,6 +76,22 @@ def list_ip_addresses(ip_version: int = None, return ips +def find_ip_address(ip_version: int = None, + device: str = None, + scope: str = None, + unique: bool = False, + **execute_params) -> \ + netaddr.IPAddress: + ips = list_ip_addresses(ip_version=ip_version, + device=device, + scope=scope, + **execute_params) + if unique: + return ips.unique + else: + return ips.first + + def parse_ip_address(text: str) -> typing.Tuple[netaddr.IPAddress, int]: if '/' in text: # Remove netmask prefix length diff --git a/tobiko/tests/functional/openstack/stacks/test_vlan.py b/tobiko/tests/functional/openstack/stacks/test_vlan.py new file mode 100644 index 000000000..b43eed68e --- /dev/null +++ b/tobiko/tests/functional/openstack/stacks/test_vlan.py @@ -0,0 +1,68 @@ +# Copyright (c) 2019 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 testtools + +import tobiko +from tobiko.openstack import stacks +from tobiko.shell import ip +from tobiko.tests.functional.openstack.stacks import test_cirros + + +class VlanProxyServerStackTest(test_cirros.CirrosServerStackTest): + + #: Stack of resources with a server attached to a floating IP + stack = tobiko.required_fixture(stacks.VlanProxyServerStackFixture) + + +class UbuntuVlanServerTest(testtools.TestCase): + + #: Stack of resources with a server attached to a floating IP + stack = tobiko.required_fixture(stacks.UbuntuServerStackFixture) + + def test_vlan_ipv4_fixed_ip(self): + self._test_vlan_fixed_ip(ip_version=4) + + def test_vlan_ipv6_fixed_ip(self): + self._test_vlan_fixed_ip(ip_version=6) + + def _test_vlan_fixed_ip(self, ip_version: int): + expected_ip = self.get_vlan_fixed_ip(ip_version=ip_version) + for attempt in tobiko.retry(timeout=600., + interval=10.): + try: + actual_ip = ip.find_ip_address( + device=self.stack.vlan_device, + ip_version=ip_version, + ssh_client=self.stack.ssh_client, + scope='global', + unique=True) + except tobiko.ObjectNotFound: + attempt.check_limits() + else: + break + else: + raise RuntimeError('Broken retry loop') + self.assertEqual(expected_ip, actual_ip) + self.stack.assert_vlan_is_reachable(ip_version=ip_version) + + def get_vlan_fixed_ip(self, ip_version: int): + try: + return self.stack.find_vlan_fixed_ip(ip_version=ip_version) + except tobiko.ObjectNotFound: + self.skipTest(f"Server {self.stack.server_id} has any " + f"IPv{ip_version} address on VLAN device.") diff --git a/tobiko/tests/scenario/neutron/test_trunk.py b/tobiko/tests/scenario/neutron/test_trunk.py index 47f99735b..5c62330b0 100644 --- a/tobiko/tests/scenario/neutron/test_trunk.py +++ b/tobiko/tests/scenario/neutron/test_trunk.py @@ -20,7 +20,6 @@ import testtools import tobiko from tobiko import config -from tobiko.openstack import nova from tobiko.openstack import stacks @@ -28,31 +27,33 @@ CONF = config.CONF LOG = log.getLogger(__name__) -class CentosRebootTrunkServerStackFixture( - stacks.CentosTrunkServerStackFixture): +class RebootTrunkServerStackFixture(stacks.UbuntuServerStackFixture): pass -class CentosTrunkTest(testtools.TestCase): +class TrunkTest(testtools.TestCase): """Tests trunk functionality""" - stack = tobiko.required_fixture(CentosRebootTrunkServerStackFixture) + stack = tobiko.required_fixture(RebootTrunkServerStackFixture) + + vlan_proxy_stack = tobiko.required_fixture( + stacks.VlanProxyServerStackFixture) + + @property + def vlan_proxy_ssh_client(self): + return self.vlan_proxy_stack.ssh_client + + def test_activate_server(self): + self.stack.ensure_server_status('ACTIVE') + self.stack.assert_is_reachable() + self.stack.assert_vlan_is_reachable(ip_version=4) + + def test_shutoff_server(self): + self.stack.ensure_server_status('SHUTOFF') + self.stack.assert_is_unreachable() + self.stack.assert_vlan_is_unreachable(ip_version=4) @pytest.mark.ovn_migration - def test_after_server_reboot(self): - self.shutoff_server() - self.activate_server() - - def test_after_server_shutoff(self): - self.activate_server() - self.shutoff_server() - - def activate_server(self) -> nova.NovaServer: - server = self.stack.ensure_server_status('ACTIVE') - self.stack.assert_is_reachable() - return server - - def shutoff_server(self) -> nova.NovaServer: - server = self.stack.ensure_server_status('SHUTOFF') - self.stack.assert_is_unreachable() - return server + def test_shutoff_then_activate_server(self): + self.test_shutoff_server() + self.test_activate_server()