From 64dcfb7291d0454d52dc8f5a547ae57caa3b628e Mon Sep 17 00:00:00 2001
From: Mark Goddard <mark@stackhpc.com>
Date: Wed, 19 Jun 2024 14:05:32 +0100
Subject: [PATCH] Add support for customising Neutron physical network names

Previously Kolla Ansible hard-coded Neutron physical networks starting
at physnet1 up to physnetN, matching the number of interfaces in
neutron_external_interface and bridges in neutron_bridge_name.

Sometimes we may want to customise the physical network names used. This
may be to allow for not all hosts having access to all physical
networks, or to use more descriptive names.

For example, in an environment with a separate physical network for
Ironic provisioning, controllers might have access to two physical
networks, while compute nodes have access to one.

This change adds a neutron_physical_networks variable, making it
possible to customise the Neutron physical network names used for the
OVS, OVN, Linux bridge and OVS DPDK plugins. The default behaviour is
unchanged.

Change-Id: Ib5b8ea727014964919c6b3bd2352bac4a4ac1787
---
 ansible/group_vars/all.yml                    |  1 +
 .../templates/linuxbridge_agent.ini.j2        |  3 +-
 .../roles/neutron/templates/ml2_conf.ini.j2   |  4 +-
 .../templates/openvswitch_agent.ini.j2        |  3 +-
 .../roles/ovn-controller/tasks/setup-ovs.yml  |  6 ++-
 ansible/roles/ovs-dpdk/defaults/main.yml      |  6 ++-
 doc/source/reference/networking/neutron.rst   | 41 ++++++++++++++++---
 ...on-physical-networks-5b908bed9809c3b4.yaml |  6 +++
 8 files changed, 57 insertions(+), 13 deletions(-)
 create mode 100644 releasenotes/notes/neutron-physical-networks-5b908bed9809c3b4.yaml

diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml
index e8cdca9f8..5b5e417a6 100644
--- a/ansible/group_vars/all.yml
+++ b/ansible/group_vars/all.yml
@@ -1080,6 +1080,7 @@ designate_notifications_topic_name: "notifications_designate"
 #######################
 neutron_bgp_router_id: "1.1.1.1"
 neutron_bridge_name: "{{ 'br-dvs' if neutron_plugin_agent == 'vmware_dvs' else 'br_dpdk' if enable_ovs_dpdk | bool else 'br-ex' }}"
+neutron_physical_networks: "{% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index }}{% if not loop.last %},{% endif %}{% endfor %}"
 # Comma-separated type of enabled ml2 type drivers
 neutron_type_drivers: "flat,vlan,vxlan{% if neutron_plugin_agent == 'ovn' %},geneve{% endif %}"
 # Comma-separated types of tenant networks (should be listed in 'neutron_type_drivers')
diff --git a/ansible/roles/neutron/templates/linuxbridge_agent.ini.j2 b/ansible/roles/neutron/templates/linuxbridge_agent.ini.j2
index 1dbaae0ed..5b0ae990b 100644
--- a/ansible/roles/neutron/templates/linuxbridge_agent.ini.j2
+++ b/ansible/roles/neutron/templates/linuxbridge_agent.ini.j2
@@ -5,7 +5,8 @@ extensions = {{ neutron_agent_extensions|map(attribute='name')|join(',') }}
 
 [linux_bridge]
 {% if inventory_hostname in groups["network"] or (inventory_hostname in groups["compute"] and computes_need_external_bridge | bool ) %}
-physical_interface_mappings = {% for interface in neutron_external_interface.split(',') %}physnet{{ loop.index0 + 1 }}:{{ interface }}{% if not loop.last %},{% endif %}{% endfor %}
+{# Format: physnet1:br1,physnet2:br2 #}
+physical_interface_mappings = {{ neutron_physical_networks.split(',') | zip(neutron_external_interface.split(',')) | map('join', ':') | join(',') }}
 {% endif %}
 
 [securitygroup]
diff --git a/ansible/roles/neutron/templates/ml2_conf.ini.j2 b/ansible/roles/neutron/templates/ml2_conf.ini.j2
index 9c70eb898..0e3447769 100644
--- a/ansible/roles/neutron/templates/ml2_conf.ini.j2
+++ b/ansible/roles/neutron/templates/ml2_conf.ini.j2
@@ -15,7 +15,7 @@ extension_drivers = {{ neutron_extension_drivers | map(attribute='name') | join(
 
 [ml2_type_vlan]
 {% if enable_ironic | bool %}
-network_vlan_ranges = physnet1
+network_vlan_ranges = {{ neutron_physical_networks }}
 {% else %}
 network_vlan_ranges =
 {% endif %}
@@ -24,7 +24,7 @@ network_vlan_ranges =
 {% if enable_ironic | bool %}
 flat_networks = *
 {% else %}
-flat_networks = {% for interface in neutron_external_interface.split(',') %}physnet{{ loop.index0 + 1 }}{% if not loop.last %},{% endif %}{% endfor %}
+flat_networks = {{ neutron_physical_networks }}
 {% endif %}
 
 [ml2_type_vxlan]
diff --git a/ansible/roles/neutron/templates/openvswitch_agent.ini.j2 b/ansible/roles/neutron/templates/openvswitch_agent.ini.j2
index 88834e2de..8ac25af7e 100644
--- a/ansible/roles/neutron/templates/openvswitch_agent.ini.j2
+++ b/ansible/roles/neutron/templates/openvswitch_agent.ini.j2
@@ -15,7 +15,8 @@ firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewal
 
 [ovs]
 {% if inventory_hostname in groups["network"] or (inventory_hostname in groups["compute"] and computes_need_external_bridge | bool ) %}
-bridge_mappings = {% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index0 + 1 }}:{{ bridge }}{% if not loop.last %},{% endif %}{% endfor %}
+{# Format: physnet1:br1,physnet2:br2 #}
+bridge_mappings = {{ neutron_physical_networks.split(',') | zip(neutron_bridge_name.split(',')) | map('join', ':') | join(',') }}
 {% endif %}
 datapath_type = {{ ovs_datapath }}
 ovsdb_connection = tcp:127.0.0.1:{{ ovsdb_port }}
diff --git a/ansible/roles/ovn-controller/tasks/setup-ovs.yml b/ansible/roles/ovn-controller/tasks/setup-ovs.yml
index 5ac61a16b..0d9ab0e30 100644
--- a/ansible/roles/ovn-controller/tasks/setup-ovs.yml
+++ b/ansible/roles/ovn-controller/tasks/setup-ovs.yml
@@ -12,8 +12,10 @@
 
 - name: Configure OVN in OVSDB
   vars:
-    ovn_mappings: "{% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index0 + 1 }}:{{ bridge }}{% if not loop.last %},{% endif %}{% endfor %}"
-    ovn_macs: "{% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index0 + 1 }}:{{ ovn_base_mac | random_mac(seed=inventory_hostname + bridge) }}{% if not loop.last %},{% endif %}{% endfor %}"
+    # Format: physnet1:br1,physnet2:br2
+    ovn_mappings: "{{ neutron_physical_networks.split(',') | zip(neutron_bridge_name.split(',')) | map('join', ':') | join(',') }}"
+    # Format: physnet1:00:11:22:33:44:55,physnet2:00:11:22:33:44:56
+    ovn_macs: "{% for physnet, bridge in neutron_physical_networks.split(',') | zip(neutron_bridge_name.split(',')) %}{{ physnet }}:{{ ovn_base_mac | random_mac(seed=inventory_hostname + bridge) }}{% if not loop.last %},{% endif %}{% endfor %}"
     ovn_cms_opts: "{{ 'enable-chassis-as-gw' if inventory_hostname in groups['ovn-controller-network'] else '' }}{{ ',availability-zones=' + neutron_ovn_availability_zones | join(',') if inventory_hostname in groups['ovn-controller-network'] and neutron_ovn_availability_zones }}"
   become: true
   kolla_toolbox:
diff --git a/ansible/roles/ovs-dpdk/defaults/main.yml b/ansible/roles/ovs-dpdk/defaults/main.yml
index 209eb95fb..ac9cd0c73 100644
--- a/ansible/roles/ovs-dpdk/defaults/main.yml
+++ b/ansible/roles/ovs-dpdk/defaults/main.yml
@@ -37,8 +37,10 @@ ovsdpdk_services:
 ####################
 # OVS
 ####################
-ovs_bridge_mappings: "{% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index0 + 1 }}:{{ bridge }}{% if not loop.last %},{% endif %}{% endfor %}"
-ovs_port_mappings: "{% for bridge in neutron_bridge_name.split(',') %} {{ neutron_external_interface.split(',')[loop.index0] }}:{{ bridge }}{% if not loop.last %},{% endif %}{% endfor %}"
+# Format: physnet1:br1,physnet2:br2
+ovs_bridge_mappings: "{{ neutron_physical_networks.split(',') | zip(neutron_bridge_name.split(',')) | map('join', ':') | join(',') }}"
+# Format: eth1:br1,eth2:br2
+ovs_port_mappings: "{{ neutron_external_interface.split(',') | zip(neutron_bridge_name.split(',')) | map('join', ':') | join(',') }}"
 tunnel_interface_network: "{{ hostvars[inventory_hostname].ansible_facts[dpdk_tunnel_interface]['ipv4']['network'] }}/{{ hostvars[inventory_hostname].ansible_facts[dpdk_tunnel_interface]['ipv4']['netmask'] }}"
 tunnel_interface_cidr: "{{ dpdk_tunnel_interface_address }}/{{ tunnel_interface_network | ipaddr('prefix') }}"
 ovs_cidr_mappings: "{% if neutron_bridge_name.split(',') | length != 1 %} {neutron_bridge_name.split(',')[0]}:{{ tunnel_interface_cidr }} {% else %} {{ neutron_bridge_name }}:{{ tunnel_interface_cidr }} {% endif %}"
diff --git a/doc/source/reference/networking/neutron.rst b/doc/source/reference/networking/neutron.rst
index c748c6d4e..8a627572d 100644
--- a/doc/source/reference/networking/neutron.rst
+++ b/doc/source/reference/networking/neutron.rst
@@ -20,13 +20,20 @@ Neutron external interface is used for communication with the external world,
 for example provider networks, routers and floating IPs.
 For setting up the neutron external interface modify
 ``/etc/kolla/globals.yml`` setting ``neutron_external_interface`` to the
-desired interface name. This interface is used by hosts in the ``network``
-group. It is also used by hosts in the ``compute`` group if
+desired interface name or comma-separated list of interface names. Its default
+value is ``eth1``. These external interfaces are used by hosts in the
+``network`` group.  They are also used by hosts in the ``compute`` group if
 ``enable_neutron_provider_networks`` is set or DVR is enabled.
 
-The interface is plugged into a bridge (Open vSwitch or Linux Bridge, depending
-on the driver) defined by ``neutron_bridge_name``, which defaults to ``br-ex``.
-The default Neutron physical network is ``physnet1``.
+The external interfaces are each plugged into a bridge (Open vSwitch or Linux
+Bridge, depending on the driver) defined by ``neutron_bridge_name``, which
+defaults to ``br-ex``. When there are multiple external interfaces,
+``neutron_bridge_name`` should be a comma-separated list of the same length.
+
+The default Neutron physical network is ``physnet1``, or ``physnet1`` to
+``physnetN`` when there are multiple external network interfaces. This may be
+changed by setting ``neutron_physical_networks`` to a comma-separated list of
+networks of the same length.
 
 Example: single interface
 -------------------------
@@ -54,6 +61,30 @@ These two lists are "zipped" together, such that ``eth1`` is plugged into the
 Ansible maps these interfaces to Neutron physical networks ``physnet1`` and
 ``physnet2`` respectively.
 
+Example: custom physical networks
+---------------------------------
+
+Sometimes we may want to customise the physical network names used. This may be
+to allow for not all hosts having access to all physical networks, or to use
+more descriptive names.
+
+For example, in an environment with a separate physical network for Ironic
+provisioning, controllers might have access to two physical networks:
+
+.. code-block:: yaml
+
+   neutron_external_interface: "eth1,eth2"
+   neutron_bridge_name: "br-ex1,br-ex2"
+   neutron_physical_network: "physnet1,physnet2"
+
+While compute nodes have access only to ``physnet2``.
+
+.. code-block:: yaml
+
+   neutron_external_interface: "eth1"
+   neutron_bridge_name: "br-ex1"
+   neutron_physical_network: "physnet2"
+
 Example: shared interface
 -------------------------
 
diff --git a/releasenotes/notes/neutron-physical-networks-5b908bed9809c3b4.yaml b/releasenotes/notes/neutron-physical-networks-5b908bed9809c3b4.yaml
new file mode 100644
index 000000000..a62e40008
--- /dev/null
+++ b/releasenotes/notes/neutron-physical-networks-5b908bed9809c3b4.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Adds a ``neutron_physical_networks`` variable for customising Neutron
+    physical network names. The default behaviour of using ``physnet1`` to
+    ``physnetN`` is unchanged.
-- 
GitLab