summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--0001-add-distributed-traffic-feature-support.patch2583
-rw-r--r--conf.README9
-rw-r--r--neutron-destroy-patch-ports.service14
-rw-r--r--neutron-dhcp-agent.service14
-rw-r--r--neutron-dist.conf10
-rwxr-xr-xneutron-enable-bridge-firewall.sh19
-rw-r--r--neutron-l2-agent-sysctl.conf2
-rwxr-xr-xneutron-l2-agent.modules4
-rw-r--r--neutron-l3-agent.service14
-rw-r--r--neutron-linuxbridge-agent.service16
-rw-r--r--neutron-linuxbridge-cleanup.service15
-rw-r--r--neutron-macvtap-agent.service14
-rw-r--r--neutron-metadata-agent.service14
-rw-r--r--neutron-metering-agent.service14
-rw-r--r--neutron-netns-cleanup.service15
-rw-r--r--neutron-openvswitch-agent.service18
-rw-r--r--neutron-ovs-cleanup.service16
-rw-r--r--neutron-rpc-server.service15
-rw-r--r--neutron-server.service16
-rw-r--r--neutron-sriov-nic-agent.service14
-rw-r--r--neutron-sudoers4
-rw-r--r--neutron.logrotate7
-rw-r--r--openstack-neutron.spec827
-rw-r--r--sources1
25 files changed, 3676 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index e69de29..b739ee2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1 @@
+/neutron-15.3.4.tar.gz
diff --git a/0001-add-distributed-traffic-feature-support.patch b/0001-add-distributed-traffic-feature-support.patch
new file mode 100644
index 0000000..7c37c36
--- /dev/null
+++ b/0001-add-distributed-traffic-feature-support.patch
@@ -0,0 +1,2583 @@
+Author: wangkuntian <wangkuntian@uniontech.com>
+Date: Fri Oct 13 16:25:17 2023 +0800
+
+ feat: add distributed traffic feature
+---
+ agent/l3/dvr_edge_ha_router.py | 4 +-
+ agent/l3/extensions/rg_port_forwarding.py | 398 ++++++++++++++++++++++++++++
+ agent/l3/ha.py | 9 -
+ agent/l3/ha_router.py | 74 +++---
+ agent/l3/keepalived_state_change.py | 12 +-
+ agent/l3/router_info.py | 19 +-
+ agent/linux/dhcp.py | 137 +++++++++-
+ agent/linux/interface.py | 43 +--
+ agent/linux/keepalived.py | 1 +
+ api/rpc/agentnotifiers/dhcp_rpc_agent_api.py | 9 +
+ api/rpc/callbacks/resources.py | 3 +
+ conf/agent/l3/keepalived.py | 2 +
+ conf/common.py | 16 +-
+ conf/policies/__init__.py | 2 +
+ conf/policies/rg_port_forwarding.py | 76 ++++++
+ db/l3_attrs_db.py | 7 +-
+ db/l3_db.py | 27 +-
+ db/l3_hamode_db.py | 97 +++++++
+ db/migration/alembic_migrations/versions/EXPAND_HEAD | 2 +-
+ .../train/expand/1c19a98b5eef_add_router_configurations.py | 36 +++
+ .../expand/cab12b72ed90_add_router_gateway_port_forwarding.py | 55 ++++
+ db/models/l3_attrs.py | 2 +
+ db/models/rg_port_forwarding.py | 59 +++++
+ extensions/rg_port_forwarding.py | 119 +++++++++
+ objects/rg_port_forwarding.py | 87 ++++++
+ objects/router.py | 15 +-
+ scheduler/l3_agent_scheduler.py | 107 ++++++--
+ services/l3_router/l3_router_plugin.py | 12 +
+ services/rg_portforwarding/__init__.py | 0
+ services/rg_portforwarding/common/__init__.py | 0
+ services/rg_portforwarding/common/exceptions.py | 77 ++++++
+ services/rg_portforwarding/pf_plugin.py | 369 ++++++++++++++++++++++++++
+ 32 files changed, 1749 insertions(+), 127 deletions(-)
+
+diff --git a/agent/l3/dvr_edge_ha_router.py b/agent/l3/dvr_edge_ha_router.py
+index 71f740bef9..b92f70b70f 100644
+--- a/agent/l3/dvr_edge_ha_router.py
++++ b/agent/l3/dvr_edge_ha_router.py
+@@ -114,9 +114,7 @@ class DvrEdgeHaRouter(dvr_edge_router.DvrEdgeRouter,
+
+ def _external_gateway_added(self, ex_gw_port, interface_name,
+ ns_name, preserve_ips):
+- link_up = self.external_gateway_link_up()
+- self._plug_external_gateway(ex_gw_port, interface_name, ns_name,
+- link_up=link_up)
++ self._plug_external_gateway(ex_gw_port, interface_name, ns_name)
+
+ def _is_this_snat_host(self):
+ return self.agent_conf.agent_mode == constants.L3_AGENT_MODE_DVR_SNAT
+diff --git a/agent/l3/extensions/rg_port_forwarding.py b/agent/l3/extensions/rg_port_forwarding.py
+new file mode 100644
+index 0000000000..a159e4df34
+--- /dev/null
++++ b/agent/l3/extensions/rg_port_forwarding.py
+@@ -0,0 +1,398 @@
++# Copyright (c) 2023 UnionTech
++# 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 collections
++from typing import Optional, List
++from oslo_concurrency import lockutils
++from oslo_log import log as logging
++
++from neutron_lib import constants
++from neutron_lib.rpc import Connection
++from neutron_lib.context import Context
++from neutron_lib.agent import l3_extension
++
++from neutron.agent.linux.ip_lib import IPDevice
++from neutron.agent.l3.router_info import RouterInfo
++from neutron.agent.linux.iptables_manager import IptablesManager
++
++from neutron.api.rpc.handlers import resources_rpc
++from neutron.api.rpc.callbacks import resources, events
++from neutron.api.rpc.callbacks.consumer import registry
++
++from neutron.common import coordination
++
++from neutron.objects.ports import Port
++from neutron.objects.router import Router
++from neutron.objects.rg_port_forwarding import RGPortForwarding
++
++LOG = logging.getLogger(__name__)
++
++PORT_FORWARDING_PREFIX = 'rg_portforwarding-'
++DEFAULT_PORT_FORWARDING_CHAIN = 'rg-pf'
++PORT_FORWARDING_CHAIN_PREFIX = 'pf-'
++
++
++def _get_port_forwarding_chain_name(pf_id):
++ chain_name = PORT_FORWARDING_CHAIN_PREFIX + pf_id
++ return chain_name[:constants.MAX_IPTABLES_CHAIN_LEN_WRAP]
++
++
++class RGPortForwardingMapping(object):
++ def __init__(self):
++ self.managed_port_forwardings = {}
++ self.router_pf_mapping = collections.defaultdict(set)
++
++ @lockutils.synchronized('rg-port-forwarding-cache')
++ def check_port_forwarding_changes(self, new_pf: RGPortForwarding) -> bool:
++ old_pf = self.managed_port_forwardings.get(new_pf.id)
++ return old_pf != new_pf
++
++ @lockutils.synchronized('rg-port-forwarding-cache')
++ def set_port_forwardings(self, port_forwardings: List[RGPortForwarding]):
++ for port_forwarding in port_forwardings:
++ self._set_router_port_forwarding(port_forwarding,
++ port_forwarding.router_id)
++
++ def _set_router_port_forwarding(self,
++ port_forwarding: RGPortForwarding,
++ router_id: str):
++ self.router_pf_mapping[router_id].add(port_forwarding.id)
++ self.managed_port_forwardings[port_forwarding.id] = port_forwarding
++
++ @lockutils.synchronized('rg-port-forwarding-cache')
++ def update_port_forwardings(self, port_forwardings):
++ for port_forwarding in port_forwardings:
++ self.managed_port_forwardings[port_forwarding.id] = port_forwarding
++
++ @lockutils.synchronized('rg-port-forwarding-cache')
++ def del_port_forwardings(self, port_forwardings):
++ for port_forwarding in port_forwardings:
++ if not self.managed_port_forwardings.get(port_forwarding.id):
++ continue
++ self.managed_port_forwardings.pop(port_forwarding.id)
++ self.router_pf_mapping[port_forwarding.router_id].discard(
++ port_forwarding.id)
++ if not self.router_pf_mapping[port_forwarding.router_id]:
++ self.router_pf_mapping.pop(port_forwarding.router_id)
++
++ @lockutils.synchronized('rg-port-forwarding-cache')
++ def clean_port_forwardings_by_router_id(self, router_id: str):
++ pf_ids = self.router_pf_mapping.pop(router_id, [])
++ for pf_id in pf_ids:
++ self.managed_port_forwardings.pop(pf_id, None)
++
++
++class RGPortForwardingAgentExtension(l3_extension.L3AgentExtension):
++ SUPPORTED_RESOURCE_TYPES = [resources.RGPORTFORWARDING]
++
++ def consume_api(self, agent_api):
++ self.agent_api = agent_api
++
++ def initialize(self, connection, driver_type):
++ self.mapping = RGPortForwardingMapping()
++ self.resource_rpc = resources_rpc.ResourcesPullRpcApi()
++ self._register_rpc_consumers()
++
++ def _register_rpc_consumers(self):
++ registry.register(self._handle_notification,
++ resources.RGPORTFORWARDING)
++ self._connection = Connection()
++ endpoints = [resources_rpc.ResourcesPushRpcCallback()]
++ topic = resources_rpc.resource_type_versioned_topic(
++ resources.RGPORTFORWARDING)
++ self._connection.create_consumer(topic, endpoints, fanout=True)
++ self._connection.consume_in_threads()
++
++ def _handle_notification(self, context: Context,
++ resource_type: str,
++ forwardings, event_type):
++ for forwarding in forwardings:
++ self._process_port_forwarding_event(
++ context, forwarding, event_type)
++
++ def _get_gw_port_and_ip(self,
++ ri: RouterInfo) -> (Optional[Port], Optional[str]):
++ ex_gw_port = ri.get_ex_gw_port()
++ ex_gw_port_ip = self._get_gw_port_ip(ex_gw_port)
++ if not ex_gw_port_ip:
++ LOG.error(f"Router {ri.router_id} external port "
++ f"{ex_gw_port['id']} does not have any IP addresses")
++ return None, None
++ return ex_gw_port, ex_gw_port_ip
++
++ def _process_port_forwarding_event(self, context: Context,
++ port_forwarding: RGPortForwarding,
++ event_type: str):
++ router_id = port_forwarding.router_id
++ ri = self._get_router_info(router_id)
++ if not self._check_if_need_process(ri, force=True):
++ return
++
++ ex_gw_port, ex_gw_port_ip = self._get_gw_port_and_ip(ri)
++ if not ex_gw_port or not ex_gw_port_ip:
++ return
++
++ (interface_name, namespace,
++ iptables_manager) = self._get_resource_by_router(ri, ex_gw_port)
++
++ if event_type == events.CREATED:
++ self._process_create([port_forwarding], ri, interface_name,
++ ex_gw_port_ip, namespace, iptables_manager)
++ elif event_type == events.UPDATED:
++ self._process_update([port_forwarding], interface_name,
++ ex_gw_port_ip, namespace, iptables_manager)
++ elif event_type == events.DELETED:
++ self._process_delete([port_forwarding], interface_name,
++ ex_gw_port_ip, namespace, iptables_manager)
++
++ def ha_state_change(self, context: Context, data: Router) -> None:
++ pass
++
++ def update_network(self, context: Context, data: dict) -> None:
++ pass
++
++ def add_router(self, context: Context, data: Router) -> None:
++ LOG.info(f"call add_router for {data['id']}")
++ self.process_port_forwarding(context, data)
++
++ def update_router(self, context: Context, data: Router) -> None:
++ LOG.info(f"call update_router for {data['id']}")
++ self.process_port_forwarding(context, data)
++
++ def delete_router(self, context: Context, data: Router) -> None:
++ self.mapping.clean_port_forwardings_by_router_id(data['id'])
++
++ def _get_router_info(self, router_id) -> Optional[RouterInfo]:
++ router_info = self.agent_api.get_router_info(router_id)
++ if router_info:
++ return router_info
++ LOG.debug("Router %s is not managed by this agent. "
++ "It was possibly deleted concurrently.", router_id)
++
++ @staticmethod
++ def _check_if_need_process(ri: RouterInfo, force: bool = False) -> bool:
++ if not ri or not ri.get_ex_gw_port():
++ return False
++
++ if force:
++ return True
++
++ is_distributed = ri.router.get('distributed')
++ agent_mode = ri.agent_conf.agent_mode
++ if (is_distributed and
++ agent_mode in [constants.L3_AGENT_MODE_DVR_NO_EXTERNAL,
++ constants.L3_AGENT_MODE_DVR]):
++ # just support centralized cases
++ return False
++
++ if is_distributed and not ri.snat_namespace.exists():
++ return False
++
++ return True
++
++ def process_port_forwarding(self, context: Context, data: Router):
++ ri = self._get_router_info(data['id'])
++ if not self._check_if_need_process(ri):
++ return
++ self.check_local_port_forwardings(context, ri)
++
++ @staticmethod
++ def _get_gw_port_ip(gw_port: dict) -> Optional[str]:
++ fixed_ips = gw_port.get('fixed_ips', [])
++ if not fixed_ips:
++ return
++ return fixed_ips[0].get('ip_address', None)
++
++ @staticmethod
++ def _get_resource_by_router(ri: RouterInfo, ex_gw_port: dict) -> (
++ str, str, IptablesManager):
++ is_distributed = ri.router.get('distributed')
++ if is_distributed:
++ interface_name = ri.get_snat_external_device_interface_name(
++ ex_gw_port)
++ namespace = ri.snat_namespace.name
++ iptables_manager = ri.snat_iptables_manager
++ else:
++ interface_name = ri.get_external_device_interface_name(ex_gw_port)
++ namespace = ri.ns_name
++ iptables_manager = ri.iptables_manager
++
++ return interface_name, namespace, iptables_manager
++
++ def check_local_port_forwardings(self, context: Context, ri: RouterInfo):
++ pfs = self.resource_rpc.bulk_pull(
++ context, resources.RGPORTFORWARDING,
++ filter_kwargs={'router_id': ri.router_id})
++ if not pfs:
++ return
++ ex_gw_port, ex_gw_port_ip = self._get_gw_port_and_ip(ri)
++ if not ex_gw_port_ip or not ex_gw_port_ip:
++ return
++ (interface_name, namespace,
++ iptables_manager) = self._get_resource_by_router(ri, ex_gw_port)
++ local_pfs = set(self.mapping.managed_port_forwardings.keys())
++ new_pfs = []
++ updated_pfs = []
++ current_pfs = set()
++ for pf in pfs:
++ if pf.id in self.mapping.managed_port_forwardings:
++ if self.mapping.check_port_forwarding_changes(pf):
++ updated_pfs.append(pf)
++ else:
++ new_pfs.append(pf)
++ current_pfs.add(pf.id)
++
++ remove_pf_ids_set = local_pfs - current_pfs
++ remove_pfs = [self.mapping.managed_port_forwardings[pf_id]
++ for pf_id in remove_pf_ids_set]
++
++ self._process_create(new_pfs, ri, interface_name, ex_gw_port_ip,
++ namespace, iptables_manager)
++
++ self._process_update(updated_pfs, interface_name, ex_gw_port_ip,
++ namespace, iptables_manager)
++
++ self._process_delete(remove_pfs, interface_name, ex_gw_port_ip,
++ namespace, iptables_manager)
++
++ @staticmethod
++ def _install_default_rules(iptables_manager: IptablesManager):
++ default_rule = '-j %s-%s' % (iptables_manager.wrap_name,
++ DEFAULT_PORT_FORWARDING_CHAIN)
++ LOG.info(f'Add default chain {DEFAULT_PORT_FORWARDING_CHAIN}')
++ LOG.info(f'Add default rule {default_rule}')
++ iptables_manager.ipv4['nat'].add_chain(DEFAULT_PORT_FORWARDING_CHAIN)
++ iptables_manager.ipv4['nat'].add_rule('PREROUTING', default_rule)
++ iptables_manager.apply()
++
++ @staticmethod
++ def _get_rg_rules(port_forward: RGPortForwarding, wrap_name: str):
++ chain_rule_list = []
++ pf_chain_name = _get_port_forwarding_chain_name(port_forward.id)
++ chain_rule_list.append(
++ (DEFAULT_PORT_FORWARDING_CHAIN, f'-j {wrap_name}-{pf_chain_name}'))
++ gw_ip_address = port_forward.gw_ip_address
++ protocol = port_forward.protocol
++ internal_ip_address = str(port_forward.internal_ip_address)
++ internal_port = port_forward.internal_port
++ external_port = port_forward.external_port
++ chain_rule = (
++ pf_chain_name,
++ f'-d {gw_ip_address}/32 -p {protocol} -m {protocol} '
++ f'--dport {external_port} '
++ f'-j DNAT --to-destination {internal_ip_address}:{internal_port}'
++ )
++ chain_rule_list.append(chain_rule)
++ return chain_rule_list
++
++ def _rule_apply(self,
++ iptables_manager: IptablesManager,
++ port_forwarding: RGPortForwarding,
++ rule_tag: str):
++ iptables_manager.ipv4['nat'].clear_rules_by_tag(rule_tag)
++ if (DEFAULT_PORT_FORWARDING_CHAIN not in
++ iptables_manager.ipv4['nat'].chains):
++ self._install_default_rules(iptables_manager)
++
++ for chain, rule in self._get_rg_rules(port_forwarding,
++ iptables_manager.wrap_name):
++ LOG.info(f'Add router gateway port forwarding '
++ f'rule {rule} in {chain}')
++ if chain not in iptables_manager.ipv4['nat'].chains:
++ iptables_manager.ipv4['nat'].add_chain(chain)
++ iptables_manager.ipv4['nat'].add_rule(chain, rule, tag=rule_tag)
++
++ def _store_local(self, pf_objs: List[RGPortForwarding], event_type: str):
++ if event_type == events.CREATED:
++ self.mapping.set_port_forwardings(pf_objs)
++ elif event_type == events.UPDATED:
++ self.mapping.update_port_forwardings(pf_objs)
++ elif event_type == events.DELETED:
++ self.mapping.del_port_forwardings(pf_objs)
++
++ def _process_create(self,
++ port_forwardings: List[RGPortForwarding],
++ ri: RouterInfo,
++ interface_name: str,
++ interface_ip: str,
++ namespace: str,
++ iptables_manager: IptablesManager):
++ if not port_forwardings:
++ return
++
++ ha_port = ri.router.get(constants.HA_INTERFACE_KEY, None)
++ if ha_port and ha_port['status'] == constants.PORT_STATUS_ACTIVE:
++ ri.enable_keepalived()
++
++ for port_forwarding in port_forwardings:
++ if port_forwarding.id in self.mapping.managed_port_forwardings:
++ LOG.debug("Skip port forwarding %s for create, as it had been "
++ "managed by agent", port_forwarding.id)
++ continue
++ rule_tag = PORT_FORWARDING_PREFIX + port_forwarding.id
++ port_forwarding.gw_ip_address = interface_ip
++ self._rule_apply(iptables_manager, port_forwarding, rule_tag)
++ iptables_manager.apply()
++ self._store_local(port_forwardings, events.CREATED)
++
++ def _process_update(self,
++ port_forwardings: List[RGPortForwarding],
++ interface_name: str,
++ interface_ip: str,
++ namespace: str,
++ iptables_manager: IptablesManager):
++ if not port_forwardings:
++ return
++ device = IPDevice(interface_name, namespace=namespace)
++ for port_forwarding in port_forwardings:
++ # check if port forwarding change from OVO and router rpc
++ if not self.mapping.check_port_forwarding_changes(port_forwarding):
++ LOG.debug("Skip port forwarding %s for update, as there is no "
++ "difference between the memory managed by agent",
++ port_forwarding.id)
++ continue
++ current_chain = _get_port_forwarding_chain_name(port_forwarding.id)
++ iptables_manager.ipv4['nat'].remove_chain(current_chain)
++ ori_pf = self.mapping.managed_port_forwardings[port_forwarding.id]
++ device.delete_socket_conntrack_state(interface_ip,
++ ori_pf.external_port,
++ protocol=ori_pf.protocol)
++ rule_tag = PORT_FORWARDING_PREFIX + port_forwarding.id
++ port_forwarding.gw_ip_address = interface_ip
++ self._rule_apply(iptables_manager, port_forwarding, rule_tag)
++ iptables_manager.apply()
++ self._store_local(port_forwardings, events.UPDATED)
++
++ @coordination.synchronized('router-lock-ns-{namespace}')
++ def _process_delete(self,
++ port_forwardings: List[RGPortForwarding],
++ interface_name: str,
++ interface_ip: str,
++ namespace: str,
++ iptables_manager: IptablesManager):
++ if not port_forwardings:
++ return
++ device = IPDevice(interface_name, namespace=namespace)
++ for port_forwarding in port_forwardings:
++ current_chain = _get_port_forwarding_chain_name(port_forwarding.id)
++ iptables_manager.ipv4['nat'].remove_chain(current_chain)
++ device.delete_socket_conntrack_state(
++ interface_ip,
++ port_forwarding.external_port,
++ protocol=port_forwarding.protocol)
++
++ iptables_manager.apply()
++
++ self._store_local(port_forwardings, events.DELETED)
+diff --git a/agent/l3/ha.py b/agent/l3/ha.py
+index 17891dc983..182fa68175 100644
+--- a/agent/l3/ha.py
++++ b/agent/l3/ha.py
+@@ -163,15 +163,6 @@ class AgentMixin(object):
+ 'agent %(host)s',
+ state_change_data)
+
+- # Set external gateway port link up or down according to state
+- if state == 'master':
+- ri.set_external_gw_port_link_status(link_up=True, set_gw=True)
+- elif state == 'backup':
+- ri.set_external_gw_port_link_status(link_up=False)
+- else:
+- LOG.warning('Router %s has status %s, '
+- 'no action to router gateway device.',
+- router_id, state)
+ # TODO(dalvarez): Fix bug 1677279 by moving the IPv6 parameters
+ # configuration to keepalived-state-change in order to remove the
+ # dependency that currently exists on l3-agent running for the IPv6
+diff --git a/agent/l3/ha_router.py b/agent/l3/ha_router.py
+index 0a21902771..ef10ff76e4 100644
+--- a/agent/l3/ha_router.py
++++ b/agent/l3/ha_router.py
+@@ -17,6 +17,7 @@ import shutil
+ import signal
+
+ import netaddr
++from typing import Optional, List
+ from neutron_lib.api.definitions import portbindings
+ from neutron_lib import constants as n_consts
+ from neutron_lib.utils import runtime
+@@ -137,6 +138,22 @@ class HaRouter(router.RouterInfo):
+ else:
+ return False
+
++ @property
++ def configurations(self) -> Optional[dict]:
++ return self.router.get('configurations', {})
++
++ @property
++ def master(self) -> Optional[str]:
++ if self.configurations:
++ return self.configurations.get('master_agent', None)
++ return None
++
++ @property
++ def slaves(self) -> Optional[List[str]]:
++ if self.configurations:
++ return self.configurations.get('slave_agents', [])
++ return []
++
+ def initialize(self, process_monitor):
+ ha_port = self.router.get(n_consts.HA_INTERFACE_KEY)
+ if not ha_port:
+@@ -162,19 +179,32 @@ class HaRouter(router.RouterInfo):
+ throttle_restart_value=(
+ self.agent_conf.ha_vrrp_advert_int * THROTTLER_MULTIPLIER))
+
++ # The following call is required to ensure that if the state path does
++ # not exist it gets created.
++ self.keepalived_manager.get_full_config_file_path('test')
++
+ config = self.keepalived_manager.config
+
+ interface_name = self.get_ha_device_name()
+ subnets = self.ha_port.get('subnets', [])
+ ha_port_cidrs = [subnet['cidr'] for subnet in subnets]
++ nopreempt = True
++ state = 'BACKUP'
++ priority = self.ha_priority
++ if self.slaves and self.master:
++ nopreempt = False
++ if self.master == self.agent_conf.host:
++ state = 'MASTER'
++ priority = keepalived.HA_DEFAULT_MASTER_PRIORITY
++
+ instance = keepalived.KeepalivedInstance(
+- 'BACKUP',
++ state,
+ interface_name,
+ self.ha_vr_id,
+ ha_port_cidrs,
+- nopreempt=True,
++ nopreempt=nopreempt,
+ advert_int=self.agent_conf.ha_vrrp_advert_int,
+- priority=self.ha_priority,
++ priority=priority,
+ vrrp_health_check_interval=(
+ self.agent_conf.ha_vrrp_health_check_interval),
+ ha_conf_dir=self.keepalived_manager.get_conf_dir())
+@@ -396,13 +426,16 @@ class HaRouter(router.RouterInfo):
+ ha_device = self.get_ha_device_name()
+ ha_cidr = self._get_primary_vip()
+ config_dir = self.keepalived_manager.get_conf_dir()
+- state_change_log = (
+- "%s/neutron-keepalived-state-change.log") % config_dir
++ state_change_log = f"{config_dir}/neutron-keepalived-state-change.log"
+
+ def callback(pid_file):
++ LOG.info(f'Router: {self.router_id} master is {self.master}, '
++ f'salves are {self.slaves}.')
+ cmd = [
+ STATE_CHANGE_PROC_NAME,
+ '--router_id=%s' % self.router_id,
++ '--master_agent=%s' % self.master,
++ '--slave_agents=%s' % ','.join(self.slaves),
+ '--namespace=%s' % self.ha_namespace,
+ '--conf_dir=%s' % config_dir,
+ '--log-file=%s' % state_change_log,
+@@ -453,9 +486,7 @@ class HaRouter(router.RouterInfo):
+ return port1_filtered == port2_filtered
+
+ def external_gateway_added(self, ex_gw_port, interface_name):
+- link_up = self.external_gateway_link_up()
+- self._plug_external_gateway(ex_gw_port, interface_name,
+- self.ns_name, link_up=link_up)
++ self._plug_external_gateway(ex_gw_port, interface_name, self.ns_name)
+ self._add_gateway_vip(ex_gw_port, interface_name)
+ self._disable_ipv6_addressing_on_interface(interface_name)
+
+@@ -519,30 +550,3 @@ class HaRouter(router.RouterInfo):
+ if (self.keepalived_manager.get_process().active and
+ self.ha_state == 'master'):
+ super(HaRouter, self).enable_radvd(internal_ports)
+-
+- def external_gateway_link_up(self):
+- # Check HA router ha_state for its gateway port link state.
+- # 'backup' instance will not link up the gateway port.
+- return self.ha_state == 'master'
+-
+- def set_external_gw_port_link_status(self, link_up, set_gw=False):
+- link_state = "up" if link_up else "down"
+- LOG.info('Set router %s gateway device link state to %s.',
+- self.router_id, link_state)
+-
+- ex_gw_port = self.get_ex_gw_port()
+- ex_gw_port_id = (ex_gw_port and ex_gw_port['id'] or
+- self.ex_gw_port and self.ex_gw_port['id'])
+- if ex_gw_port_id:
+- interface_name = self.get_external_device_name(ex_gw_port_id)
+- ns_name = self.get_gw_ns_name()
+- if (not self.driver.set_link_status(
+- interface_name, namespace=ns_name, link_up=link_up) and
+- link_up):
+- LOG.error('Gateway interface for router %s was not set up; '
+- 'router will not work properly', self.router_id)
+- if link_up and set_gw:
+- preserve_ips = self.get_router_preserve_ips()
+- self._external_gateway_settings(ex_gw_port, interface_name,
+- ns_name, preserve_ips)
+- self.routes_updated([], self.routes)
+diff --git a/agent/l3/keepalived_state_change.py b/agent/l3/keepalived_state_change.py
+index 7fd9e4269e..8c10a8b00f 100644
+--- a/agent/l3/keepalived_state_change.py
++++ b/agent/l3/keepalived_state_change.py
+@@ -47,9 +47,12 @@ class KeepalivedUnixDomainConnection(agent_utils.UnixDomainHTTPConnection):
+
+
+ class MonitorDaemon(daemon.Daemon):
+- def __init__(self, pidfile, router_id, user, group, namespace, conf_dir,
+- interface, cidr):
++ def __init__(self, pidfile, host, router_id, master, slaves, user, group,
++ namespace, conf_dir, interface, cidr):
++ self.host = host
+ self.router_id = router_id
++ self.master = master
++ self.slaves = slaves
+ self.namespace = namespace
+ self.conf_dir = conf_dir
+ self.interface = interface
+@@ -62,6 +65,8 @@ class MonitorDaemon(daemon.Daemon):
+ user=user, group=group)
+
+ def run(self):
++ LOG.debug(f'Router: {self.router_id} master is {self.master}, '
++ f'salves are {self.slaves}.')
+ self._thread_ip_monitor = threading.Thread(
+ target=ip_lib.ip_monitor,
+ args=(self.namespace, self.queue, self.event_stop,
+@@ -169,7 +174,10 @@ def main():
+ keepalived.register_l3_agent_keepalived_opts()
+ configure(cfg.CONF)
+ MonitorDaemon(cfg.CONF.pid_file,
++ cfg.CONF.host,
+ cfg.CONF.router_id,
++ cfg.CONF.master_agent,
++ cfg.CONF.slave_agents,
+ cfg.CONF.user,
+ cfg.CONF.group,
+ cfg.CONF.namespace,
+diff --git a/agent/l3/router_info.py b/agent/l3/router_info.py
+index ea2b488cd2..eabdcc6e54 100644
+--- a/agent/l3/router_info.py
++++ b/agent/l3/router_info.py
+@@ -697,16 +697,14 @@ class RouterInfo(BaseRouterInfo):
+ return [common_utils.ip_to_cidr(ip['floating_ip_address'])
+ for ip in floating_ips]
+
+- def _plug_external_gateway(self, ex_gw_port, interface_name, ns_name,
+- link_up=True):
++ def _plug_external_gateway(self, ex_gw_port, interface_name, ns_name):
+ self.driver.plug(ex_gw_port['network_id'],
+ ex_gw_port['id'],
+ interface_name,
+ ex_gw_port['mac_address'],
+ namespace=ns_name,
+ prefix=EXTERNAL_DEV_PREFIX,
+- mtu=ex_gw_port.get('mtu'),
+- link_up=link_up)
++ mtu=ex_gw_port.get('mtu'))
+
+ def _get_external_gw_ips(self, ex_gw_port):
+ gateway_ips = []
+@@ -766,11 +764,7 @@ class RouterInfo(BaseRouterInfo):
+ LOG.debug("External gateway added: port(%s), interface(%s), ns(%s)",
+ ex_gw_port, interface_name, ns_name)
+ self._plug_external_gateway(ex_gw_port, interface_name, ns_name)
+- self._external_gateway_settings(ex_gw_port, interface_name,
+- ns_name, preserve_ips)
+
+- def _external_gateway_settings(self, ex_gw_port, interface_name,
+- ns_name, preserve_ips):
+ # Build up the interface and gateway IP addresses that
+ # will be added to the interface.
+ ip_cidrs = common_utils.fixed_ip_cidrs(ex_gw_port['fixed_ips'])
+@@ -815,19 +809,18 @@ class RouterInfo(BaseRouterInfo):
+ return any(netaddr.IPAddress(gw_ip).version == 6
+ for gw_ip in gateway_ips)
+
+- def get_router_preserve_ips(self):
++ def external_gateway_added(self, ex_gw_port, interface_name):
+ preserve_ips = self._list_floating_ip_cidrs() + list(
+ self.centralized_port_forwarding_fip_set)
+ preserve_ips.extend(self.agent.pd.get_preserve_ips(self.router_id))
+- return preserve_ips
+
+- def external_gateway_added(self, ex_gw_port, interface_name):
+- preserve_ips = self.get_router_preserve_ips()
+ self._external_gateway_added(
+ ex_gw_port, interface_name, self.ns_name, preserve_ips)
+
+ def external_gateway_updated(self, ex_gw_port, interface_name):
+- preserve_ips = self.get_router_preserve_ips()
++ preserve_ips = self._list_floating_ip_cidrs() + list(
++ self.centralized_port_forwarding_fip_set)
++ preserve_ips.extend(self.agent.pd.get_preserve_ips(self.router_id))
+ self._external_gateway_added(
+ ex_gw_port, interface_name, self.ns_name, preserve_ips)
+
+diff --git a/agent/linux/dhcp.py b/agent/linux/dhcp.py
+index 249e1a8199..5b82fe328f 100644
+--- a/agent/linux/dhcp.py
++++ b/agent/linux/dhcp.py
+@@ -19,6 +19,7 @@ import os
+ import re
+ import shutil
+ import time
++from typing import List
+
+ import netaddr
+ from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext
+@@ -40,6 +41,7 @@ from neutron.agent.linux import iptables_manager
+ from neutron.cmd import runtime_checks as checks
+ from neutron.common import ipv6_utils
+ from neutron.common import utils as common_utils
++from neutron.conf.common import NETWORK_HOST_OPTS
+ from neutron.ipam import utils as ipam_utils
+
+ LOG = logging.getLogger(__name__)
+@@ -81,6 +83,51 @@ def port_requires_dhcp_configuration(port):
+ constants.DEVICE_OWNER_DHCP]
+
+
++class Octopus(object):
++ def __init__(self):
++ self.tentacles = collections.defaultdict(Tentacle)
++
++ def __str__(self):
++ lines = ['']
++ for subnet_id, tentacle in self.tentacles.items():
++ line = (f"Subnet {subnet_id} has multi routers "
++ f"{len(tentacle.gateway_ports) > 1} "
++ f"{tentacle}")
++ lines.append(line)
++ return '\n'.join(lines)
++
++
++class Tentacle(object):
++ def __init__(self, gateway_ip: str):
++ self.gateway_ip = gateway_ip
++ self.tags = []
++ self.gateway_ports = collections.defaultdict(Sucker)
++ self.suckers = collections.defaultdict(Sucker)
++
++ def __str__(self):
++ lines = [""]
++ for port_id, sucker in self.suckers.items():
++ line = f" Port {port_id} {sucker}"
++ lines.append(line)
++ return '\n'.join(lines)
++
++
++class Sucker(object):
++ def __init__(self, host: str, device_owner: str, ip_address: str):
++ self.host = host
++ self.device_owner = device_owner
++ self.ip_address = ip_address
++ self.tag = None
++
++ def subnet_tag(self, subnet_id: str):
++ return f'{self.host}-subnet-{subnet_id}'
++
++ def __str__(self):
++ return (f"ip: {self.ip_address} \t"
++ f"binding_host: {self.host} \t"
++ f"device_owner: {self.device_owner}")
++
++
+ class DictModel(dict):
+ """Convert dict into an object that provides attribute access to values."""
+
+@@ -149,6 +196,10 @@ class DhcpBase(object):
+ self.process_monitor = process_monitor
+ self.device_manager = DeviceManager(self.conf, plugin)
+ self.version = version
++ self.octopus = Octopus()
++ self._init_octopus()
++ self.compute_to_network = dict()
++ self._init_compute_to_network()
+
+ @abc.abstractmethod
+ def enable(self):
+@@ -193,6 +244,31 @@ class DhcpBase(object):
+ """True if the metadata-proxy should be enabled for the network."""
+ raise NotImplementedError()
+
++ def _init_compute_to_network(self):
++ for network_node in self.conf.network_nodes:
++ self.conf.register_opts(NETWORK_HOST_OPTS, group=network_node)
++ network_group = self.conf.get(network_node, None)
++ if network_group:
++ compute_nodes = network_group.get('compute_nodes', [])
++ for compute_node in compute_nodes:
++ self.compute_to_network[compute_node] = network_node
++
++ def _init_octopus(self):
++ for subnet in self.network.subnets:
++ self.octopus.tentacles[subnet.id] = Tentacle(subnet.gateway_ip)
++
++ for port in self.network.ports:
++ for ip in port.fixed_ips:
++ host = port.get('binding:host_id')
++ device_owner = port.get('device_owner')
++ ip_address = ip.get('ip_address')
++ tentacle = self.octopus.tentacles[ip.subnet_id]
++ sucker = Sucker(host, device_owner, ip_address)
++ tentacle.suckers[port.id] = sucker
++ if (device_owner in (constants.DEVICE_OWNER_HA_REPLICATED_INT,
++ constants.DEVICE_OWNER_ROUTER_INTF)):
++ tentacle.gateway_ports[host] = sucker
++
+
+ @six.add_metaclass(abc.ABCMeta)
+ class DhcpLocalProcess(DhcpBase):
+@@ -841,8 +917,18 @@ class Dnsmasq(DhcpLocalProcess):
+ (port.mac_address, tag, name, ip_address,
+ 'set:', self._PORT_TAG_PREFIX % port.id))
+ else:
+- buf.write('%s,%s%s,%s\n' %
+- (port.mac_address, tag, name, ip_address))
++ subnet_tag = f'subnet-{alloc.subnet_id}'
++ if self.conf.enable_set_route_for_single_port:
++ tentacle = self.octopus.tentacles[alloc.subnet_id]
++ sucker = tentacle.suckers[port.id]
++ tentacle.tags.append(subnet_tag)
++ if sucker.device_owner.startswith(
++ constants.DEVICE_OWNER_COMPUTE_PREFIX):
++ subnet_tag = sucker.subnet_tag(alloc.subnet_id)
++ sucker.tag = subnet_tag
++
++ buf.write(f'{port.mac_address},{tag}{name},{ip_address},'
++ f'set:{subnet_tag}\n')
+
+ file_utils.replace_file(filename, buf.getvalue())
+ LOG.debug('Done building host file %s', filename)
+@@ -1059,7 +1145,8 @@ class Dnsmasq(DhcpLocalProcess):
+ """Write a dnsmasq compatible options file."""
+ options, subnet_index_map = self._generate_opts_per_subnet()
+ options += self._generate_opts_per_port(subnet_index_map)
+-
++ if self.conf.enable_set_route_for_single_port:
++ options += self._generate_opts_for_compute_port(options)
+ name = self.get_conf_file_name('opts')
+ file_utils.replace_file(name, '\n'.join(options))
+ return name
+@@ -1220,6 +1307,50 @@ class Dnsmasq(DhcpLocalProcess):
+ vx_ips))))
+ return options
+
++ def _generate_opts_for_compute_port(self, options: List[str]) -> List[str]:
++ new_options = []
++ LOG.debug(self.octopus)
++ if not self.compute_to_network:
++ LOG.warning('CONF.enable_set_route_for_single_port is True, '
++ 'but not configured.')
++ return new_options
++ for subnet_id, tentacle in self.octopus.tentacles.items():
++ if len(tentacle.tags) == 0:
++ continue
++ if len(tentacle.gateway_ports) <= 1:
++ LOG.info(f'Subnet {subnet_id} is not bound '
++ f'to different routers, '
++ f'so skip generate options for compute ports.')
++ continue
++ for port_id, sucker in tentacle.suckers.items():
++ if not sucker.tag:
++ continue
++ if not sucker.device_owner.startswith(
++ constants.DEVICE_OWNER_COMPUTE_PREFIX):
++ continue
++ network_node = self.compute_to_network.get(sucker.host, None)
++ if not network_node:
++ LOG.warning(f'Compute host {sucker.host} not configured.')
++ continue
++ port = tentacle.gateway_ports.get(network_node, None)
++ if not port:
++ LOG.warning(f'Subnet {subnet_id} does not have gateway '
++ f'port on network host {network_node}.')
++ continue
++ if tentacle.gateway_ip == port.ip_address:
++ continue
++ for option in options.copy():
++ if subnet_id in option:
++ option = option.replace(f'subnet-{subnet_id}',
++ sucker.tag)
++ if ('option:classless-static-route' in option or
++ ',249,' in option or 'option:router' in option):
++ gateway_ip = option.split(',')[-1]
++ option = option.replace(gateway_ip,
++ port.ip_address)
++ new_options.append(option)
++ return new_options
++
+ def _make_subnet_interface_ip_map(self):
+ subnet_lookup = dict(
+ (netaddr.IPNetwork(subnet.cidr), subnet.id)
+diff --git a/agent/linux/interface.py b/agent/linux/interface.py
+index 3ac476d7ba..2e6455707c 100644
+--- a/agent/linux/interface.py
++++ b/agent/linux/interface.py
+@@ -259,17 +259,16 @@ class LinuxInterfaceDriver(object):
+
+ @abc.abstractmethod
+ def plug_new(self, network_id, port_id, device_name, mac_address,
+- bridge=None, namespace=None, prefix=None, mtu=None,
+- link_up=True):
++ bridge=None, namespace=None, prefix=None, mtu=None):
+ """Plug in the interface only for new devices that don't exist yet."""
+
+ def plug(self, network_id, port_id, device_name, mac_address,
+- bridge=None, namespace=None, prefix=None, mtu=None, link_up=True):
++ bridge=None, namespace=None, prefix=None, mtu=None):
+ if not ip_lib.device_exists(device_name,
+ namespace=namespace):
+ self._safe_plug_new(
+ network_id, port_id, device_name, mac_address, bridge,
+- namespace, prefix, mtu, link_up)
++ namespace, prefix, mtu)
+ else:
+ LOG.info("Device %s already exists", device_name)
+ if mtu:
+@@ -279,11 +278,11 @@ class LinuxInterfaceDriver(object):
+ LOG.warning("No MTU configured for port %s", port_id)
+
+ def _safe_plug_new(self, network_id, port_id, device_name, mac_address,
+- bridge=None, namespace=None, prefix=None, mtu=None, link_up=True):
++ bridge=None, namespace=None, prefix=None, mtu=None):
+ try:
+ self.plug_new(
+ network_id, port_id, device_name, mac_address, bridge,
+- namespace, prefix, mtu, link_up)
++ namespace, prefix, mtu)
+ except TypeError:
+ LOG.warning("Interface driver's plug_new() method should now "
+ "accept additional optional parameter 'link_up'. "
+@@ -321,27 +320,10 @@ class LinuxInterfaceDriver(object):
+ LOG.warning("Interface driver cannot update MTU for ports")
+ self._mtu_update_warn_logged = True
+
+- def set_link_status(self, device_name, namespace=None, link_up=True):
+- ns_dev = ip_lib.IPWrapper(namespace=namespace).device(device_name)
+- try:
+- utils.wait_until_true(ns_dev.exists, timeout=3)
+- except utils.WaitTimeout:
+- LOG.debug('Device %s may have been deleted concurrently',
+- device_name)
+- return False
+-
+- if link_up:
+- ns_dev.link.set_up()
+- else:
+- ns_dev.link.set_down()
+-
+- return True
+-
+
+ class NullDriver(LinuxInterfaceDriver):
+ def plug_new(self, network_id, port_id, device_name, mac_address,
+- bridge=None, namespace=None, prefix=None, mtu=None,
+- link_up=True):
++ bridge=None, namespace=None, prefix=None, mtu=None):
+ pass
+
+ def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
+@@ -377,8 +359,7 @@ class OVSInterfaceDriver(LinuxInterfaceDriver):
+ ovs.replace_port(device_name, *attrs)
+
+ def plug_new(self, network_id, port_id, device_name, mac_address,
+- bridge=None, namespace=None, prefix=None, mtu=None,
+- link_up=True):
++ bridge=None, namespace=None, prefix=None, mtu=None):
+ """Plug in the interface."""
+ if not bridge:
+ bridge = self.conf.ovs_integration_bridge
+@@ -442,8 +423,8 @@ class OVSInterfaceDriver(LinuxInterfaceDriver):
+ else:
+ LOG.warning("No MTU configured for port %s", port_id)
+
+- if link_up:
+- ns_dev.link.set_up()
++ ns_dev.link.set_up()
++
+ if self.conf.ovs_use_veth:
+ # ovs-dpdk does not do checksum calculations for veth interface
+ # (bug 1832021)
+@@ -488,8 +469,7 @@ class BridgeInterfaceDriver(LinuxInterfaceDriver):
+ DEV_NAME_PREFIX = 'ns-'
+
+ def plug_new(self, network_id, port_id, device_name, mac_address,
+- bridge=None, namespace=None, prefix=None, mtu=None,
+- link_up=True):
++ bridge=None, namespace=None, prefix=None, mtu=None):
+ """Plugin the interface."""
+ ip = ip_lib.IPWrapper()
+
+@@ -508,8 +488,7 @@ class BridgeInterfaceDriver(LinuxInterfaceDriver):
+ LOG.warning("No MTU configured for port %s", port_id)
+
+ root_veth.link.set_up()
+- if link_up:
+- ns_veth.link.set_up()
++ ns_veth.link.set_up()
+
+ def unplug(self, device_name, bridge=None, namespace=None, prefix=None):
+ """Unplug the interface."""
+diff --git a/agent/linux/keepalived.py b/agent/linux/keepalived.py
+index f47a27f1d1..405a781f0b 100644
+--- a/agent/linux/keepalived.py
++++ b/agent/linux/keepalived.py
+@@ -32,6 +32,7 @@ from neutron.common import utils
+ VALID_STATES = ['MASTER', 'BACKUP']
+ VALID_AUTH_TYPES = ['AH', 'PASS']
+ HA_DEFAULT_PRIORITY = 50
++HA_DEFAULT_MASTER_PRIORITY = 100
+ PRIMARY_VIP_RANGE_SIZE = 24
+ KEEPALIVED_SERVICE_NAME = 'keepalived'
+ KEEPALIVED_EMAIL_FROM = 'neutron@openstack.local'
+diff --git a/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py b/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py
+index 29b61b2c9b..fbad5f1d9a 100644
+--- a/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py
++++ b/api/rpc/agentnotifiers/dhcp_rpc_agent_api.py
+@@ -312,6 +312,15 @@ class DhcpAgentNotifyAPI(object):
+ return False
+ if set(orig.keys()) != set(new.keys()):
+ return False
++
++ if cfg.CONF.enable_set_route_for_single_port:
++ device_owner = new.get('device_owner', None)
++ orig_device_owner = orig.get('device_owner', None)
++ if (not orig_device_owner and device_owner and
++ device_owner.startswith(
++ constants.DEVICE_OWNER_COMPUTE_PREFIX)):
++ return True
++
+ for k in orig.keys():
+ if k in ('status', 'updated_at', 'revision_number'):
+ continue
+diff --git a/api/rpc/callbacks/resources.py b/api/rpc/callbacks/resources.py
+index 734f05eb6f..de56211c53 100644
+--- a/api/rpc/callbacks/resources.py
++++ b/api/rpc/callbacks/resources.py
+@@ -15,6 +15,7 @@ from neutron.objects import conntrack_helper
+ from neutron.objects.logapi import logging_resource as log_object
+ from neutron.objects import network
+ from neutron.objects import port_forwarding
++from neutron.objects import rg_port_forwarding
+ from neutron.objects import ports
+ from neutron.objects.qos import policy
+ from neutron.objects import securitygroup
+@@ -33,6 +34,7 @@ SUBNET = subnet.Subnet.obj_name()
+ SECURITYGROUP = securitygroup.SecurityGroup.obj_name()
+ SECURITYGROUPRULE = securitygroup.SecurityGroupRule.obj_name()
+ PORTFORWARDING = port_forwarding.PortForwarding.obj_name()
++RGPORTFORWARDING = rg_port_forwarding.RGPortForwarding.obj_name()
+ CONNTRACKHELPER = conntrack_helper.ConntrackHelper.obj_name()
+
+
+@@ -47,6 +49,7 @@ _VALID_CLS = (
+ securitygroup.SecurityGroupRule,
+ log_object.Log,
+ port_forwarding.PortForwarding,
++ rg_port_forwarding.RGPortForwarding,
+ conntrack_helper.ConntrackHelper,
+ )
+
+diff --git a/conf/agent/l3/keepalived.py b/conf/agent/l3/keepalived.py
+index bd46c723fc..5ff3492280 100644
+--- a/conf/agent/l3/keepalived.py
++++ b/conf/agent/l3/keepalived.py
+@@ -20,6 +20,8 @@ from neutron._i18n import _
+
+ CLI_OPTS = [
+ cfg.StrOpt('router_id', help=_('ID of the router')),
++ cfg.StrOpt('master_agent', help=_('The master agent of router')),
++ cfg.ListOpt('slave_agents', help=_('The slave agents of router')),
+ cfg.StrOpt('namespace', help=_('Namespace of the router')),
+ cfg.StrOpt('conf_dir', help=_('Path to the router directory')),
+ cfg.StrOpt('monitor_interface', help=_('Interface to monitor')),
+diff --git a/conf/common.py b/conf/common.py
+index f885429613..45e0b48723 100644
+--- a/conf/common.py
++++ b/conf/common.py
+@@ -145,7 +145,21 @@ core_opts = [
+ "Setting to any positive integer means that on failure "
+ "the connection is retried that many times. "
+ "For example, setting to 3 means total attempts to "
+- "connect will be 4."))
++ "connect will be 4.")),
++ cfg.BoolOpt('enable_set_route_for_single_port', default=False,
++ help=_("To set route path for every single port "
++ "when the same subnet has multi ports on router.")),
++ cfg.ListOpt('network_nodes',
++ default=[],
++ help=_("The list of network hosts to "
++ "make a network map "
++ "with compute node and network node.")),
++]
++
++NETWORK_HOST_OPTS = [
++ cfg.ListOpt('compute_nodes',
++ default=[],
++ help=_("The list of compute hosts."))
+ ]
+
+ core_cli_opts = [
+diff --git a/conf/policies/__init__.py b/conf/policies/__init__.py
+index aa4dda63d0..15cdaea45a 100644
+--- a/conf/policies/__init__.py
++++ b/conf/policies/__init__.py
+@@ -34,6 +34,7 @@ from neutron.conf.policies import port
+ from neutron.conf.policies import qos
+ from neutron.conf.policies import rbac
+ from neutron.conf.policies import router
++from neutron.conf.policies import rg_port_forwarding
+ from neutron.conf.policies import security_group
+ from neutron.conf.policies import segment
+ from neutron.conf.policies import service_type
+@@ -63,6 +64,7 @@ def list_rules():
+ qos.list_rules(),
+ rbac.list_rules(),
+ router.list_rules(),
++ rg_port_forwarding.list_rules(),
+ security_group.list_rules(),
+ segment.list_rules(),
+ service_type.list_rules(),
+diff --git a/conf/policies/rg_port_forwarding.py b/conf/policies/rg_port_forwarding.py
+new file mode 100644
+index 0000000000..19e2cd5e2f
+--- /dev/null
++++ b/conf/policies/rg_port_forwarding.py
+@@ -0,0 +1,76 @@
++# Copyright (c) 2023 UnionTech
++# 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_policy import policy
++from neutron.conf.policies import base
++
++COLLECTION_PATH = '/routers/{router_id}/port_forwardings'
++RESOURCE_PATH = '/routers/{router_id}/port_forwardings/{port_forwarding_id}'
++
++rules = [
++ policy.DocumentedRuleDefault(
++ 'create_router_gateway_port_forwarding',
++ base.RULE_ADMIN_OR_PARENT_OWNER,
++ 'Create a router gateway port forwarding',
++ [
++ {
++ 'method': 'POST',
++ 'path': COLLECTION_PATH,
++ },
++ ]
++ ),
++ policy.DocumentedRuleDefault(
++ 'get_router_gateway_forwarding',
++ base.RULE_ADMIN_OR_PARENT_OWNER,
++ 'Get a router gateway port forwarding',
++ [
++ {
++ 'method': 'GET',
++ 'path': COLLECTION_PATH,
++ },
++ {
++ 'method': 'GET',
++ 'path': RESOURCE_PATH,
++ },
++ ]
++ ),
++ policy.DocumentedRuleDefault(
++ 'update_router_gateway_port_forwarding',
++ base.RULE_ADMIN_OR_PARENT_OWNER,
++ 'Update a floating IP port forwarding',
++ [
++ {
++ 'method': 'PUT',
++ 'path': RESOURCE_PATH,
++ },
++ ]
++ ),
++ policy.DocumentedRuleDefault(
++ 'delete_router_gateway_port_forwarding',
++ base.RULE_ADMIN_OR_PARENT_OWNER,
++ 'Delete a floating IP port forwarding',
++ [
++ {
++ 'method': 'DELETE',
++ 'path': RESOURCE_PATH,
++ },
++ ]
++ ),
++]
++
++
++def list_rules():
++ return rules
+diff --git a/db/l3_attrs_db.py b/db/l3_attrs_db.py
+index e6d4e298b1..f292b7aa32 100644
+--- a/db/l3_attrs_db.py
++++ b/db/l3_attrs_db.py
+@@ -19,6 +19,7 @@ from oslo_config import cfg
+
+ from neutron._i18n import _
+ from neutron.db.models import l3_attrs
++from neutron.objects.base import NeutronDbObject
+
+
+ def get_attr_info():
+@@ -29,7 +30,11 @@ def get_attr_info():
+ 'availability_zone_hints': {
+ 'default': '[]',
+ 'transform_to_db': az_validator.convert_az_list_to_string,
+- 'transform_from_db': az_validator.convert_az_string_to_list}
++ 'transform_from_db': az_validator.convert_az_string_to_list},
++ 'configurations': {
++ 'default': '{}',
++ 'transform_to_db': NeutronDbObject.filter_to_json_str,
++ 'transform_from_db': NeutronDbObject.load_json_from_str}
+ }
+
+
+diff --git a/db/l3_db.py b/db/l3_db.py
+index 565b422532..b625dc1959 100644
+--- a/db/l3_db.py
++++ b/db/l3_db.py
+@@ -47,6 +47,7 @@ from neutron.common import ipv6_utils
+ from neutron.common import utils
+ from neutron.db import _utils as db_utils
+ from neutron.db.models import l3 as l3_models
++from neutron.plugins.ml2 import models as ml2_models
+ from neutron.db import models_v2
+ from neutron.db import standardattrdescription_db as st_attr
+ from neutron.extensions import l3
+@@ -1086,21 +1087,37 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
+ # with subnet's gateway-ip, return that router.
+ # Otherwise return the first router.
+ RouterPort = l3_models.RouterPort
++ RouterPortBinding = orm.aliased(ml2_models.PortBinding,
++ name="router_port_binding")
++ ComputePortBinding = orm.aliased(ml2_models.PortBinding,
++ name="compute_port_binding")
+ gw_port = orm.aliased(models_v2.Port, name="gw_port")
+ # TODO(lujinluo): Need IPAllocation and Port object
+ routerport_qry = context.session.query(
+- RouterPort.router_id, models_v2.IPAllocation.ip_address).join(
+- RouterPort.port, models_v2.Port.fixed_ips).filter(
++ RouterPort.router_id, models_v2.IPAllocation.ip_address,
++ RouterPortBinding.host, ComputePortBinding.host,
++ ).join(
++ RouterPort.port, models_v2.Port.fixed_ips
++ ).filter(
+ models_v2.Port.network_id == internal_port['network_id'],
+ RouterPort.port_type.in_(constants.ROUTER_INTERFACE_OWNERS),
+- models_v2.IPAllocation.subnet_id == internal_subnet['id']
+- ).join(gw_port, gw_port.device_id == RouterPort.router_id).filter(
++ models_v2.IPAllocation.subnet_id == internal_subnet['id'],
++ ComputePortBinding.port_id == internal_port['id'],
++ ).join(
++ gw_port, gw_port.device_id == RouterPort.router_id
++ ).filter(
+ gw_port.network_id == external_network_id,
+ gw_port.device_owner == DEVICE_OWNER_ROUTER_GW
++ ).join(
++ RouterPortBinding, RouterPortBinding.port_id == models_v2.Port.id
+ ).distinct()
+
+ first_router_id = None
+- for router_id, interface_ip in routerport_qry:
++ for (router_id, interface_ip,
++ network_host, compute_host) in routerport_qry:
++ network_node = self.compute_to_network.get(compute_host, None)
++ if network_node and network_node == network_host:
++ return router_id
+ if interface_ip == internal_subnet['gateway_ip']:
+ return router_id
+ if not first_router_id:
+diff --git a/db/l3_hamode_db.py b/db/l3_hamode_db.py
+index bff388e166..4c414016dd 100644
+--- a/db/l3_hamode_db.py
++++ b/db/l3_hamode_db.py
+@@ -32,6 +32,7 @@ from neutron_lib import constants
+ from neutron_lib.db import api as db_api
+ from neutron_lib import exceptions as n_exc
+ from neutron_lib.exceptions import l3 as l3_exc
++from neutron_lib.exceptions import agent as agent_exc
+ from neutron_lib.exceptions import l3_ext_ha_mode as l3ha_exc
+ from neutron_lib.objects import exceptions as obj_base
+ from neutron_lib.plugins import utils as p_utils
+@@ -54,6 +55,7 @@ from neutron.db import l3_dvr_db
+ from neutron.objects import base
+ from neutron.objects import l3_hamode
+ from neutron.objects import router as l3_obj
++from neutron.objects import agent as agent_obj
+
+
+ VR_ID_RANGE = set(range(1, 255))
+@@ -378,12 +380,85 @@ class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin,
+ if not self.get_ha_network(context, router['tenant_id']):
+ self._create_ha_network(context, router['tenant_id'])
+
++ @staticmethod
++ def _check_router_configurations_creation(context,
++ configurations: dict,
++ is_ha: bool = True):
++ agents = agent_obj.Agent.get_objects(context,
++ binary='neutron-l3-agent')
++ agents = [agent['host'] for agent in agents]
++ master = configurations.get('master_agent', None)
++ slaves = configurations.get('slave_agents', [])
++ preferred_agent = configurations.get('preferred_agent', None)
++ if is_ha:
++ if master and slaves:
++ for agent in [master] + slaves:
++ if agent not in agents:
++ raise agent_exc.AgentNotFound(id=agent)
++ if master in slaves:
++ raise l3_exc.RouterAgentConflict()
++ else:
++ if master or slaves:
++ raise l3_exc.RouterAgentNotGiven()
++ else:
++ if preferred_agent and preferred_agent not in agents:
++ raise agent_exc.AgentNotFound(id=preferred_agent)
++
++ @staticmethod
++ def _check_router_configurations_update(context,
++ configurations: dict,
++ old_configurations: dict,
++ is_ha: bool = True) -> bool:
++ if configurations == old_configurations:
++ return False
++
++ agents = agent_obj.Agent.get_objects(context,
++ binary='neutron-l3-agent')
++ agents = [agent['host'] for agent in agents]
++ master = configurations.get('master_agent', None)
++ slaves = configurations.get('slave_agents', [])
++ preferred_agent = configurations.get('preferred_agent', None)
++ old_master = old_configurations.get('master_agent', None)
++ old_slaves = old_configurations.get('slave_agents', [])
++ old_preferred_agent = old_configurations.get('preferred_agent', None)
++ if is_ha:
++ if master:
++ if master != old_master:
++ if master not in agents:
++ raise agent_exc.AgentNotFound(id=master)
++ old_configurations['master_agent'] = master
++ if slaves:
++ if slaves != old_slaves:
++ for slave in slaves:
++ if slave not in agents:
++ raise agent_exc.AgentNotFound(id=slave)
++ old_configurations['slave_agents'] = slaves
++ if (old_configurations['master_agent'] in
++ old_configurations['slave_agents']):
++ raise l3_exc.RouterAgentConflict()
++ else:
++ if preferred_agent:
++ if preferred_agent != old_preferred_agent:
++ if preferred_agent not in agents:
++ raise agent_exc.AgentNotFound(id=preferred_agent)
++ old_configurations['preferred_agent'] = preferred_agent
++ else:
++ old_configurations['preferred_agent'] = None
++
++ return True
++
+ @registry.receives(resources.ROUTER, [events.PRECOMMIT_CREATE],
+ priority_group.PRIORITY_ROUTER_EXTENDED_ATTRIBUTE)
+ def _precommit_router_create(self, resource, event, trigger, context,
+ router, router_db, **kwargs):
+ """Event handler to set ha flag and status on creation."""
+ is_ha = self._is_ha(router)
++ configurations = router.get('configurations', {})
++ if configurations:
++ self._check_router_configurations_creation(context, configurations,
++ is_ha)
++ self.set_extra_attr_value(context, router_db, 'configurations',
++ configurations)
+ router['ha'] = is_ha
+ self.set_extra_attr_value(context, router_db, 'ha', is_ha)
+ if not is_ha:
+@@ -465,6 +540,28 @@ class L3_HA_NAT_db_mixin(l3_dvr_db.L3_NAT_with_dvr_db_mixin,
+ self.set_extra_attr_value(
+ payload.context, payload.desired_state, 'ha', requested_ha_state)
+
++ @registry.receives(resources.ROUTER, [events.PRECOMMIT_UPDATE],
++ priority_group.PRIORITY_ROUTER_EXTENDED_ATTRIBUTE)
++ def _validate_configurations(self, resource, event, trigger, payload=None):
++ old_configurations = payload.states[0].get('configurations', {})
++ configurations = payload.request_body.get('configurations', {})
++
++ if not configurations:
++ return
++
++ if payload.desired_state.admin_state_up:
++ msg = _('Cannot change configurations of active routers. Please '
++ 'set router admin_state_up to False prior to upgrade')
++ raise n_exc.BadRequest(resource='router', msg=msg)
++
++ need_update = self._check_router_configurations_update(
++ payload.context, configurations, old_configurations,
++ payload.states[0]['ha'])
++
++ if need_update:
++ self.set_extra_attr_value(payload.context, payload.desired_state,
++ 'configurations', old_configurations)
++
+ @registry.receives(resources.ROUTER, [events.AFTER_UPDATE],
+ priority_group.PRIORITY_ROUTER_EXTENDED_ATTRIBUTE)
+ def _reconfigure_ha_resources(self, resource, event, trigger, context,
+diff --git a/db/migration/alembic_migrations/versions/EXPAND_HEAD b/db/migration/alembic_migrations/versions/EXPAND_HEAD
+index ffa2bbaaf6..0c8e4a2178 100644
+--- a/db/migration/alembic_migrations/versions/EXPAND_HEAD
++++ b/db/migration/alembic_migrations/versions/EXPAND_HEAD
+@@ -1 +1 @@
+-c613d0b82681
++1c19a98b5eef
+diff --git a/db/migration/alembic_migrations/versions/train/expand/1c19a98b5eef_add_router_configurations.py b/db/migration/alembic_migrations/versions/train/expand/1c19a98b5eef_add_router_configurations.py
+new file mode 100644
+index 0000000000..f12600ef4f
+--- /dev/null
++++ b/db/migration/alembic_migrations/versions/train/expand/1c19a98b5eef_add_router_configurations.py
+@@ -0,0 +1,36 @@
++# Copyright 2023 OpenStack Foundation
++#
++# 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 alembic import op
++import sqlalchemy as sa
++
++"""add router configurations
++
++Revision ID: 1c19a98b5eef
++Revises: cab12b72ed90
++Create Date: 2023-08-01 10:05:56.412167
++
++"""
++
++# revision identifiers, used by Alembic.
++revision = '1c19a98b5eef'
++down_revision = 'cab12b72ed90'
++
++
++def upgrade():
++ # ### commands auto generated by Alembic - please adjust! ###
++ op.add_column('router_extra_attributes',
++ sa.Column('configurations', sa.String(length=4095)))
++ # ### end Alembic commands ###
+diff --git a/db/migration/alembic_migrations/versions/train/expand/cab12b72ed90_add_router_gateway_port_forwarding.py b/db/migration/alembic_migrations/versions/train/expand/cab12b72ed90_add_router_gateway_port_forwarding.py
+new file mode 100644
+index 0000000000..ad511a7ed8
+--- /dev/null
++++ b/db/migration/alembic_migrations/versions/train/expand/cab12b72ed90_add_router_gateway_port_forwarding.py
+@@ -0,0 +1,55 @@
++# Copyright 2023 OpenStack Foundation
++#
++# 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 alembic import op
++import sqlalchemy as sa
++
++"""add router gateway port forwarding
++
++Revision ID: cab12b72ed90
++Revises: c613d0b82681
++Create Date: 2023-07-04 10:27:54.485453
++
++"""
++
++# revision identifiers, used by Alembic.
++revision = 'cab12b72ed90'
++down_revision = 'c613d0b82681'
++
++
++def upgrade():
++ # ### commands auto generated by Alembic - please adjust! ###
++ op.create_table(
++ 'rgportforwardings',
++ sa.Column('id', sa.String(length=36), nullable=False),
++ sa.Column('router_id', sa.String(length=36), nullable=False),
++ sa.Column('external_port', sa.Integer(), nullable=False),
++ sa.Column('internal_neutron_port_id', sa.String(length=36),
++ nullable=False),
++ sa.Column('protocol', sa.String(length=40), nullable=False),
++ sa.Column('socket', sa.String(length=36), nullable=False),
++ sa.ForeignKeyConstraint(['internal_neutron_port_id'], ['ports.id'],
++ ondelete='CASCADE'),
++ sa.ForeignKeyConstraint(['router_id'], ['routers.id'],
++ ondelete='CASCADE'),
++ sa.PrimaryKeyConstraint('id'),
++ sa.UniqueConstraint(
++ 'internal_neutron_port_id', 'socket', 'protocol',
++ name='uniq_port_forwardings0internal_neutron_port_id0socket0protocol'),
++ sa.UniqueConstraint(
++ 'router_id', 'external_port', 'protocol',
++ name='uniq_rg_port_forwardings0router_id0external_port0protocol')
++ )
++ # ### end Alembic commands ###
+diff --git a/db/models/l3_attrs.py b/db/models/l3_attrs.py
+index 6c30ac2c16..904f4ef08d 100644
+--- a/db/models/l3_attrs.py
++++ b/db/models/l3_attrs.py
+@@ -41,6 +41,8 @@ class RouterExtraAttributes(model_base.BASEV2):
+ # Availability Zone support
+ availability_zone_hints = sa.Column(sa.String(255))
+
++ configurations = sa.Column(sa.String(4095))
++
+ router = orm.relationship(
+ 'Router', load_on_pending=True,
+ backref=orm.backref("extra_attributes", lazy='joined',
+diff --git a/db/models/rg_port_forwarding.py b/db/models/rg_port_forwarding.py
+new file mode 100644
+index 0000000000..e7963169a8
+--- /dev/null
++++ b/db/models/rg_port_forwarding.py
+@@ -0,0 +1,59 @@
++# Copyright (c) 2023 UnionTech
++# 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 sqlalchemy as sa
++from sqlalchemy import orm
++from neutron_lib.db import model_base
++from neutron_lib.db import constants as db_const
++
++from neutron.db.models import l3
++from neutron.db import models_v2
++
++
++class RGPortForwarding(model_base.BASEV2, model_base.HasId):
++ __table_args__ = (
++ sa.UniqueConstraint('router_id', 'external_port', 'protocol',
++ name='uniq_rg_port_forwardings0router_id0'
++ 'external_port0protocol'),
++ sa.UniqueConstraint('internal_neutron_port_id', 'socket', 'protocol',
++ name='uniq_port_forwardings0'
++ 'internal_neutron_port_id0socket0'
++ 'protocol')
++ )
++
++ router_id = sa.Column(sa.String(db_const.UUID_FIELD_SIZE),
++ sa.ForeignKey('routers.id',
++ ondelete="CASCADE"),
++ nullable=False)
++ external_port = sa.Column(sa.Integer, nullable=False)
++ internal_neutron_port_id = sa.Column(
++ sa.String(db_const.UUID_FIELD_SIZE),
++ sa.ForeignKey('ports.id', ondelete="CASCADE"),
++ nullable=False)
++ protocol = sa.Column(sa.String(40), nullable=False)
++ socket = sa.Column(sa.String(36), nullable=False)
++ port = orm.relationship(
++ models_v2.Port, load_on_pending=True,
++ backref=orm.backref("rg_port_forwardings",
++ lazy='subquery', uselist=True,
++ cascade='delete')
++ )
++ router = orm.relationship(
++ l3.Router, load_on_pending=True,
++ backref=orm.backref("rg_port_forwardings",
++ lazy='subquery', uselist=True,
++ cascade='delete')
++ )
++ revises_on_change = ('router', 'port',)
+diff --git a/extensions/rg_port_forwarding.py b/extensions/rg_port_forwarding.py
+new file mode 100644
+index 0000000000..c9888cd1ec
+--- /dev/null
++++ b/extensions/rg_port_forwarding.py
+@@ -0,0 +1,119 @@
++# Copyright (c) 2023 UnionTech
++# 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 six
++import abc
++import itertools
++from typing import List
++
++from neutron_lib.context import Context
++from neutron_lib.plugins import constants
++from neutron_lib.plugins import directory
++from neutron_lib.services import base as service_base
++from neutron_lib.api.extensions import APIExtensionDescriptor
++from neutron_lib.api.definitions import rg_port_forwarding as apidef
++
++from neutron.api.v2 import base
++from neutron.api.v2 import resource_helper
++from neutron.api.extensions import ResourceExtension
++
++
++class Rg_port_forwarding(APIExtensionDescriptor):
++ api_definition = apidef
++
++ @classmethod
++ def get_plugin_interface(cls):
++ return RGPortForwardingPluginBase
++
++ @classmethod
++ def get_resources(cls):
++ special_mappings = {'routers': 'router'}
++ plural_mappings = resource_helper.build_plural_mappings(
++ special_mappings,
++ itertools.chain(
++ apidef.RESOURCE_ATTRIBUTE_MAP,
++ apidef.SUB_RESOURCE_ATTRIBUTE_MAP
++ )
++ )
++
++ resources = resource_helper.build_resource_info(
++ plural_mappings,
++ apidef.RESOURCE_ATTRIBUTE_MAP,
++ constants.ROUTER_GATEWAY_PORTFORWARDING,
++ translate_name=True,
++ allow_bulk=True)
++
++ plugin = directory.get_plugin(constants.ROUTER_GATEWAY_PORTFORWARDING)
++
++ parent = apidef.SUB_RESOURCE_ATTRIBUTE_MAP[apidef.COLLECTION_NAME].get(
++ 'parent')
++ params = apidef.SUB_RESOURCE_ATTRIBUTE_MAP[apidef.COLLECTION_NAME].get(
++ 'parameters')
++ controller = base.create_resource(
++ apidef.COLLECTION_NAME, apidef.RESOURCE_NAME, plugin, params,
++ allow_bulk=True, parent=parent, allow_pagination=True,
++ allow_sorting=True)
++
++ resource = ResourceExtension(
++ apidef.COLLECTION_NAME, controller, parent, attr_map=params)
++ resources.append(resource)
++
++ return resources
++
++
++@six.add_metaclass(abc.ABCMeta)
++class RGPortForwardingPluginBase(service_base.ServicePluginBase):
++ path_prefix = apidef.API_PREFIX
++
++ @classmethod
++ def get_plugin_type(cls):
++ return constants.ROUTER_GATEWAY_PORTFORWARDING
++
++ def get_plugin_description(self):
++ return "Router Gateway Port Forwarding Service Plugin"
++
++ @abc.abstractmethod
++ def create_router_gateway_port_forwarding(self, context: Context,
++ router_id: str,
++ gateway_port_forwarding: dict):
++ pass
++
++ @abc.abstractmethod
++ def update_router_gateway_port_forwarding(self, context: Context, id: str,
++ router_id: str,
++ gateway_port_forwarding: dict):
++ pass
++
++ @abc.abstractmethod
++ def get_router_gateway_port_forwarding(self, context: Context, id: str,
++ router_id: str,
++ fields: List[str] = None):
++ pass
++
++ @abc.abstractmethod
++ def get_router_gateway_port_forwardings(self, context: Context,
++ router_id: str,
++ filters: List[str] = None,
++ fields: List[str] = None,
++ sorts: List[str] = None,
++ limit: int = None,
++ marker: str = None,
++ page_reverse: bool = False):
++ pass
++
++ @abc.abstractmethod
++ def delete_router_gateway_port_forwarding(self, context: Context, id: str,
++ router_id: str):
++ pass
+diff --git a/objects/rg_port_forwarding.py b/objects/rg_port_forwarding.py
+new file mode 100644
+index 0000000000..28cb4e1d4d
+--- /dev/null
++++ b/objects/rg_port_forwarding.py
+@@ -0,0 +1,87 @@
++# Copyright (c) 2023 UnionTech
++# 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 netaddr
++from neutron_lib import constants as lib_const
++from oslo_versionedobjects import fields as obj_fields
++
++from neutron.objects import base, common_types
++from neutron.db.models import rg_port_forwarding as models
++
++FIELDS_NOT_SUPPORT_FILTER = ['internal_ip_address', 'internal_port']
++
++
++@base.NeutronObjectRegistry.register
++class RGPortForwarding(base.NeutronDbObject):
++ VERSION = '1.0'
++
++ db_model = models.RGPortForwarding
++
++ primary_keys = ['id']
++ foreign_keys = {
++ 'Router': {'router_id': 'id'},
++ 'Port': {'internal_port_id': 'id'}
++ }
++ fields_need_translation = {
++ 'socket': 'socket',
++ 'internal_port_id': 'internal_neutron_port_id'
++ }
++
++ fields = {
++ 'id': common_types.UUIDField(),
++ 'router_id': common_types.UUIDField(nullable=False),
++ 'external_port': common_types.PortRangeField(nullable=False),
++ 'protocol': common_types.IpProtocolEnumField(nullable=False),
++ 'internal_port_id': common_types.UUIDField(nullable=False),
++ 'internal_ip_address': obj_fields.IPV4AddressField(),
++ 'internal_port': common_types.PortRangeField(nullable=False),
++ 'gw_ip_address': obj_fields.IPV4AddressField(),
++ }
++
++ synthetic_fields = ['gw_ip_address']
++ fields_no_update = {'id', 'router_id'}
++
++ def __eq__(self, other):
++ for attr in self.fields:
++ if getattr(self, attr) != getattr(other, attr):
++ return False
++ return True
++
++ def obj_load_attr(self, attrname):
++ super(RGPortForwarding, self).obj_load_attr(attrname)
++
++ def from_db_object(self, db_obj):
++ super(RGPortForwarding, self).from_db_object(db_obj)
++
++ @classmethod
++ def modify_fields_from_db(cls, db_obj):
++ result = super(RGPortForwarding, cls).modify_fields_from_db(db_obj)
++ if 'socket' in result:
++ groups = result['socket'].split(":")
++ result['internal_ip_address'] = netaddr.IPAddress(
++ groups[0], version=lib_const.IP_VERSION_4)
++ result['internal_port'] = int(groups[1])
++ del result['socket']
++ return result
++
++ @classmethod
++ def modify_fields_to_db(cls, fields):
++ result = super(RGPortForwarding, cls).modify_fields_to_db(fields)
++ if 'internal_ip_address' in result and 'internal_port' in result:
++ result['socket'] = (f"{result['internal_ip_address']}:"
++ f"{result['internal_port']}")
++ del result['internal_ip_address']
++ del result['internal_port']
++ return result
+diff --git a/objects/router.py b/objects/router.py
+index 1373f89515..9590a109f6 100644
+--- a/objects/router.py
++++ b/objects/router.py
+@@ -18,6 +18,7 @@ from neutron_lib.api.definitions import availability_zone as az_def
+ from neutron_lib.api.validators import availability_zone as az_validator
+ from neutron_lib import constants as n_const
+ from neutron_lib.utils import net as net_utils
++from neutron_lib.objects import utils as obj_utils
+ from oslo_versionedobjects import fields as obj_fields
+ import six
+ from sqlalchemy import func
+@@ -70,7 +71,8 @@ class RouterRoute(base.NeutronDbObject):
+ @base.NeutronObjectRegistry.register
+ class RouterExtraAttributes(base.NeutronDbObject):
+ # Version 1.0: Initial version
+- VERSION = '1.0'
++ # Version 1.1: Add configurations
++ VERSION = '1.1'
+
+ db_model = l3_attrs.RouterExtraAttributes
+
+@@ -80,7 +82,8 @@ class RouterExtraAttributes(base.NeutronDbObject):
+ 'service_router': obj_fields.BooleanField(default=False),
+ 'ha': obj_fields.BooleanField(default=False),
+ 'ha_vr_id': obj_fields.IntegerField(nullable=True),
+- 'availability_zone_hints': obj_fields.ListOfStringsField(nullable=True)
++ 'availability_zone_hints': obj_fields.ListOfStringsField(nullable=True),
++ 'configurations': common_types.DictOfMiscValuesField(nullable=True),
+ }
+
+ primary_keys = ['router_id']
+@@ -95,6 +98,9 @@ class RouterExtraAttributes(base.NeutronDbObject):
+ result[az_def.AZ_HINTS] = (
+ az_validator.convert_az_string_to_list(
+ result[az_def.AZ_HINTS]))
++ if 'configurations' in result:
++ result['configurations'] = cls.load_json_from_str(
++ result['configurations'], default={})
+ return result
+
+ @classmethod
+@@ -104,6 +110,11 @@ class RouterExtraAttributes(base.NeutronDbObject):
+ result[az_def.AZ_HINTS] = (
+ az_validator.convert_az_list_to_string(
+ result[az_def.AZ_HINTS]))
++ if ('configurations' in result and
++ not isinstance(result['configurations'],
++ obj_utils.StringMatchingFilterObj)):
++ result['configurations'] = (
++ cls.filter_to_json_str(result['configurations']))
+ return result
+
+ @classmethod
+diff --git a/scheduler/l3_agent_scheduler.py b/scheduler/l3_agent_scheduler.py
+index 5810cf85b8..7a428aef03 100644
+--- a/scheduler/l3_agent_scheduler.py
++++ b/scheduler/l3_agent_scheduler.py
+@@ -14,13 +14,15 @@
+ # under the License.
+
+ import abc
+-import collections
++import random
+ import functools
+ import itertools
+-import random
++import collections
++from typing import List, Optional
+
+ from neutron_lib.api.definitions import availability_zone as az_def
+ from neutron_lib import constants as lib_const
++from neutron_lib.context import Context
+ from neutron_lib.db import api as lib_db_api
+ from neutron_lib.exceptions import l3 as l3_exc
+ from oslo_config import cfg
+@@ -31,8 +33,9 @@ import six
+ from neutron.common import utils
+ from neutron.conf.db import l3_hamode_db
+ from neutron.db.models import l3agent as rb_model
++from neutron.objects.agent import Agent
+ from neutron.objects import l3agent as rb_obj
+-
++from neutron.services.l3_router.l3_router_plugin import L3RouterPlugin
+
+ LOG = logging.getLogger(__name__)
+ cfg.CONF.register_opts(l3_hamode_db.L3_HA_OPTS)
+@@ -228,26 +231,25 @@ class L3Scheduler(object):
+ if not candidates:
+ return
+ elif sync_router.get('ha', False):
+- chosen_agents = self._bind_ha_router(plugin, context,
+- router_id,
+- sync_router.get('tenant_id'),
++ chosen_agents = self._bind_ha_router(plugin, context, sync_router,
+ candidates)
+ if not chosen_agents:
+ return
+ chosen_agent = chosen_agents[-1]
+ else:
+ chosen_agent = self._choose_router_agent(
+- plugin, context, candidates)
++ context, plugin, candidates, sync_router)
+ self.bind_router(plugin, context, router_id, chosen_agent.id)
+ return chosen_agent
+
+ @abc.abstractmethod
+- def _choose_router_agent(self, plugin, context, candidates):
++ def _choose_router_agent(self, context, plugin, candidates, sync_router):
+ """Choose an agent from candidates based on a specific policy."""
+ pass
+
+ @abc.abstractmethod
+- def _choose_router_agents_for_ha(self, plugin, context, candidates):
++ def _choose_router_agents_for_ha(self, context, plugin, candidates,
++ sync_router):
+ """Choose agents from candidates based on a specific policy."""
+ pass
+
+@@ -315,19 +317,19 @@ class L3Scheduler(object):
+ hosting_list = [tuple(host) for host in hosting]
+ return list(set(candidates) - set(hosting_list))
+
+- def _bind_ha_router(self, plugin, context, router_id,
+- tenant_id, candidates):
++ def _bind_ha_router(self, plugin, context, sync_router, candidates):
+ """Bind a HA router to agents based on a specific policy."""
+-
++ router_id = sync_router.get('id')
++ tenant_id = sync_router.get('tenant_id')
+ candidates = self._filter_scheduled_agents(plugin, context, router_id,
+ candidates)
+
+ chosen_agents = self._choose_router_agents_for_ha(
+- plugin, context, candidates)
++ context, plugin, candidates, sync_router)
+
+ for agent in chosen_agents:
+- self.create_ha_port_and_bind(plugin, context, router_id,
+- tenant_id, agent)
++ self.create_ha_port_and_bind(plugin, context, router_id, tenant_id,
++ agent)
+
+ return chosen_agents
+
+@@ -335,10 +337,11 @@ class L3Scheduler(object):
+ class ChanceScheduler(L3Scheduler):
+ """Randomly allocate an L3 agent for a router."""
+
+- def _choose_router_agent(self, plugin, context, candidates):
++ def _choose_router_agent(self, context, plugin, candidates, sync_router):
+ return random.choice(candidates)
+
+- def _choose_router_agents_for_ha(self, plugin, context, candidates):
++ def _choose_router_agents_for_ha(self, context, plugin, candidates,
++ sync_router):
+ num_agents = self._get_num_of_agents_for_ha(len(candidates))
+ return random.sample(candidates, num_agents)
+
+@@ -346,13 +349,14 @@ class ChanceScheduler(L3Scheduler):
+ class LeastRoutersScheduler(L3Scheduler):
+ """Allocate to an L3 agent with the least number of routers bound."""
+
+- def _choose_router_agent(self, plugin, context, candidates):
++ def _choose_router_agent(self, context, plugin, candidates, sync_router):
+ candidate_ids = [candidate['id'] for candidate in candidates]
+ chosen_agent = plugin.get_l3_agent_with_min_routers(
+ context, candidate_ids)
+ return chosen_agent
+
+- def _choose_router_agents_for_ha(self, plugin, context, candidates):
++ def _choose_router_agents_for_ha(self, context, plugin, candidates,
++ sync_router):
+ num_agents = self._get_num_of_agents_for_ha(len(candidates))
+ ordered_agents = plugin.get_l3_agents_ordered_by_num_routers(
+ context, [candidate['id'] for candidate in candidates])
+@@ -397,7 +401,8 @@ class AZLeastRoutersScheduler(LeastRoutersScheduler):
+
+ return candidates
+
+- def _choose_router_agents_for_ha(self, plugin, context, candidates):
++ def _choose_router_agents_for_ha(self, context, plugin, candidates,
++ sync_router):
+ ordered_agents = plugin.get_l3_agents_ordered_by_num_routers(
+ context, [candidate['id'] for candidate in candidates])
+ num_agents = self._get_num_of_agents_for_ha(len(ordered_agents))
+@@ -416,3 +421,65 @@ class AZLeastRoutersScheduler(LeastRoutersScheduler):
+ if len(selected_agents) >= num_agents:
+ break
+ return selected_agents
++
++
++class PreferredL3AgentRoutersScheduler(LeastRoutersScheduler):
++
++ @staticmethod
++ def get_preferred_agent(sync_router: dict) -> Optional[str]:
++ configurations = sync_router.get('configurations', {})
++ if configurations:
++ return configurations.get('preferred_agent', None)
++ return None
++
++ @staticmethod
++ def get_agents(sync_router: dict) -> Optional[List[str]]:
++ configurations = sync_router.get('configurations', {})
++ if configurations:
++ master = configurations.get('master_agent', None)
++ slaves = configurations.get('slave_agents', [])
++ if master and slaves:
++ return slaves + [master]
++ return []
++
++ def _choose_router_agent(self, context: Context,
++ plugin: L3RouterPlugin,
++ candidates: List[Agent],
++ sync_router: dict) -> Agent:
++ agent = self.get_preferred_agent(sync_router)
++ if agent:
++ new_candidates = [candidate for candidate in candidates
++ if candidate['host'] == agent]
++ if not new_candidates:
++ LOG.warning(f"Router {sync_router['id']} failed to "
++ f"schedule l3 agent on {agent}.")
++ else:
++ agent = new_candidates[0]
++ LOG.debug(f"Router {sync_router['id']} l3 agent is {agent}.")
++ return agent
++ agent = super()._choose_router_agent(context, plugin, candidates,
++ sync_router)
++ return agent
++
++ def _choose_router_agents_for_ha(self, context: Context,
++ plugin: L3RouterPlugin,
++ candidates: List[Agent],
++ sync_router: dict) -> List[Agent]:
++
++ agents = self.get_agents(sync_router)
++ if agents:
++ if self.max_ha_agents < len(agents):
++ agents = agents[len(agents) - self.max_ha_agents:]
++ new_candidates = [candidate for candidate in candidates if
++ candidate['host'] in agents]
++ if len(new_candidates) != len(agents):
++ LOG.warning(f"Router {sync_router['id']} failed to "
++ f"schedule l3 agents on {agents}.")
++ else:
++ LOG.debug(f"Router {sync_router['id']} l3 agents are "
++ f"{new_candidates}.")
++ return new_candidates
++
++ return super(
++ PreferredL3AgentRoutersScheduler, self
++ )._choose_router_agents_for_ha(context, plugin, candidates, sync_router)
+diff --git a/services/l3_router/l3_router_plugin.py b/services/l3_router/l3_router_plugin.py
+index 2e8a762764..9825138261 100644
+--- a/services/l3_router/l3_router_plugin.py
++++ b/services/l3_router/l3_router_plugin.py
+@@ -38,6 +38,7 @@ from oslo_utils import importutils
+
+ from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api
+ from neutron.api.rpc.handlers import l3_rpc
++from neutron.conf.common import NETWORK_HOST_OPTS
+ from neutron.db import dns_db
+ from neutron.db import extraroute_db
+ from neutron.db import l3_dvr_ha_scheduler_db
+@@ -135,6 +136,17 @@ class L3RouterPlugin(service_base.ServicePluginBase,
+
+ self.add_worker(rpc_worker)
+ self.l3_driver_controller = driver_controller.DriverController(self)
++ self.compute_to_network = dict()
++ self._init_compute_to_network()
++
++ def _init_compute_to_network(self):
++ for network_node in cfg.CONF.network_nodes:
++ cfg.CONF.register_opts(NETWORK_HOST_OPTS, group=network_node)
++ network_group = cfg.CONF.get(network_node, None)
++ if network_group:
++ compute_nodes = network_group.get('compute_nodes', [])
++ for compute_node in compute_nodes:
++ self.compute_to_network[compute_node] = network_node
+
+ @property
+ def supported_extension_aliases(self):
+diff --git a/services/rg_portforwarding/__init__.py b/services/rg_portforwarding/__init__.py
+new file mode 100644
+index 0000000000..e69de29bb2
+diff --git a/services/rg_portforwarding/common/__init__.py b/services/rg_portforwarding/common/__init__.py
+new file mode 100644
+index 0000000000..e69de29bb2
+diff --git a/services/rg_portforwarding/common/exceptions.py b/services/rg_portforwarding/common/exceptions.py
+new file mode 100644
+index 0000000000..73cea68e32
+--- /dev/null
++++ b/services/rg_portforwarding/common/exceptions.py
+@@ -0,0 +1,77 @@
++# Copyright (c) 2023 UnionTech
++# 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 neutron._i18n import _
++from neutron_lib import exceptions
++
++
++class PortForwardingNotSupportFilterField(exceptions.BadRequest):
++ message = _("Port Forwarding filter %(filter)s is not supported.")
++
++
++class RouterDoesNotHaveGateway(exceptions.BadRequest):
++ message = _("Router %(router_id)s does not have any gateways.")
++
++
++class RouterGatewayPortNotFound(exceptions.NotFound):
++ message = _("Router %(router_id)s 's gateway port %(gw_port_id)s "
++ "could not be found.")
++
++
++class RouterGatewayPortDoesNotHaveAnyIPAddresses(exceptions.NotFound):
++ message = _("Router %(router_id)s 's gateway port %(gw_port_id)s "
++ "does not have any IP addresses.")
++
++
++class RouterGatewayPortForwardingNotFound(exceptions.NotFound):
++ message = _("Router Gateway Port Forwarding %(id)s could not be found.")
++
++
++class PortHasBindingFloatingIP(exceptions.InUse):
++ message = _("Cannot create port forwarding to floating IP "
++ "%(floating_ip_address)s (%(fip_id)s) with port %(port_id)s "
++ "using fixed IP %(fixed_ip)s, as that port already "
++ "has a binding floating IP.")
++
++
++class InconsistentPortAndIP(exceptions.BadRequest):
++ message = _("Port %(port_id)s does not have ip address %(ip_address)s.")
++
++
++class RouterGatewayPortForwardingAlreadyExists(exceptions.BadRequest):
++ message = _("A duplicate router gateway port forwarding entry "
++ "with same attributes already exists, "
++ "conflicting values are %(conflict)s.")
++
++
++class PortNetworkNotBindOnRouter(exceptions.BadRequest):
++ message = _("Port %(port_id)s 's network %(network_id)s "
++ "not bind on router %(router_id)s.")
++
++
++class RouterGatewayPortForwardingUpdateFailed(exceptions.BadRequest):
++ message = _("Another router port forwarding entry with the same "
++ "attributes already exists, conflicting "
++ "values are %(conflict)s.")
++
++
++class DeletedRouterWithRGForwarding(exceptions.InUse):
++ message = _("Cant not delete router, "
++ "router %(router_id)s has port forwardings to remove.")
++
++
++class DeletedRouterGatewayWithRGForwarding(exceptions.InUse):
++ message = _("Cant not delete or update router gateway, "
++ "router %(router_id)s has port forwardings to remove.")
+diff --git a/services/rg_portforwarding/pf_plugin.py b/services/rg_portforwarding/pf_plugin.py
+new file mode 100644
+index 0000000000..ed8e68e53c
+--- /dev/null
++++ b/services/rg_portforwarding/pf_plugin.py
+@@ -0,0 +1,369 @@
++# Copyright (c) 2023 UnionTech
++# 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 typing import List, Dict, Optional
++
++from neutron_lib import constants
++from neutron_lib.context import Context
++from neutron_lib.plugins import directory
++from neutron_lib.db import resource_extend
++from neutron_lib.plugins.constants import L3
++from neutron_lib.db.api import CONTEXT_WRITER
++from neutron_lib.exceptions import PortNotFound
++from neutron_lib.exceptions.l3 import RouterNotFound
++from neutron_lib.callbacks import registry, resources
++from neutron_lib.callbacks import events as lib_events
++from neutron_lib.callbacks.events import DBEventPayload
++from neutron_lib.api.definitions import rg_port_forwarding as apidef
++from neutron_lib.objects.exceptions import NeutronDbObjectDuplicateEntry
++
++from neutron.db import db_base_plugin_common
++from neutron.db.l3_dvr_db import is_distributed_router
++from neutron.db.l3_hamode_db import is_ha_router
++
++from neutron.objects.base import Pager
++from neutron.objects.ports import Port
++from neutron.objects.router import Router, FloatingIP
++from neutron.objects.rg_port_forwarding import RGPortForwarding
++from neutron.objects.rg_port_forwarding import FIELDS_NOT_SUPPORT_FILTER
++
++from neutron.extensions.rg_port_forwarding import RGPortForwardingPluginBase
++from neutron.services.l3_router.l3_router_plugin import L3RouterPlugin
++from neutron.services.rg_portforwarding.common import exceptions
++
++from neutron.api.rpc.callbacks import events
++from neutron.api.rpc.handlers import resources_rpc
++
++LOG = logging.getLogger(__name__)
++
++
++@resource_extend.has_resource_extenders
++@registry.has_registry_receivers
++class RGPortForwardingPlugin(RGPortForwardingPluginBase):
++ required_service_plugins = ['router']
++
++ supported_extension_aliases = [apidef.ALIAS]
++
++ __native_pagination_support = True
++ __native_sorting_support = True
++ __filter_validation_support = True
++
++ def __init__(self):
++ super(RGPortForwardingPlugin, self).__init__()
++ self.push_api = resources_rpc.ResourcesPushRpcApi()
++ self.l3_plugin = directory.get_plugin(L3)
++ self.core_plugin = directory.get_plugin()
++
++ @staticmethod
++ def _get_router(context: Context, router_id: str) -> Optional[Router]:
++ router = Router.get_object(context, id=router_id)
++ if not router:
++ raise RouterNotFound(router_id=router_id)
++ return router
++
++ @staticmethod
++ def _get_router_gateway(context: Context, router: Router) -> str:
++ gw_port_id = router.get('gw_port_id', None)
++ if not gw_port_id:
++ raise exceptions.RouterDoesNotHaveGateway(router_id=router.id)
++ gw_port = Port.get_object(
++ context, id=gw_port_id)
++ if not gw_port:
++ raise exceptions.RouterGatewayPortNotFound(router_id=router.id,
++ gw_port_id=gw_port_id)
++ gw_port_ips = gw_port.get("fixed_ips", [])
++ if len(gw_port_ips) <= 0:
++ raise exceptions.RouterGatewayPortDoesNotHaveAnyIPAddresses(
++ router_id=router.id, gw_port_id=gw_port_id)
++ gw_ip_address = gw_port_ips[0].get('ip_address')
++ return gw_ip_address
++
++ @staticmethod
++ def _get_port(context: Context, port_id: str) -> Optional[Port]:
++ port = Port.get_object(context, id=port_id)
++ if not port:
++ raise PortNotFound(port_id=port_id)
++ return port
++
++ @staticmethod
++ def _get_ports(context: Context, router_id: str, port: Port,
++ device_owner: str) -> Optional[List[Port]]:
++ ports = Port.get_ports_by_router_and_network(
++ context, router_id, device_owner, port.network_id)
++ if not ports:
++ raise exceptions.PortNetworkNotBindOnRouter(
++ port_id=port.id,
++ network_id=port.network_id,
++ router_id=router_id)
++ return ports
++
++ @staticmethod
++ def _validate_filter_for_port_forwarding(filters: Dict[str, str]) -> None:
++ if not filters:
++ return
++ for filter_member_key in filters.keys():
++ if filter_member_key in FIELDS_NOT_SUPPORT_FILTER:
++ raise exceptions.PortForwardingNotSupportFilterField(
++ filter=filter_member_key)
++
++ @staticmethod
++ def _check_port_has_binding_floating_ip(context: Context, port_id: str,
++ ip_address: str) -> None:
++ floatingip_objs = FloatingIP.get_objects(
++ context.elevated(),
++ fixed_port_id=port_id)
++ if floatingip_objs:
++ floating_ip_address = floatingip_objs[0].floating_ip_address
++ raise exceptions.PortHasBindingFloatingIP(
++ floating_ip_address=floating_ip_address,
++ fip_id=floatingip_objs[0].id,
++ port_id=port_id,
++ fixed_ip=ip_address)
++
++ @staticmethod
++ def _get_device_owner(router: Router) -> str:
++ if is_distributed_router(router):
++ return constants.DEVICE_OWNER_DVR_INTERFACE
++ elif is_ha_router(router):
++ return constants.DEVICE_OWNER_HA_REPLICATED_INT
++ return constants.DEVICE_OWNER_ROUTER_INTF
++
++ def _check_router_port(self, context: Context, router: Router,
++ port: Port):
++ device_owner = self._get_device_owner(router)
++ self._get_ports(context, router.id, port, device_owner)
++
++ def _check_port(self, context: Context, port_id: str, ip: str) -> Port:
++ port = self._get_port(context, port_id)
++ self._check_port_has_binding_floating_ip(context, port_id, ip)
++ fixed_ips = port.get('fixed_ips', [])
++ result = list(map(lambda x: str(x.get('ip_address')) == ip, fixed_ips))
++ if not any(result):
++ raise exceptions.InconsistentPortAndIP(port_id=port, ip_address=ip)
++ return port
++
++ def _check_router(self, context: Context, router_id: str) -> (Router, str):
++ router = self._get_router(context, router_id)
++ gw_ip_address = self._get_router_gateway(context, router)
++ return router, gw_ip_address
++
++ def _check_port_forwarding_create(self, context: Context, router_id: str,
++ pf_dict: Dict) -> None:
++ router, gw_ip_address = self._check_router(context, router_id)
++ pf_dict['router_id'] = router_id
++ pf_dict[apidef.GW_IP_ADDRESS] = gw_ip_address
++ internal_port_id = pf_dict[apidef.INTERNAL_PORT_ID]
++ internal_ip_address = pf_dict[apidef.INTERNAL_IP_ADDRESS]
++ internal_port = self._check_port(context, internal_port_id,
++ internal_ip_address)
++ self._check_router_port(context, router, internal_port)
++
++ @staticmethod
++ def _check_port_forwarding(context: Context, pf_obj: RGPortForwarding):
++ pf_objs = RGPortForwarding.get_objects(
++ context,
++ router_id=pf_obj.router_id,
++ protocol=pf_obj.protocol)
++
++ for obj in pf_objs:
++ if obj.id == pf_obj.get('id', None):
++ continue
++ # Ensure there are no conflicts on the outside
++ if obj.external_port == pf_obj.external_port:
++ raise exceptions.RouterGatewayPortForwardingAlreadyExists(
++ conflict={
++ 'router_id': pf_obj.router_id,
++ 'protocol': pf_obj.protocol,
++ 'external_port': obj.external_port,
++ }
++ )
++ # Ensure there are no conflicts in the inside
++ # socket: internal_ip_address + internal_port
++ if (obj.internal_port_id == pf_obj.internal_port_id and
++ obj.internal_ip_address == pf_obj.internal_ip_address and
++ obj.internal_port == pf_obj.internal_port):
++ raise exceptions.RouterGatewayPortForwardingAlreadyExists(
++ conflict={
++ 'router_id': pf_obj.router_id,
++ 'protocol': pf_obj.protocol,
++ 'internal_port_id': obj.internal_port_id,
++ 'internal_ip_address': str(obj.internal_ip_address),
++ 'internal_port': obj.internal_port
++ }
++ )
++
++ @staticmethod
++ def _find_existing_rg_port_forwarding(context: Context,
++ router_id: str,
++ port_forwarding: Dict,
++ specify_params: List = None):
++ # Because the session had been flushed by NeutronDbObjectDuplicateEntry
++ # so if we want to use the context to get another db queries, we need
++ # to rollback first.
++ context.session.rollback()
++ if not specify_params:
++ specify_params = [
++ {
++ 'router_id': router_id,
++ 'external_port': port_forwarding['external_port'],
++ 'protocol': port_forwarding['protocol']
++ },
++ {
++ 'internal_port_id': port_forwarding['internal_port_id'],
++ 'internal_ip_address': port_forwarding[
++ 'internal_ip_address'],
++ 'internal_port': port_forwarding['internal_port'],
++ 'protocol': port_forwarding['protocol']
++ }]
++ for param in specify_params:
++ objs = RGPortForwarding.get_objects(context, **param)
++ if objs:
++ return objs[0], param
++
++ @db_base_plugin_common.make_result_with_fields
++ @db_base_plugin_common.convert_result_to_dict
++ def get_router_gateway_port_forwardings(self, context: Context,
++ router_id: str,
++ filters: List[str] = None,
++ fields: List[str] = None,
++ sorts: List[str] = None,
++ limit: int = None,
++ marker: str = None,
++ page_reverse: bool = False):
++
++ router, gw_ip_address = self._check_router(context, router_id)
++ filters = filters or {}
++ self._validate_filter_for_port_forwarding(filters)
++ pager = Pager(sorts, limit, page_reverse, marker)
++ port_forwardings = RGPortForwarding.get_objects(
++ context, _pager=pager, router_id=router_id, **filters)
++ for pf in port_forwardings:
++ setattr(pf, 'gw_ip_address', gw_ip_address)
++ return port_forwardings
++
++ @db_base_plugin_common.convert_result_to_dict
++ def create_router_gateway_port_forwarding(self, context: Context,
++ router_id: str,
++ gateway_port_forwarding: dict):
++ port_forwarding = gateway_port_forwarding.get(apidef.RESOURCE_NAME)
++ self._check_port_forwarding_create(context, router_id, port_forwarding)
++ with CONTEXT_WRITER.using(context):
++ pf_obj = RGPortForwarding(context, **port_forwarding)
++ self._check_port_forwarding(context, pf_obj)
++ try:
++ pf_obj.create()
++ except NeutronDbObjectDuplicateEntry:
++ _, conflict = self._find_existing_rg_port_forwarding(
++ context, router_id, port_forwarding)
++ raise exceptions.RouterGatewayPortForwardingAlreadyExists(
++ conflict=conflict
++ )
++ self.push_api.push(context, [pf_obj], events.CREATED)
++ return pf_obj
++
++ @db_base_plugin_common.convert_result_to_dict
++ def update_router_gateway_port_forwarding(self, context: Context, id: str,
++ router_id: str,
++ gateway_port_forwarding: dict):
++
++ router = self._get_router(context, router_id)
++ gw_ip_address = self._get_router_gateway(context, router)
++ pf_obj = RGPortForwarding.get_object(context, id=id)
++ if not pf_obj:
++ raise exceptions.RouterGatewayPortForwardingNotFound(id=id)
++
++ port_forwarding = gateway_port_forwarding.get(apidef.RESOURCE_NAME, {})
++ port_forwarding[apidef.GW_IP_ADDRESS] = gw_ip_address
++ new_port_id = port_forwarding.get(apidef.INTERNAL_PORT_ID)
++ new_internal_ip = port_forwarding.get(apidef.INTERNAL_IP_ADDRESS, None)
++
++ if new_port_id and new_port_id != pf_obj.internal_port_id:
++ self._check_port_has_binding_floating_ip(context,
++ new_port_id,
++ new_internal_ip)
++
++ if any([new_internal_ip, new_port_id]):
++ port_forwarding.update({
++ apidef.INTERNAL_IP_ADDRESS: new_internal_ip
++ if new_internal_ip else
++ str(pf_obj.internal_ip_address),
++ apidef.INTERNAL_PORT_ID: new_port_id
++ if new_port_id else pf_obj.internal_port
++ })
++
++ with CONTEXT_WRITER.using(context):
++ pf_obj.update_fields(port_forwarding, reset_changes=True)
++ self._check_port_forwarding(context, pf_obj)
++ try:
++ pf_obj.update()
++ except NeutronDbObjectDuplicateEntry:
++ _, conflict = self._find_existing_rg_port_forwarding(
++ context, router_id, port_forwarding)
++ raise exceptions.RouterGatewayPortForwardingAlreadyExists(
++ conflict=conflict
++ )
++ self.push_api.push(context, [pf_obj], events.UPDATED)
++ return pf_obj
++
++ @db_base_plugin_common.make_result_with_fields
++ @db_base_plugin_common.convert_result_to_dict
++ def get_router_gateway_port_forwarding(self, context: Context, id: str,
++ router_id: str,
++ fields: List[str] = None):
++ _, gw_ip_address = self._check_router(context, router_id)
++ pf_obj = RGPortForwarding.get_object(context, id=id)
++ if not pf_obj:
++ raise exceptions.RouterGatewayPortForwardingNotFound(id=id)
++ setattr(pf_obj, apidef.GW_IP_ADDRESS, gw_ip_address)
++ return pf_obj
++
++ def delete_router_gateway_port_forwarding(self, context: Context, id: str,
++ router_id: str):
++ pf_obj = RGPortForwarding.get_object(context, id=id)
++ if not pf_obj:
++ raise exceptions.RouterGatewayPortForwardingNotFound(id=id)
++ with CONTEXT_WRITER.using(context):
++ pf_obj.delete()
++ self.push_api.push(context, [pf_obj], events.DELETED)
++
++ @registry.receives(resources.ROUTER, [lib_events.BEFORE_DELETE])
++ def _receive_router_before_delete(self, resource: str, event: str,
++ trigger: L3RouterPlugin,
++ payload: DBEventPayload):
++ router_id = payload.resource_id
++ context = payload.context
++ port_forwardings = RGPortForwarding.get_objects(context,
++ router_id=router_id)
++ if port_forwardings:
++ ex = exceptions.DeletedRouterWithRGForwarding(router_id=router_id)
++ LOG.info(ex.msg)
++ raise ex
++
++ @registry.receives(resources.ROUTER_GATEWAY, [lib_events.BEFORE_DELETE,
++ lib_events.BEFORE_UPDATE])
++ def _receive_router_gateway_before_delete(self, resource: str, event: str,
++ trigger: L3RouterPlugin,
++ payload: DBEventPayload):
++ router_id = payload.resource_id
++ context = payload.context
++ port_forwardings = RGPortForwarding.get_objects(context,
++ router_id=router_id)
++ if port_forwardings:
++ ex = exceptions.DeletedRouterGatewayWithRGForwarding(
++ router_id=router_id)
++ LOG.info(ex.msg)
++ raise ex
diff --git a/conf.README b/conf.README
new file mode 100644
index 0000000..ee87997
--- /dev/null
+++ b/conf.README
@@ -0,0 +1,9 @@
+This directory can be used to configure Neutron services with custom
+user-defined configuration files. To use the facility, just drop a file (or a
+symlink) that has .conf file name extension into an appropriate directory to
+make a service read it during initialization. 'common' directory is read by all
+Neutron services.
+
+Note that user-defined configuration files override any configuration values
+defined in other files read by services. Service specific configuration
+directories beat common one.
diff --git a/neutron-destroy-patch-ports.service b/neutron-destroy-patch-ports.service
new file mode 100644
index 0000000..130601e
--- /dev/null
+++ b/neutron-destroy-patch-ports.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=OpenStack Neutron Destroy Patch Ports
+After=syslog.target network.target openvswitch.service
+Before=neutron-openvswitch-agent.service
+
+[Service]
+Type=oneshot
+User=neutron
+ExecStart=/usr/bin/python -m neutron.cmd.destroy_patch_ports --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/openvswitch_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-openvswitch-agent
+PrivateTmp=false
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-dhcp-agent.service b/neutron-dhcp-agent.service
new file mode 100644
index 0000000..a407f56
--- /dev/null
+++ b/neutron-dhcp-agent.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=OpenStack Neutron DHCP Agent
+After=syslog.target network.target
+
+[Service]
+Type=simple
+User=neutron
+ExecStart=/usr/bin/neutron-dhcp-agent --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/dhcp_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-dhcp-agent --log-file /var/log/neutron/dhcp-agent.log
+PrivateTmp=false
+KillMode=process
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-dist.conf b/neutron-dist.conf
new file mode 100644
index 0000000..0637f10
--- /dev/null
+++ b/neutron-dist.conf
@@ -0,0 +1,10 @@
+[DEFAULT]
+verbose = True
+lock_path = $state_path/lock
+allow_overlapping_ips = True
+use_stderr = False
+api_paste_config = /usr/share/neutron/api-paste.ini
+
+[agent]
+root_helper = sudo neutron-rootwrap /etc/neutron/rootwrap.conf
+root_helper_daemon = sudo neutron-rootwrap-daemon /etc/neutron/rootwrap.conf
diff --git a/neutron-enable-bridge-firewall.sh b/neutron-enable-bridge-firewall.sh
new file mode 100755
index 0000000..ae7a141
--- /dev/null
+++ b/neutron-enable-bridge-firewall.sh
@@ -0,0 +1,19 @@
+#!/bin/sh
+
+# This script is triggered on every ovs/linuxbridge agent start. Its intent is
+# to make sure the firewall for bridged traffic is enabled before we start an
+# agent that may atttempt to set firewall rules on a bridge (a common thing for
+# linuxbridge and ovs/hybrid backend setup).
+
+# before enabling the firewall, load the relevant module
+/usr/sbin/modprobe bridge
+
+# on newer kernels (3.18+), sysctl knobs are split into a separate module;
+# attempt to load it, but don't fail if it's missing (f.e. when running against
+# an older kernel version)
+/usr/sbin/modprobe br_netfilter 2>> /dev/null || :
+
+# now enable the firewall in case it's disabled (f.e. rhel 7.2 and earlier)
+for proto in ip ip6; do
+ /usr/sbin/sysctl -w net.bridge.bridge-nf-call-${proto}tables=1
+done
diff --git a/neutron-l2-agent-sysctl.conf b/neutron-l2-agent-sysctl.conf
new file mode 100644
index 0000000..6692625
--- /dev/null
+++ b/neutron-l2-agent-sysctl.conf
@@ -0,0 +1,2 @@
+net.bridge.bridge-nf-call-ip6tables = 1
+net.bridge.bridge-nf-call-iptables = 1
diff --git a/neutron-l2-agent.modules b/neutron-l2-agent.modules
new file mode 100755
index 0000000..3e64115
--- /dev/null
+++ b/neutron-l2-agent.modules
@@ -0,0 +1,4 @@
+#!/bin/sh
+/sbin/modprobe -b bridge >/dev/null 2>&1
+/sbin/modprobe -b br_netfilter >/dev/null 2>&1
+exit 0
diff --git a/neutron-l3-agent.service b/neutron-l3-agent.service
new file mode 100644
index 0000000..469d3ce
--- /dev/null
+++ b/neutron-l3-agent.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=OpenStack Neutron Layer 3 Agent
+After=syslog.target network.target
+
+[Service]
+Type=simple
+User=neutron
+ExecStart=/usr/bin/neutron-l3-agent --config-file /usr/share/neutron/neutron-dist.conf --config-dir /usr/share/neutron/l3_agent --config-file /etc/neutron/neutron.conf --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-l3-agent --log-file /var/log/neutron/l3-agent.log
+PrivateTmp=false
+KillMode=process
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-linuxbridge-agent.service b/neutron-linuxbridge-agent.service
new file mode 100644
index 0000000..bbd73f3
--- /dev/null
+++ b/neutron-linuxbridge-agent.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=OpenStack Neutron Linux Bridge Agent
+After=syslog.target network.target
+
+[Service]
+Type=simple
+User=neutron
+PermissionsStartOnly=true
+ExecStartPre=/usr/bin/neutron-enable-bridge-firewall.sh
+ExecStart=/usr/bin/neutron-linuxbridge-agent --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/linuxbridge_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-linuxbridge-agent --log-file /var/log/neutron/linuxbridge-agent.log
+PrivateTmp=true
+KillMode=process
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-linuxbridge-cleanup.service b/neutron-linuxbridge-cleanup.service
new file mode 100644
index 0000000..579d901
--- /dev/null
+++ b/neutron-linuxbridge-cleanup.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=OpenStack Neutron Linux Bridge Cleanup Utility
+After=syslog.target network.target
+Before=neutron-linuxbridge-agent.service neutron-dhcp-agent.service neutron-l3-agent.service openstack-nova-compute.service
+
+[Service]
+Type=oneshot
+User=neutron
+ExecStart=/usr/bin/neutron-linuxbridge-cleanup --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/linuxbridge_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-linuxbridge-cleanup --log-file /var/log/neutron/linuxbridge-cleanup.log
+ExecStop=/usr/bin/neutron-linuxbridge-cleanup --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/linuxbridge_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-linuxbridge-cleanup --log-file /var/log/neutron/linuxbridge-cleanup.log
+PrivateTmp=true
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-macvtap-agent.service b/neutron-macvtap-agent.service
new file mode 100644
index 0000000..ba7e401
--- /dev/null
+++ b/neutron-macvtap-agent.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=OpenStack Neutron macvtap L2 agent
+After=syslog.target
+
+[Service]
+Type=simple
+User=neutron
+ExecStart=/usr/bin/neutron-macvtap-agent --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-macvtap-agent --log-file /var/log/neutron/macvtap-agent.log
+PrivateTmp=true
+KillMode=process
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-metadata-agent.service b/neutron-metadata-agent.service
new file mode 100644
index 0000000..62a3106
--- /dev/null
+++ b/neutron-metadata-agent.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=OpenStack Neutron Metadata Agent
+After=syslog.target network.target
+
+[Service]
+Type=simple
+User=neutron
+ExecStart=/usr/bin/neutron-metadata-agent --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/metadata_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-metadata-agent --log-file /var/log/neutron/metadata-agent.log
+PrivateTmp=false
+KillMode=process
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-metering-agent.service b/neutron-metering-agent.service
new file mode 100644
index 0000000..618b341
--- /dev/null
+++ b/neutron-metering-agent.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=OpenStack Neutron Metering Agent
+After=syslog.target network.target
+
+[Service]
+Type=simple
+User=neutron
+ExecStart=/usr/bin/neutron-metering-agent --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/metering_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-metering-agent --log-file /var/log/neutron/metering-agent.log
+PrivateTmp=false
+KillMode=process
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-netns-cleanup.service b/neutron-netns-cleanup.service
new file mode 100644
index 0000000..7a730e4
--- /dev/null
+++ b/neutron-netns-cleanup.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=OpenStack Neutron Netns Cleanup Utility
+After=syslog.target network.target openvswitch.service
+Before=neutron-openvswitch-agent.service neutron-dhcp-agent.service neutron-l3-agent.service openstack-nova-compute.service
+
+[Service]
+Type=oneshot
+User=neutron
+ExecStart=/usr/bin/neutron-netns-cleanup --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/dhcp_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-netns-cleanup --log-file /var/log/neutron/netns-cleanup.log
+ExecStop=/usr/bin/neutron-netns-cleanup --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/dhcp_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-netns-cleanup --log-file /var/log/neutron/netns-cleanup.log --force
+PrivateTmp=false
+RemainAfterExit=yes
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-openvswitch-agent.service b/neutron-openvswitch-agent.service
new file mode 100644
index 0000000..25b10c2
--- /dev/null
+++ b/neutron-openvswitch-agent.service
@@ -0,0 +1,18 @@
+[Unit]
+Description=OpenStack Neutron Open vSwitch Agent
+After=syslog.target network.target network.service openvswitch.service
+PartOf=network.service
+Requires=openvswitch.service
+
+[Service]
+Type=simple
+User=neutron
+PermissionsStartOnly=true
+ExecStartPre=/usr/bin/neutron-enable-bridge-firewall.sh
+ExecStart=/usr/bin/neutron-openvswitch-agent --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/openvswitch_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-openvswitch-agent --log-file /var/log/neutron/openvswitch-agent.log
+PrivateTmp=true
+KillMode=process
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-ovs-cleanup.service b/neutron-ovs-cleanup.service
new file mode 100644
index 0000000..82bfa1a
--- /dev/null
+++ b/neutron-ovs-cleanup.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=OpenStack Neutron Open vSwitch Cleanup Utility
+After=syslog.target network.target openvswitch.service
+Before=neutron-openvswitch-agent.service neutron-dhcp-agent.service neutron-l3-agent.service openstack-nova-compute.service
+
+[Service]
+Type=oneshot
+User=neutron
+ExecStart=/usr/bin/neutron-ovs-cleanup --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/openvswitch_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-ovs-cleanup --log-file /var/log/neutron/ovs-cleanup.log
+ExecStop=/usr/bin/neutron-ovs-cleanup --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/openvswitch_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-ovs-cleanup --log-file /var/log/neutron/ovs-cleanup.log
+PrivateTmp=true
+RemainAfterExit=yes
+TimeoutSec=0
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-rpc-server.service b/neutron-rpc-server.service
new file mode 100644
index 0000000..6aca432
--- /dev/null
+++ b/neutron-rpc-server.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=OpenStack Neutron (RPC only) Server
+After=syslog.target network.target
+
+[Service]
+Type=notify
+User=neutron
+ExecStart=/usr/bin/neutron-rpc-server --config-file /usr/share/neutron/neutron-dist.conf --config-dir /usr/share/neutron/server --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugin.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-rpc-server --log-file /var/log/neutron/rpc-server.log
+PrivateTmp=true
+NotifyAccess=all
+KillMode=process
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-server.service b/neutron-server.service
new file mode 100644
index 0000000..e68024c
--- /dev/null
+++ b/neutron-server.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=OpenStack Neutron Server
+After=syslog.target network.target
+
+[Service]
+Type=notify
+User=neutron
+ExecStart=/usr/bin/neutron-server --config-file /usr/share/neutron/neutron-dist.conf --config-dir /usr/share/neutron/server --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugin.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-server --log-file /var/log/neutron/server.log
+PrivateTmp=true
+NotifyAccess=all
+KillMode=process
+Restart=on-failure
+TimeoutStartSec=0
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-sriov-nic-agent.service b/neutron-sriov-nic-agent.service
new file mode 100644
index 0000000..8233cbf
--- /dev/null
+++ b/neutron-sriov-nic-agent.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=OpenStack Neutron SR-IOV NIC Agent
+After=syslog.target network.target
+
+[Service]
+Type=simple
+User=neutron
+ExecStart=/usr/bin/neutron-sriov-nic-agent --config-file /usr/share/neutron/neutron-dist.conf --config-file /etc/neutron/neutron.conf --config-file /etc/neutron/plugins/ml2/sriov_agent.ini --config-dir /etc/neutron/conf.d/common --config-dir /etc/neutron/conf.d/neutron-sriov-nic-agent --log-file /var/log/neutron/sriov-nic-agent.log
+PrivateTmp=false
+KillMode=process
+Restart=on-failure
+
+[Install]
+WantedBy=multi-user.target
diff --git a/neutron-sudoers b/neutron-sudoers
new file mode 100644
index 0000000..9273f58
--- /dev/null
+++ b/neutron-sudoers
@@ -0,0 +1,4 @@
+Defaults:neutron !requiretty
+
+neutron ALL = (root) NOPASSWD: /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf *
+neutron ALL = (root) NOPASSWD: /usr/bin/neutron-rootwrap-daemon /etc/neutron/rootwrap.conf
diff --git a/neutron.logrotate b/neutron.logrotate
new file mode 100644
index 0000000..fc1c70b
--- /dev/null
+++ b/neutron.logrotate
@@ -0,0 +1,7 @@
+/var/log/neutron/*.log {
+ rotate 14
+ size 10M
+ missingok
+ compress
+ copytruncate
+}
diff --git a/openstack-neutron.spec b/openstack-neutron.spec
new file mode 100644
index 0000000..6203f35
--- /dev/null
+++ b/openstack-neutron.spec
@@ -0,0 +1,827 @@
+%define gitPatch() \
+cd %1; \
+git init && git config user.name "openstack-plugin" && git config user.email "openstack-plugin"; \
+git add . && git commit -m "openstack-plugin init"; \
+git apply --check %2 || exit 1 && git apply %2; \
+git add . && git commit -m "openstack-plugin patch"
+
+%define gitUnPatch() \
+cd %1;\
+git reset --hard HEAD~;\
+rm -rf %1/.git
+
+%{!?upstream_version: %global upstream_version %{version}%{?milestone}}
+%global service neutron
+
+%define cleanup_orphan_rootwrap_daemons() \
+for pid in $(ps -f --ppid 1 | awk '/.*neutron-rootwrap-daemon/ { print $2 }'); do \
+ kill $(ps --ppid $pid -o pid=) \
+done \
+%nil
+
+%global common_desc \
+Neutron is a virtual network service for Openstack. Just like \
+OpenStack Nova provides an API to dynamically request and configure \
+virtual servers, Neutron provides an API to dynamically request and \
+configure virtual networks. These networks connect "interfaces" from \
+other OpenStack services (e.g., virtual NICs from Nova VMs). The \
+Neutron API supports extensions to provide advanced network \
+capabilities (e.g., QoS, ACLs, network monitoring, etc.)
+
+Name: openstack-%{service}
+Version: 15.3.4
+Release: 3
+Summary: OpenStack Networking Service
+
+License: ASL 2.0
+URL: http://launchpad.net/%{service}/
+
+Source0: https://tarballs.openstack.org/%{service}/%{service}-%{upstream_version}.tar.gz
+
+Source1: %{service}.logrotate
+Source2: %{service}-sudoers
+Source10: neutron-server.service
+Source11: neutron-linuxbridge-agent.service
+Source12: neutron-openvswitch-agent.service
+Source15: neutron-dhcp-agent.service
+Source16: neutron-l3-agent.service
+Source17: neutron-metadata-agent.service
+Source18: neutron-ovs-cleanup.service
+Source19: neutron-macvtap-agent.service
+Source20: neutron-metering-agent.service
+Source21: neutron-sriov-nic-agent.service
+Source22: neutron-netns-cleanup.service
+Source29: neutron-rpc-server.service
+
+Source30: %{service}-dist.conf
+Source31: conf.README
+Source32: neutron-linuxbridge-cleanup.service
+Source33: neutron-enable-bridge-firewall.sh
+Source34: neutron-l2-agent-sysctl.conf
+# We use the legacy service to load modules because it allows to gracefully
+# ignore a missing kernel module (f.e. br_netfilter on earlier kernels). It's
+# essentially because .modules files are shell scripts.
+Source35: neutron-l2-agent.modules
+Source36: neutron-destroy-patch-ports.service
+Source37: 0001-add-distributed-traffic-feature-support.patch
+# Required for tarball sources verification
+
+BuildArch: noarch
+
+BuildRequires: git-core
+BuildRequires: openstack-macros
+BuildRequires: python3-devel
+BuildRequires: python3-babel
+BuildRequires: python3-keystoneauth1
+BuildRequires: python3-keystonemiddleware
+BuildRequires: python3-neutron-lib
+BuildRequires: python3-novaclient
+BuildRequires: python3-oslo-cache
+BuildRequires: python3-oslo-concurrency
+BuildRequires: python3-oslo-config
+BuildRequires: python3-oslo-db
+BuildRequires: python3-oslo-log
+BuildRequires: python3-oslo-messaging
+BuildRequires: python3-oslo-policy
+BuildRequires: python3-oslo-privsep
+BuildRequires: python3-oslo-rootwrap
+BuildRequires: python3-oslo-service
+BuildRequires: python3-oslo-upgradecheck
+BuildRequires: python3-oslo-versionedobjects
+BuildRequires: python3-osprofiler
+BuildRequires: python3-ovsdbapp
+BuildRequires: python3-pbr
+BuildRequires: python3-psutil
+BuildRequires: python3-pyroute2
+BuildRequires: python3-pecan
+BuildRequires: python3-tenacity
+BuildRequires: python3-os-vif
+BuildRequires: systemd
+
+
+Requires: openstack-%{service}-common = %{version}-%{release}
+
+# dnsmasq is not a hard requirement, but is currently the only option
+# when neutron-dhcp-agent is deployed.
+Requires: dnsmasq
+Requires: dnsmasq-utils
+
+# radvd is not a hard requirement, but is currently the only option
+# for IPv6 deployments.
+Requires: radvd
+
+# dibbler is not a hard requirement, but is currently the default option
+# for IPv6 prefix delegation.
+Requires: dibbler-client
+
+# conntrack is not a hard requirement, but is currently used by L3 agent
+# to immediately drop connections after a floating IP is disassociated
+Requires: conntrack-tools
+
+# keepalived is not a hard requirement, but is currently used by DVR L3
+# agent
+Requires: keepalived
+
+# haproxy implements metadata proxy process
+Requires: haproxy
+
+# Those are not hard requirements, ipset is used by ipset-cleanup in the subpackage,
+# iptables is used by the l3-agent which currently is not in a separate package,
+# iputils provides tools like arping which are used by l3-agent and iproute-tc
+# (or iproute in case of CentOS 7 and RHEL 7), provides tc binary which is
+# used by e.g. l3-agent and openvswitch-agent when QoS extension is enabled
+# in agent's config.
+Requires: ipset
+Requires: iptables
+Requires: iputils
+Requires: iproute-tc
+
+
+%{?systemd_ordering}
+
+
+
+%description
+%{common_desc}
+
+
+%package -n python3-%{service}
+Summary: Neutron Python libraries
+%{?python_provide:%python_provide python3-%{service}}
+Requires: python3-alembic
+Requires: python3-debtcollector
+Requires: python3-designateclient
+Requires: python3-eventlet
+Requires: python3-greenlet
+Requires: python3-futurist
+Requires: python3-jinja2
+Requires: python3-keystoneauth1
+Requires: python3-keystonemiddleware
+Requires: python3-netaddr
+Requires: python3-neutronclient
+Requires: python3-neutron-lib
+Requires: python3-novaclient
+Requires: python3-os-vif
+Requires: python3-oslo-cache
+Requires: python3-oslo-concurrency
+Requires: python3-oslo-config
+Requires: python3-oslo-context
+Requires: python3-oslo-db
+Requires: python3-oslo-i18n
+Requires: python3-oslo-log
+Requires: python3-oslo-messaging
+Requires: python3-oslo-middleware
+Requires: python3-oslo-policy
+Requires: python3-oslo-privsep
+Requires: python3-oslo-reports
+Requires: python3-oslo-rootwrap
+Requires: python3-oslo-serialization
+Requires: python3-oslo-service
+Requires: python3-oslo-upgradecheck
+Requires: python3-oslo-utils
+Requires: python3-oslo-versionedobjects
+Requires: python3-osprofiler
+Requires: python3-ovsdbapp
+Requires: python3-pecan
+Requires: python3-pbr
+Requires: python3-psutil
+Requires: python3-pyroute2
+Requires: python3-requests
+Requires: python3-tenacity
+Requires: python3-routes
+Requires: python3-os-ken
+Requires: python3-sqlalchemy
+Requires: python3-stevedore
+Requires: python3-tooz
+Requires: python3-webob
+Requires: python3-openstacksdk
+Requires: python3-pyOpenSSL
+Requires: python3-packaging
+
+Requires: python3-httplib2
+Requires: python3-netifaces
+Requires: python3-paste
+Requires: python3-paste-deploy
+Requires: python3-decorator
+Requires: python3-os-xenapi
+
+
+%description -n python3-%{service}
+%{common_desc}
+
+This package contains the Neutron Python library.
+
+
+%package -n python3-%{service}-tests
+Summary: Neutron tests
+%{?python_provide:%python_provide python3-%{service}-tests}
+Requires: python3-%{service} = %{version}-%{release}
+Requires: python3-ddt
+Requires: python3-fixtures
+Requires: python3-mock
+Requires: python3-subunit
+Requires: python3-testrepository
+Requires: python3-testtools
+Requires: python3-testresources
+Requires: python3-testscenarios
+Requires: python3-oslotest
+Requires: python3-os-testr
+Requires: python3-PyMySQL
+Requires: python3-tempest
+
+Requires: python3-webtest
+
+
+# pstree is used during functional testing to ensure our internal
+# libraries managing processes work correctly.
+Requires: psmisc
+# nfs-utils is needed because it creates user with uid 65534 which
+# is required by neutron functional tests.
+Requires: nfs-utils
+
+
+%description -n python3-%{service}-tests
+%{common_desc}
+
+This package contains Neutron test files.
+
+
+%package common
+Summary: Neutron common files
+Requires(pre): shadow-utils
+Requires: python3-%{service} = %{version}-%{release}
+Requires: sudo
+
+
+%description common
+%{common_desc}
+
+This package contains Neutron common files.
+
+
+%package linuxbridge
+Summary: Neutron Linuxbridge agent
+Requires: ebtables
+Requires: ipset
+Requires: iproute
+Requires: iptables
+Requires: conntrack-tools
+# kmod is needed to get access to /usr/sbin/modprobe needed by
+# neutron-enable-bridge-firewall.sh triggered by the service unit file
+Requires: kmod
+Requires: openstack-%{service}-common = %{version}-%{release}
+
+
+%description linuxbridge
+%{common_desc}
+
+This package contains the Neutron agent that implements virtual
+networks using VLAN or VXLAN using Linuxbridge technology.
+
+
+%package macvtap-agent
+Summary: Neutron macvtap agent
+Requires: openstack-%{service}-common = %{version}-%{release}
+
+
+%description macvtap-agent
+%{common_desc}
+
+This package contains the Neutron agent that implements
+macvtap attachments for libvirt qemu/kvm instances.
+
+
+%package ml2
+Summary: Neutron ML2 plugin
+Requires: openstack-%{service}-common = %{version}-%{release}
+# needed for brocade and cisco drivers
+#(TODO) ncclient is not in reuirement projects so it should be requirement in neutron
+# plugin packages, not in main neutron. Remove this lines completely if everythin keeps
+# working.
+#Requires: python3-ncclient
+
+
+%description ml2
+%{common_desc}
+
+This package contains a Neutron plugin that allows the use of drivers
+to support separately extensible sets of network types and the mechanisms
+for accessing those types.
+
+
+%package openvswitch
+Summary: Neutron openvswitch plugin
+Requires: openstack-%{service}-common = %{version}-%{release}
+# We require openvswitch when using vsctl to access ovsdb;
+# but if we use native access, then we just need python bindings.
+# since we don't know what users actually use, we depend on both.
+Requires: ipset
+Requires: iptables
+Requires: openvswitch
+Requires: python3-openvswitch
+# kmod is needed to get access to /usr/sbin/modprobe needed by
+# neutron-enable-bridge-firewall.sh triggered by the service unit file
+Requires: kmod
+
+
+%description openvswitch
+%{common_desc}
+
+This package contains the Neutron plugin that implements virtual
+networks using Open vSwitch.
+
+
+%package metering-agent
+Summary: Neutron bandwidth metering agent
+Requires: iptables
+Requires: openstack-%{service}-common = %{version}-%{release}
+
+
+%description metering-agent
+%{common_desc}
+
+This package contains the Neutron agent responsible for generating bandwidth
+utilization notifications.
+
+
+%package rpc-server
+Summary: Neutron (RPC only) Server
+Requires: openstack-%{service}-common = %{version}-%{release}
+
+
+%description rpc-server
+%{common_desc}
+
+This package contains an alternative Neutron server that handles AMQP RPC
+workload only.
+
+
+%package sriov-nic-agent
+Summary: Neutron SR-IOV NIC agent
+Requires: openstack-%{service}-common = %{version}-%{release}
+
+
+%description sriov-nic-agent
+%{common_desc}
+
+This package contains the Neutron agent to support advanced features of
+SR-IOV network cards.
+
+%package distributed-traffic
+Summary: The plug-in package of openstack-neutron for router gateway portforwarding feature
+Requires: git
+Requires: python3-crudini
+Requires: openstack-neutron
+
+%description distributed-traffic
+The plug-in package of openstack-neutron for router gateway portforwarding feature
+
+
+%prep
+%autosetup -n %{service}-%{upstream_version} -S git
+sed -i 's/\/usr\/bin\/python/\/usr\/bin\/python3/' %{SOURCE36}
+
+find %{service} -name \*.py -exec sed -i '/\/usr\/bin\/env python/{d;q}' {} +
+
+# Let's handle dependencies ourseleves
+%py_req_cleanup
+
+# Kill egg-info in order to generate new SOURCES.txt
+rm -rf neutron.egg-info
+
+
+%build
+export SKIP_PIP_INSTALL=1
+%{py3_build}
+# Generate i18n files
+# (amoralej) we can remove '-D neutron' once https://review.openstack.org/#/c/485070/ is merged
+%{__python3} setup.py compile_catalog -d build/lib/%{service}/locale -D neutron
+
+# Generate configuration files
+PYTHONPATH=.
+for file in `ls etc/oslo-config-generator/*`; do
+ oslo-config-generator --config-file=$file
+done
+
+find etc -name *.sample | while read filename
+do
+ filedir=$(dirname $filename)
+ file=$(basename $filename .sample)
+ mv ${filename} ${filedir}/${file}
+done
+
+# Loop through values in neutron-dist.conf and make sure that the values
+# are substituted into the neutron.conf as comments. Some of these values
+# will have been uncommented as a way of upstream setting defaults outside
+# of the code. For notification_driver, there are commented examples
+# above uncommented settings, so this specifically skips those comments
+# and instead comments out the actual settings and substitutes the
+# correct default values.
+while read name eq value; do
+ test "$name" && test "$value" || continue
+ if [ "$name" = "notification_driver" ]; then
+ sed -ri "0,/^$name *=/{s!^$name *=.*!# $name = $value!}" etc/%{service}.conf
+ else
+ sed -ri "0,/^(#)? *$name *=/{s!^(#)? *$name *=.*!# $name = $value!}" etc/%{service}.conf
+ fi
+done < %{SOURCE30}
+
+%install
+%{py3_install}
+
+# Remove unused files
+rm -rf %{buildroot}%{python3_sitelib}/bin
+rm -rf %{buildroot}%{python3_sitelib}/doc
+rm -rf %{buildroot}%{python3_sitelib}/tools
+
+# Move rootwrap files to proper location
+install -d -m 755 %{buildroot}%{_datarootdir}/%{service}/rootwrap
+mv %{buildroot}/usr/etc/%{service}/rootwrap.d/*.filters %{buildroot}%{_datarootdir}/%{service}/rootwrap
+
+# Move config files to proper location
+install -d -m 755 %{buildroot}%{_sysconfdir}/%{service}
+mv %{buildroot}/usr/etc/%{service}/* %{buildroot}%{_sysconfdir}/%{service}
+mv %{buildroot}%{_sysconfdir}/%{service}/api-paste.ini %{buildroot}%{_datadir}/%{service}/api-paste.ini
+
+# The generated config files are not moved automatically by setup.py
+install -d -m 755 %{buildroot}%{_sysconfdir}/%{service}/plugins/ml2
+
+mv etc/%{service}.conf %{buildroot}%{_sysconfdir}/%{service}/%{service}.conf
+for agent in dhcp l3 metadata metering
+do
+ mv etc/${agent}_agent.ini %{buildroot}%{_sysconfdir}/%{service}/${agent}_agent.ini
+done
+for file in linuxbridge_agent ml2_conf openvswitch_agent sriov_agent
+do
+ mv etc/%{service}/plugins/ml2/${file}.ini %{buildroot}%{_sysconfdir}/%{service}/plugins/ml2/${file}.ini
+done
+
+
+
+
+# Install logrotate
+install -p -D -m 644 %{SOURCE1} %{buildroot}%{_sysconfdir}/logrotate.d/openstack-%{service}
+
+# Install sudoers
+install -p -D -m 440 %{SOURCE2} %{buildroot}%{_sysconfdir}/sudoers.d/%{service}
+
+# Install systemd units
+install -p -D -m 644 %{SOURCE10} %{buildroot}%{_unitdir}/neutron-server.service
+install -p -D -m 644 %{SOURCE11} %{buildroot}%{_unitdir}/neutron-linuxbridge-agent.service
+install -p -D -m 644 %{SOURCE12} %{buildroot}%{_unitdir}/neutron-openvswitch-agent.service
+install -p -D -m 644 %{SOURCE15} %{buildroot}%{_unitdir}/neutron-dhcp-agent.service
+install -p -D -m 644 %{SOURCE16} %{buildroot}%{_unitdir}/neutron-l3-agent.service
+install -p -D -m 644 %{SOURCE17} %{buildroot}%{_unitdir}/neutron-metadata-agent.service
+install -p -D -m 644 %{SOURCE18} %{buildroot}%{_unitdir}/neutron-ovs-cleanup.service
+install -p -D -m 644 %{SOURCE19} %{buildroot}%{_unitdir}/neutron-macvtap-agent.service
+install -p -D -m 644 %{SOURCE20} %{buildroot}%{_unitdir}/neutron-metering-agent.service
+install -p -D -m 644 %{SOURCE21} %{buildroot}%{_unitdir}/neutron-sriov-nic-agent.service
+install -p -D -m 644 %{SOURCE22} %{buildroot}%{_unitdir}/neutron-netns-cleanup.service
+install -p -D -m 644 %{SOURCE29} %{buildroot}%{_unitdir}/neutron-rpc-server.service
+install -p -D -m 644 %{SOURCE32} %{buildroot}%{_unitdir}/neutron-linuxbridge-cleanup.service
+install -p -D -m 644 %{SOURCE36} %{buildroot}%{_unitdir}/neutron-destroy-patch-ports.service
+
+
+# Install helper scripts
+install -p -D -m 755 %{SOURCE33} %{buildroot}%{_bindir}/neutron-enable-bridge-firewall.sh
+
+# Install sysctl and modprobe config files to enable bridge firewalling
+# NOTE(ihrachys) we effectively duplicate same settings for each affected l2
+# agent. This can be revisited later.
+install -p -D -m 644 %{SOURCE34} %{buildroot}%{_sysctldir}/99-neutron-openvswitch-agent.conf
+install -p -D -m 644 %{SOURCE34} %{buildroot}%{_sysctldir}/99-neutron-linuxbridge-agent.conf
+install -p -D -m 755 %{SOURCE35} %{buildroot}%{_sysconfdir}/sysconfig/modules/neutron-openvswitch-agent.modules
+install -p -D -m 755 %{SOURCE35} %{buildroot}%{_sysconfdir}/sysconfig/modules/neutron-linuxbridge-agent.modules
+
+# Install README file that describes how to configure services with custom configuration files
+install -p -D -m 755 %{SOURCE31} %{buildroot}%{_sysconfdir}/%{service}/conf.d/README
+
+# Setup directories
+install -d -m 755 %{buildroot}%{_datadir}/%{service}
+install -d -m 755 %{buildroot}%{_sharedstatedir}/%{service}
+install -d -m 755 %{buildroot}%{_localstatedir}/log/%{service}
+install -d -m 755 %{buildroot}%{_localstatedir}/run/%{service}
+install -d -m 755 %{buildroot}%{_sysconfdir}/%{service}/kill_scripts
+
+# Install dist conf
+install -p -D -m 640 %{SOURCE30} %{buildroot}%{_datadir}/%{service}/%{service}-dist.conf
+
+# Create and populate configuration directory for L3 agent that is not accessible for user modification
+mkdir -p %{buildroot}%{_datadir}/%{service}/l3_agent
+ln -s %{_sysconfdir}/%{service}/l3_agent.ini %{buildroot}%{_datadir}/%{service}/l3_agent/l3_agent.conf
+
+# Create dist configuration directory for neutron-server (may be filled by advanced services)
+mkdir -p %{buildroot}%{_datadir}/%{service}/server
+
+# Create configuration directories for all services that can be populated by users with custom *.conf files
+mkdir -p %{buildroot}/%{_sysconfdir}/%{service}/conf.d/common
+for service in server rpc-server ovs-cleanup netns-cleanup linuxbridge-cleanup macvtap-agent; do
+ mkdir -p %{buildroot}/%{_sysconfdir}/%{service}/conf.d/%{service}-$service
+done
+for service in linuxbridge openvswitch dhcp l3 metadata metering sriov-nic; do
+ mkdir -p %{buildroot}/%{_sysconfdir}/%{service}/conf.d/%{service}-$service-agent
+done
+
+# Install i18n .mo files (.po and .pot are not required)
+install -d -m 755 %{buildroot}%{_datadir}
+rm -f %{buildroot}%{python3_sitelib}/%{service}/locale/*/LC_*/%{service}*po
+rm -f %{buildroot}%{python3_sitelib}/%{service}/locale/*pot
+mv %{buildroot}%{python3_sitelib}/%{service}/locale %{buildroot}%{_datadir}/locale
+
+# Install router gateway portforwarding feature patch
+install -D -p -m 644 %{SOURCE37} %{buildroot}%{python3_sitelib}/openstack-plugin/neutron/$(basename %{SOURCE37})
+
+# Find language files
+%find_lang %{service} --all-name
+
+%pre common
+getent group %{service} >/dev/null || groupadd -r %{service}
+getent passwd %{service} >/dev/null || \
+ useradd -r -g %{service} -d %{_sharedstatedir}/%{service} -s /sbin/nologin \
+ -c "OpenStack Neutron Daemons" %{service}
+exit 0
+
+
+%post
+%systemd_post neutron-dhcp-agent.service
+%systemd_post neutron-l3-agent.service
+%systemd_post neutron-metadata-agent.service
+%systemd_post neutron-server.service
+%systemd_post neutron-netns-cleanup.service
+%systemd_post neutron-ovs-cleanup.service
+%systemd_post neutron-linuxbridge-cleanup.service
+
+
+%preun
+%systemd_preun neutron-dhcp-agent.service
+%systemd_preun neutron-l3-agent.service
+%systemd_preun neutron-metadata-agent.service
+%systemd_preun neutron-server.service
+%systemd_preun neutron-netns-cleanup.service
+%systemd_preun neutron-ovs-cleanup.service
+%systemd_preun neutron-linuxbridge-cleanup.service
+
+
+%postun
+%systemd_postun_with_restart neutron-dhcp-agent.service
+%systemd_postun_with_restart neutron-l3-agent.service
+%systemd_postun_with_restart neutron-metadata-agent.service
+%systemd_postun_with_restart neutron-server.service
+%cleanup_orphan_rootwrap_daemons
+
+
+%post macvtap-agent
+%systemd_post neutron-macvtap-agent.service
+
+
+%preun macvtap-agent
+%systemd_preun neutron-macvtap-agent.service
+
+
+%postun macvtap-agent
+%systemd_postun_with_restart neutron-macvtap-agent.service
+%cleanup_orphan_rootwrap_daemons
+
+
+%post linuxbridge
+%systemd_post neutron-linuxbridge-agent.service
+
+
+%preun linuxbridge
+%systemd_preun neutron-linuxbridge-agent.service
+
+
+%postun linuxbridge
+%systemd_postun_with_restart neutron-linuxbridge-agent.service
+%cleanup_orphan_rootwrap_daemons
+
+%post openvswitch
+%systemd_post neutron-openvswitch-agent.service
+%systemd_post neutron-destroy-patch-ports.service
+
+if [ $1 -ge 2 ]; then
+ # We're upgrading
+
+ # Detect if the neutron-openvswitch-agent is running
+ ovs_agent_running=0
+ systemctl status neutron-openvswitch-agent > /dev/null 2>&1 && ovs_agent_running=1 || :
+
+ # If agent is running, stop it
+ [ $ovs_agent_running -eq 1 ] && systemctl stop neutron-openvswitch-agent > /dev/null 2>&1 || :
+
+ # Search all orphaned neutron-rootwrap-daemon processes and since all are triggered by sudo,
+ # get the actual rootwrap-daemon process.
+ %cleanup_orphan_rootwrap_daemons
+
+ # If agent was running, start it back with new code
+ [ $ovs_agent_running -eq 1 ] && systemctl start neutron-openvswitch-agent > /dev/null 2>&1 || :
+fi
+
+
+%preun openvswitch
+%systemd_preun neutron-openvswitch-agent.service
+%systemd_preun neutron-destroy-patch-ports.service
+
+
+%post metering-agent
+%systemd_post neutron-metering-agent.service
+
+
+%preun metering-agent
+%systemd_preun neutron-metering-agent.service
+
+
+%postun metering-agent
+%systemd_postun_with_restart neutron-metering-agent.service
+%cleanup_orphan_rootwrap_daemons
+
+
+%post sriov-nic-agent
+%systemd_post neutron-sriov-nic-agent.service
+
+
+%preun sriov-nic-agent
+%systemd_preun neutron-sriov-nic-agent.service
+
+
+%postun sriov-nic-agent
+%systemd_postun_with_restart neutron-sriov-nic-agent.service
+%cleanup_orphan_rootwrap_daemons
+
+
+%post distributed-traffic
+export patch_name=$(basename %{SOURCE37})
+%gitPatch %{python3_sitelib}/neutron %{python3_sitelib}/openstack-plugin/neutron/$patch_name
+crudini --set %{python3_sitelib}/neutron-*.egg-info/entry_points.txt neutron.objects RGPortForwarding neutron.objects.rg_port_forwarding:RGPortForwarding
+crudini --set %{python3_sitelib}/neutron-*.egg-info/entry_points.txt neutron.service_plugins rg_port_forwarding neutron.services.rg_portforwarding.pf_plugin:RGPortForwardingPlugin
+crudini --set %{python3_sitelib}/neutron-*.egg-info/entry_points.txt neutron.agent.l3.extensions rg_port_forwarding neutron.agent.l3.extensions.rg_port_forwarding:RGPortForwardingAgentExtension
+
+
+%preun distributed-traffic
+%gitUnPatch %{python3_sitelib}/neutron
+crudini --del %{python3_sitelib}/neutron-*.egg-info/entry_points.txt neutron.objects RGPortForwarding
+crudini --del %{python3_sitelib}/neutron-*.egg-info/entry_points.txt neutron.service_plugins rg_port_forwarding
+crudini --del %{python3_sitelib}/neutron-*.egg-info/entry_points.txt neutron.agent.l3.extensions rg_port_forwarding
+
+
+%files
+%license LICENSE
+%{_bindir}/neutron-api
+%{_bindir}/neutron-db-manage
+%{_bindir}/neutron-debug
+%{_bindir}/neutron-dhcp-agent
+%{_bindir}/neutron-ipset-cleanup
+%{_bindir}/neutron-keepalived-state-change
+%{_bindir}/neutron-l3-agent
+%{_bindir}/neutron-linuxbridge-cleanup
+%{_bindir}/neutron-metadata-agent
+%{_bindir}/neutron-netns-cleanup
+%{_bindir}/neutron-ovs-cleanup
+%{_bindir}/neutron-pd-notify
+%{_bindir}/neutron-sanity-check
+%{_bindir}/neutron-status
+%{_bindir}/neutron-server
+%{_bindir}/neutron-usage-audit
+%{_bindir}/neutron-rootwrap-xen-dom0
+%{_unitdir}/neutron-dhcp-agent.service
+%{_unitdir}/neutron-l3-agent.service
+%{_unitdir}/neutron-metadata-agent.service
+%{_unitdir}/neutron-server.service
+%{_unitdir}/neutron-netns-cleanup.service
+%{_unitdir}/neutron-ovs-cleanup.service
+%{_unitdir}/neutron-linuxbridge-cleanup.service
+%attr(-, root, %{service}) %{_datadir}/%{service}/api-paste.ini
+%dir %{_datadir}/%{service}/l3_agent
+%dir %{_datadir}/%{service}/server
+%{_datadir}/%{service}/l3_agent/*.conf
+%config(noreplace) %attr(0640, root, %{service}) %{_sysconfdir}/%{service}/dhcp_agent.ini
+%config(noreplace) %attr(0640, root, %{service}) %{_sysconfdir}/%{service}/l3_agent.ini
+%config(noreplace) %attr(0640, root, %{service}) %{_sysconfdir}/%{service}/metadata_agent.ini
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-dhcp-agent
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-l3-agent
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-metadata-agent
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-server
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-netns-cleanup
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-ovs-cleanup
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-linuxbridge-cleanup
+%dir %{_sysconfdir}/%{service}/kill_scripts
+
+
+%files -n python3-%{service}-tests
+%license LICENSE
+%{python3_sitelib}/%{service}/tests
+
+%files -n python3-%{service}
+%license LICENSE
+%{python3_sitelib}/%{service}
+%{python3_sitelib}/%{service}-*.egg-info
+%exclude %{python3_sitelib}/%{service}/tests
+
+
+%files common -f %{service}.lang
+%license LICENSE
+%doc README.rst
+# though this script is not exactly needed on all nodes but for ovs and
+# linuxbridge agents only, it's probably good enough to put it here
+%{_bindir}/neutron-enable-bridge-firewall.sh
+%{_bindir}/neutron-rootwrap
+%{_bindir}/neutron-rootwrap-daemon
+%dir %{_sysconfdir}/%{service}
+%{_sysconfdir}/%{service}/conf.d/README
+%dir %{_sysconfdir}/%{service}/conf.d
+%dir %{_sysconfdir}/%{service}/conf.d/common
+%dir %{_sysconfdir}/%{service}/plugins
+%attr(-, root, %{service}) %{_datadir}/%{service}/%{service}-dist.conf
+%config(noreplace) %attr(0640, root, %{service}) %{_sysconfdir}/%{service}/%{service}.conf
+%config(noreplace) %{_sysconfdir}/%{service}/rootwrap.conf
+%config(noreplace) %{_sysconfdir}/logrotate.d/*
+%{_sysconfdir}/sudoers.d/%{service}
+%dir %attr(0755, %{service}, %{service}) %{_sharedstatedir}/%{service}
+%dir %attr(0750, %{service}, %{service}) %{_localstatedir}/log/%{service}
+%dir %{_datarootdir}/%{service}
+%dir %{_datarootdir}/%{service}/rootwrap
+%{_datarootdir}/%{service}/rootwrap/debug.filters
+%{_datarootdir}/%{service}/rootwrap/dhcp.filters
+%{_datarootdir}/%{service}/rootwrap/dibbler.filters
+%{_datarootdir}/%{service}/rootwrap/ipset-firewall.filters
+%{_datarootdir}/%{service}/rootwrap/l3.filters
+%{_datarootdir}/%{service}/rootwrap/privsep.filters
+%{_datarootdir}/%{service}/rootwrap/ebtables.filters
+%{_datarootdir}/%{service}/rootwrap/iptables-firewall.filters
+
+
+%files linuxbridge
+%license LICENSE
+%{_bindir}/neutron-linuxbridge-agent
+%{_unitdir}/neutron-linuxbridge-agent.service
+%{_datarootdir}/%{service}/rootwrap/linuxbridge-plugin.filters
+%dir %{_sysconfdir}/%{service}/plugins/ml2
+%config(noreplace) %attr(0640, root, %{service}) %{_sysconfdir}/%{service}/plugins/ml2/linuxbridge_agent.ini
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-linuxbridge-agent
+%{_sysctldir}/99-neutron-linuxbridge-agent.conf
+%{_sysconfdir}/sysconfig/modules/neutron-linuxbridge-agent.modules
+
+
+%files macvtap-agent
+%license LICENSE
+%{_bindir}/neutron-macvtap-agent
+%{_unitdir}/neutron-macvtap-agent.service
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-macvtap-agent
+
+
+%files ml2
+%license LICENSE
+%doc %{service}/plugins/ml2/README
+%dir %{_sysconfdir}/%{service}/plugins/ml2
+%config(noreplace) %attr(0640, root, %{service}) %{_sysconfdir}/%{service}/plugins/ml2/*.ini
+%exclude %{_sysconfdir}/%{service}/plugins/ml2/linuxbridge_agent.ini
+%exclude %{_sysconfdir}/%{service}/plugins/ml2/openvswitch_agent.ini
+
+
+%files openvswitch
+%license LICENSE
+%{_bindir}/neutron-openvswitch-agent
+%{_unitdir}/neutron-openvswitch-agent.service
+%{_unitdir}/neutron-destroy-patch-ports.service
+%{_datarootdir}/%{service}/rootwrap/openvswitch-plugin.filters
+%dir %{_sysconfdir}/%{service}/plugins/ml2
+%config(noreplace) %attr(0640, root, %{service}) %{_sysconfdir}/%{service}/plugins/ml2/openvswitch_agent.ini
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-openvswitch-agent
+%{_sysctldir}/99-neutron-openvswitch-agent.conf
+%{_sysconfdir}/sysconfig/modules/neutron-openvswitch-agent.modules
+
+
+%files metering-agent
+%license LICENSE
+%config(noreplace) %attr(0640, root, %{service}) %{_sysconfdir}/%{service}/metering_agent.ini
+%{_unitdir}/neutron-metering-agent.service
+%{_bindir}/neutron-metering-agent
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-metering-agent
+
+
+%files rpc-server
+%license LICENSE
+%{_bindir}/neutron-rpc-server
+%{_unitdir}/neutron-rpc-server.service
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-rpc-server
+
+
+%files sriov-nic-agent
+%license LICENSE
+%{_unitdir}/neutron-sriov-nic-agent.service
+%{_bindir}/neutron-sriov-nic-agent
+%config(noreplace) %attr(0640, root, %{service}) %{_sysconfdir}/%{service}/plugins/ml2/sriov_agent.ini
+%dir %{_sysconfdir}/%{service}/conf.d/%{service}-sriov-nic-agent
+
+%files distributed-traffic
+%{python3_sitelib}/openstack-plugin/neutron/%{basename %{SOURCE37}}
+
+%changelog
+* Tue Oct 17 2023 wangkuntian <wangkuntian@uniontech.com> - 15.3.4-3
+- Add distributed traffic feature package
+
+* Tue Nov 23 2021 zhangy1317 <zhangy1317@foxmail.com> - 15.3.4-2
+- Fix install issue
+
+* Fri Nov 05 2021 wangxiyuan <wangxiyuan1007@gmail.com> 15.3.4-1
+- Support OpenStack Train release
diff --git a/sources b/sources
new file mode 100644
index 0000000..e72b4b6
--- /dev/null
+++ b/sources
@@ -0,0 +1 @@
+a9187146f6247e8ac3b3e87fd4fb70f1 neutron-15.3.4.tar.gz