Merge "Add first VLAN Transparency tests"
diff --git a/neutron_tempest_plugin/common/ip.py b/neutron_tempest_plugin/common/ip.py
index d981770..7b172b0 100644
--- a/neutron_tempest_plugin/common/ip.py
+++ b/neutron_tempest_plugin/common/ip.py
@@ -57,6 +57,20 @@
         return shell.execute(command_line, ssh_client=self.ssh_client,
                              timeout=self.timeout).stdout
 
+    def configure_vlan(self, addresses, port, vlan_tag, subport_ips):
+        port_device = get_port_device_name(addresses=addresses, port=port)
+        subport_device = '{!s}.{!s}'.format(port_device, vlan_tag)
+        LOG.debug('Configuring VLAN subport interface %r on top of interface '
+                  '%r with IPs: %s', subport_device, port_device,
+                  ', '.join(subport_ips))
+
+        self.add_link(link=port_device, name=subport_device, link_type='vlan',
+                      segmentation_id=vlan_tag)
+        self.set_link(device=subport_device, state='up')
+        for subport_ip in subport_ips:
+            self.add_address(address=subport_ip, device=subport_device)
+        return subport_device
+
     def configure_vlan_subport(self, port, subport, vlan_tag, subnets):
         addresses = self.list_addresses()
         try:
@@ -77,18 +91,19 @@
                 "Unable to get IP address and subnet prefix lengths for "
                 "subport")
 
-        port_device = get_port_device_name(addresses=addresses, port=port)
-        subport_device = '{!s}.{!s}'.format(port_device, vlan_tag)
-        LOG.debug('Configuring VLAN subport interface %r on top of interface '
-                  '%r with IPs: %s', subport_device, port_device,
-                  ', '.join(subport_ips))
+        return self.configure_vlan(addresses, port, vlan_tag, subport_ips)
 
-        self.add_link(link=port_device, name=subport_device, link_type='vlan',
-                      segmentation_id=vlan_tag)
-        self.set_link(device=subport_device, state='up')
-        for subport_ip in subport_ips:
-            self.add_address(address=subport_ip, device=subport_device)
-        return subport_device
+    def configure_vlan_transparent(self, port, vlan_tag, ip_addresses):
+        addresses = self.list_addresses()
+        try:
+            subport_device = get_vlan_device_name(addresses, ip_addresses)
+        except ValueError:
+            pass
+        else:
+            LOG.debug('Interface %r already configured.', subport_device)
+            return subport_device
+
+        return self.configure_vlan(addresses, port, vlan_tag, ip_addresses)
 
     def list_namespaces(self):
         namespaces_output = self.execute("netns")
@@ -128,6 +143,23 @@
         # ip addr add 192.168.1.1/24 dev em1
         return self.execute('address', 'add', address, 'dev', device)
 
+    def delete_address(self, address, device):
+        # ip addr del 192.168.1.1/24 dev em1
+        return self.execute('address', 'del', address, 'dev', device)
+
+    def add_route(self, address, device, gateway=None):
+        if gateway:
+            # ip route add 192.168.1.0/24 via 192.168.22.1 dev em1
+            return self.execute(
+                'route', 'add', address, 'via', gateway, 'dev', device)
+        else:
+            # ip route add 192.168.1.0/24 dev em1
+            return self.execute('route', 'add', address, 'dev', device)
+
+    def delete_route(self, address, device):
+        # ip route del 192.168.1.0/24 dev em1
+        return self.execute('route', 'del', address, 'dev', device)
+
     def list_routes(self, *args):
         output = self.execute('route', 'show', *args)
         return list(parse_routes(output))
@@ -312,6 +344,15 @@
     raise ValueError(msg)
 
 
+def get_vlan_device_name(addresses, ip_addresses):
+    for address in list_ip_addresses(addresses=addresses,
+            ip_addresses=ip_addresses):
+        return address.device.name
+
+    msg = "Fixed IPs {0!r} not found on server.".format(' '.join(ip_addresses))
+    raise ValueError(msg)
+
+
 def _get_ip_address_prefix_len_pairs(port, subnets):
     subnets = {subnet['id']: subnet for subnet in subnets}
     for fixed_ip in port['fixed_ips']:
diff --git a/neutron_tempest_plugin/scenario/test_vlan_transparency.py b/neutron_tempest_plugin/scenario/test_vlan_transparency.py
new file mode 100644
index 0000000..d9a529c
--- /dev/null
+++ b/neutron_tempest_plugin/scenario/test_vlan_transparency.py
@@ -0,0 +1,186 @@
+# 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 oslo_log import log as logging
+from tempest.lib.common.utils import data_utils
+from tempest.lib import decorators
+
+from neutron_tempest_plugin.common import ip
+from neutron_tempest_plugin.common import ssh
+from neutron_tempest_plugin import config
+from neutron_tempest_plugin.scenario import base
+
+
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+MIN_VLAN_ID = 1
+MAX_VLAN_ID = 4094
+
+
+class VlanTransparencyTest(base.BaseTempestTestCase):
+    credentials = ['primary', 'admin']
+    force_tenant_isolation = False
+
+    required_extensions = ['vlan-transparent', 'allowed-address-pairs']
+
+    @classmethod
+    def resource_setup(cls):
+        super(VlanTransparencyTest, cls).resource_setup()
+        # setup basic topology for servers we can log into
+        cls.rand_name = data_utils.rand_name(
+            cls.__name__.rsplit('.', 1)[-1])
+        cls.network = cls.create_network(name=cls.rand_name,
+                                         vlan_transparent=True)
+        cls.subnet = cls.create_subnet(network=cls.network,
+                                       name=cls.rand_name)
+        cls.router = cls.create_router_by_client()
+        cls.create_router_interface(cls.router['id'], cls.subnet['id'])
+        cls.keypair = cls.create_keypair(name=cls.rand_name)
+        cls.vm_ports = []
+        cls.security_group = cls.create_security_group(name=cls.rand_name)
+        cls.create_loginable_secgroup_rule(cls.security_group['id'])
+
+        if CONF.neutron_plugin_options.default_image_is_advanced:
+            cls.flavor_ref = CONF.compute.flavor_ref
+            cls.image_ref = CONF.compute.image_ref
+        else:
+            cls.flavor_ref = \
+                CONF.neutron_plugin_options.advanced_image_flavor_ref
+            cls.image_ref = CONF.neutron_plugin_options.advanced_image_ref
+
+    @classmethod
+    def skip_checks(cls):
+        super(VlanTransparencyTest, cls).skip_checks()
+        if not (CONF.neutron_plugin_options.advanced_image_ref or
+                CONF.neutron_plugin_options.default_image_is_advanced):
+            raise cls.skipException(
+                'Advanced image is required to run these tests.')
+
+    def _create_port_and_server(self, index,
+                                port_security=True,
+                                allowed_address_pairs=None):
+        server_name = 'server-%s-%d' % (self.rand_name, index)
+        port_name = 'port-%s-%d' % (self.rand_name, index)
+        if port_security:
+            sec_groups = [self.security_group['id']]
+        else:
+            sec_groups = None
+        self.vm_ports.append(
+            self.create_port(network=self.network, name=port_name,
+                             security_groups=sec_groups,
+                             port_security_enabled=port_security,
+                             allowed_address_pairs=allowed_address_pairs))
+        return self.create_server(flavor_ref=self.flavor_ref,
+                                  image_ref=self.image_ref,
+                                  key_name=self.keypair['name'],
+                                  networks=[{'port': self.vm_ports[-1]['id']}],
+                                  name=server_name)['server']
+
+    def _configure_vlan_transparent(self, port, ssh_client,
+                                    vlan_tag, vlan_ip):
+        ip_command = ip.IPCommand(ssh_client=ssh_client)
+        addresses = ip_command.list_addresses(port=port)
+        port_iface = ip.get_port_device_name(addresses, port)
+        subport_iface = ip_command.configure_vlan_transparent(
+            port=port, vlan_tag=vlan_tag, ip_addresses=[vlan_ip])
+
+        for address in ip_command.list_addresses(ip_addresses=vlan_ip):
+            self.assertEqual(subport_iface, address.device.name)
+            self.assertEqual(port_iface, address.device.parent)
+            break
+        else:
+            self.fail("Sub-port fixed IP not found on server.")
+
+    def _create_ssh_client(self, floating_ip):
+        if CONF.neutron_plugin_options.default_image_is_advanced:
+            username = CONF.validation.image_ssh_user
+        else:
+            username = CONF.neutron_plugin_options.advanced_image_ssh_user
+        return ssh.Client(host=floating_ip['floating_ip_address'],
+                          username=username,
+                          pkey=self.keypair['private_key'])
+
+    def _test_basic_vlan_transparency_connectivity(
+            self, port_security=True, use_allowed_address_pairs=False):
+        vlan_tag = data_utils.rand_int_id(start=MIN_VLAN_ID, end=MAX_VLAN_ID)
+        vlan_ipmask_template = '192.168.%d.{ip_last_byte}/24' % (vlan_tag %
+                                                                 256)
+        vms = []
+        vlan_ipmasks = []
+        floating_ips = []
+        ssh_clients = []
+
+        for i in range(2):
+            vlan_ipmasks.append(vlan_ipmask_template.format(
+                ip_last_byte=(i + 1) * 10))
+            if use_allowed_address_pairs:
+                allowed_address_pairs = [{'ip_address': vlan_ipmasks[i]}]
+            else:
+                allowed_address_pairs = None
+            vms.append(self._create_port_and_server(
+                index=i,
+                port_security=port_security,
+                allowed_address_pairs=allowed_address_pairs))
+            floating_ips.append(self.create_floatingip(port=self.vm_ports[-1]))
+            ssh_clients.append(
+                self._create_ssh_client(floating_ip=floating_ips[i]))
+
+            self.check_connectivity(
+                host=floating_ips[i]['floating_ip_address'],
+                ssh_client=ssh_clients[i])
+            self._configure_vlan_transparent(port=self.vm_ports[-1],
+                                             ssh_client=ssh_clients[i],
+                                             vlan_tag=vlan_tag,
+                                             vlan_ip=vlan_ipmasks[i])
+
+        if port_security:
+            # Ping from vm0 to vm1 via VLAN interface should fail because
+            # we haven't allowed ICMP
+            self.check_remote_connectivity(
+                ssh_clients[0],
+                vlan_ipmasks[1].split('/')[0],
+                servers=vms,
+                should_succeed=False)
+
+            # allow intra-security-group traffic
+            sg_rule = self.create_pingable_secgroup_rule(
+                self.security_group['id'])
+            self.addCleanup(
+                    self.os_primary.network_client.delete_security_group_rule,
+                    sg_rule['id'])
+
+        # Ping from vm0 to vm1 via VLAN interface should pass because
+        # either port security is disabled or the ICMP sec group rule has been
+        # added
+        self.check_remote_connectivity(
+            ssh_clients[0],
+            vlan_ipmasks[1].split('/')[0],
+            servers=vms)
+        # Ping from vm1 to vm0 and check untagged packets are not dropped
+        self.check_remote_connectivity(
+            ssh_clients[1],
+            self.vm_ports[-2]['fixed_ips'][0]['ip_address'],
+            servers=vms)
+
+    @decorators.idempotent_id('a2694e3a-6d4d-4a23-9fcc-c3ed3ef37b16')
+    def test_vlan_transparent_port_sec_disabled(self):
+        self._test_basic_vlan_transparency_connectivity(
+            port_security=False, use_allowed_address_pairs=False)
+
+    @decorators.idempotent_id('2dd03b4f-9c20-4cda-8c6a-40fa453ec69a')
+    def test_vlan_transparent_allowed_address_pairs(self):
+        self._test_basic_vlan_transparency_connectivity(
+            port_security=True, use_allowed_address_pairs=True)
diff --git a/zuul.d/master_jobs.yaml b/zuul.d/master_jobs.yaml
index 5325f67..ee7c6e3 100644
--- a/zuul.d/master_jobs.yaml
+++ b/zuul.d/master_jobs.yaml
@@ -173,15 +173,21 @@
     pre-run: playbooks/linuxbridge-scenario-pre-run.yaml
     vars:
       network_api_extensions: *api_extensions
+      network_api_extensions_linuxbridge:
+        - vlan-transparent
       network_available_features: *available_features
+      # TODO(eolivare): remove VLAN Transparency tests from blacklist
+      # when bug https://bugs.launchpad.net/neutron/+bug/1907548 will be fixed
+      tempest_black_regex: "(^neutron_tempest_plugin.scenario.test_vlan_transparency.VlanTransparencyTest)"
       devstack_localrc:
         Q_AGENT: linuxbridge
-        NETWORK_API_EXTENSIONS: "{{ network_api_extensions | join(',') }}"
+        NETWORK_API_EXTENSIONS: "{{ (network_api_extensions + network_api_extensions_linuxbridge) | join(',') }}"
       devstack_local_conf:
         post-config:
           $NEUTRON_CONF:
             DEFAULT:
               enable_dvr: false
+              vlan_transparent: true
             AGENT:
               debug_iptables_rules: true
           # NOTE(slaweq): We can get rid of this hardcoded absolute path when
@@ -190,6 +196,7 @@
           /$NEUTRON_CORE_PLUGIN_CONF:
             ml2:
               type_drivers: flat,vlan,local,vxlan
+              mechanism_drivers: linuxbridge
         test-config:
           $TEMPEST_CONFIG:
             network-feature-enabled:
@@ -204,6 +211,8 @@
     timeout: 10000
     vars:
       network_api_extensions: *api_extensions
+      network_api_extensions_ovn:
+        - vlan-transparent
       # TODO(haleyb): Remove IPv6Test from blacklist when
       # https://bugs.launchpad.net/neutron/+bug/1881558 is fixed.
       # TODO(slaweq): Remove test_trunk_subport_lifecycle test from the
@@ -217,7 +226,7 @@
           (^neutron_tempest_plugin.scenario.test_mtu.NetworkWritableMtuTest)"
       devstack_localrc:
         Q_AGENT: ovn
-        NETWORK_API_EXTENSIONS: "{{ network_api_extensions | join(',') }}"
+        NETWORK_API_EXTENSIONS: "{{ (network_api_extensions + network_api_extensions_ovn) | join(',') }}"
         Q_ML2_PLUGIN_MECHANISM_DRIVERS: ovn,logger
         Q_ML2_PLUGIN_TYPE_DRIVERS: local,flat,vlan,geneve
         Q_ML2_TENANT_NETWORK_TYPE: geneve
@@ -228,6 +237,11 @@
         OVN_DBS_LOG_LEVEL: dbg
         ENABLE_TLS: True
         OVN_IGMP_SNOOPING_ENABLE: True
+        # TODO(eolivare): Remove OVN_BUILD_FROM_SOURCE once vlan-transparency
+        # is included in an ovn released version
+        OVN_BUILD_FROM_SOURCE: True
+        OVN_BRANCH: "v20.12.0"
+        OVS_BRANCH: "branch-2.15"
       devstack_services:
         br-ex-tcpdump: true
         br-int-flows: true
@@ -259,6 +273,7 @@
           $NEUTRON_CONF:
             DEFAULT:
               enable_dvr: false
+              vlan_transparent: true
           /$NEUTRON_CORE_PLUGIN_CONF:
             ml2:
               type_drivers: local,flat,vlan,geneve
diff --git a/zuul.d/ussuri_jobs.yaml b/zuul.d/ussuri_jobs.yaml
index b71a460..9cc0621 100644
--- a/zuul.d/ussuri_jobs.yaml
+++ b/zuul.d/ussuri_jobs.yaml
@@ -163,6 +163,7 @@
         OVN_BUILD_MODULES: True
         # TODO(skaplons): v2.13.1 is incompatible with kernel 4.15.0-118, sticking to commit hash until new v2.13 tag is created
         OVS_BRANCH: 0047ca3a0290f1ef954f2c76b31477cf4b9755f5
+        OVN_BRANCH: "v20.03.0"
 
 - job:
     name: neutron-tempest-plugin-dvr-multinode-scenario-ussuri