2570 lines
117 KiB
Python
2570 lines
117 KiB
Python
# Copyright 2019 Red Hat, Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import collections
|
|
import copy
|
|
import datetime
|
|
|
|
import netaddr
|
|
from neutron_lib.api.definitions import l3
|
|
from neutron_lib.api.definitions import port_security as psec
|
|
from neutron_lib.api.definitions import portbindings
|
|
from neutron_lib.api.definitions import provider_net as pnet
|
|
from neutron_lib.api.definitions import segment as segment_def
|
|
from neutron_lib import constants as const
|
|
from neutron_lib import context as n_context
|
|
from neutron_lib import exceptions as n_exc
|
|
from neutron_lib.exceptions import l3 as l3_exc
|
|
from neutron_lib.plugins import constants as plugin_constants
|
|
from neutron_lib.plugins import directory
|
|
from neutron_lib.plugins import utils as p_utils
|
|
from neutron_lib.services.logapi import constants as log_const
|
|
from neutron_lib.utils import helpers
|
|
from neutron_lib.utils import net as n_net
|
|
from oslo_config import cfg
|
|
from oslo_log import log
|
|
from oslo_utils import excutils
|
|
from oslo_utils import timeutils
|
|
from ovsdbapp.backend.ovs_idl import idlutils
|
|
import tenacity
|
|
|
|
from neutron.common.ovn import acl as ovn_acl
|
|
from neutron.common.ovn import constants as ovn_const
|
|
from neutron.common.ovn import utils
|
|
from neutron.common import utils as common_utils
|
|
from neutron.conf.plugins.ml2.drivers.ovn import ovn_conf
|
|
from neutron.db import ovn_revision_numbers_db as db_rev
|
|
from neutron.db import segments_db
|
|
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
|
|
import placement as placement_extension
|
|
from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb.extensions \
|
|
import qos as qos_extension
|
|
from neutron.scheduler import l3_ovn_scheduler
|
|
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
OvnPortInfo = collections.namedtuple(
|
|
"OvnPortInfo",
|
|
[
|
|
"type",
|
|
"options",
|
|
"addresses",
|
|
"port_security",
|
|
"parent_name",
|
|
"tag",
|
|
"dhcpv4_options",
|
|
"dhcpv6_options",
|
|
"cidrs",
|
|
"device_owner",
|
|
"security_group_ids",
|
|
"address4_scope_id",
|
|
"address6_scope_id",
|
|
"vnic_type",
|
|
"capabilities",
|
|
],
|
|
)
|
|
|
|
|
|
GW_INFO = collections.namedtuple('GatewayInfo', ['network_id', 'subnet_id',
|
|
'router_ip', 'gateway_ip',
|
|
'ip_version', 'ip_prefix'])
|
|
|
|
|
|
class OVNClient(object):
|
|
|
|
def __init__(self, nb_idl, sb_idl):
|
|
self._nb_idl = nb_idl
|
|
self._sb_idl = sb_idl
|
|
|
|
self._plugin_property = None
|
|
self._l3_plugin_property = None
|
|
|
|
# TODO(ralonsoh): handle the OVN client extensions with an ext. manager
|
|
self._qos_driver = qos_extension.OVNClientQosExtension(driver=self)
|
|
self.placement_extension = (
|
|
placement_extension.OVNClientPlacementExtension(self))
|
|
self._ovn_scheduler = l3_ovn_scheduler.get_scheduler()
|
|
|
|
@property
|
|
def _plugin(self):
|
|
if self._plugin_property is None:
|
|
self._plugin_property = directory.get_plugin()
|
|
return self._plugin_property
|
|
|
|
@property
|
|
def _l3_plugin(self):
|
|
if self._l3_plugin_property is None:
|
|
self._l3_plugin_property = directory.get_plugin(
|
|
plugin_constants.L3)
|
|
return self._l3_plugin_property
|
|
|
|
def _transaction(self, commands, txn=None):
|
|
"""Create a new transaction or add the commands to an existing one."""
|
|
if txn is None:
|
|
with self._nb_idl.transaction(check_error=True) as new_txn:
|
|
for cmd in commands:
|
|
new_txn.add(cmd)
|
|
else:
|
|
for cmd in commands:
|
|
txn.add(cmd)
|
|
|
|
def is_external_ports_supported(self):
|
|
return self._nb_idl.is_col_present(
|
|
'Logical_Switch_Port', 'ha_chassis_group')
|
|
|
|
# TODO(ihrachys) remove when min OVN version >= 21.06
|
|
def is_allow_stateless_supported(self):
|
|
return self._nb_idl.is_col_supports_value('ACL', 'action',
|
|
'allow-stateless')
|
|
|
|
def _get_allowed_addresses_from_port(self, port):
|
|
if not port.get(psec.PORTSECURITY):
|
|
return [], []
|
|
|
|
if utils.is_lsp_trusted(port):
|
|
return [], []
|
|
|
|
allowed_addresses = set()
|
|
new_macs = set()
|
|
addresses = port['mac_address']
|
|
for ip in port.get('fixed_ips', []):
|
|
addresses += ' ' + ip['ip_address']
|
|
|
|
for allowed_address in port.get('allowed_address_pairs', []):
|
|
# If allowed address pair has same mac as the port mac,
|
|
# append the allowed ip address to the 'addresses'.
|
|
# Else we will have multiple entries for the same mac in
|
|
# 'Logical_Switch_Port.port_security'.
|
|
if allowed_address['mac_address'] == port['mac_address']:
|
|
addresses += ' ' + allowed_address['ip_address']
|
|
else:
|
|
allowed_addresses.add(allowed_address['mac_address'] + ' ' +
|
|
allowed_address['ip_address'])
|
|
new_macs.add(allowed_address['mac_address'])
|
|
|
|
allowed_addresses.add(addresses)
|
|
|
|
return list(allowed_addresses), list(new_macs)
|
|
|
|
def _get_subnet_dhcp_options_for_port(self, port, ip_version):
|
|
"""Returns the subnet dhcp options for the port.
|
|
|
|
Return the first found DHCP options belong for the port.
|
|
"""
|
|
subnets = [
|
|
fixed_ip['subnet_id']
|
|
for fixed_ip in port['fixed_ips']
|
|
if netaddr.IPAddress(fixed_ip['ip_address']).version == ip_version]
|
|
get_opts = self._nb_idl.get_subnets_dhcp_options(subnets)
|
|
if get_opts:
|
|
if ip_version == const.IP_VERSION_6:
|
|
# Always try to find a dhcpv6 stateful v6 subnet to return.
|
|
# This ensures port can get one stateful v6 address when port
|
|
# has multiple dhcpv6 stateful and stateless subnets.
|
|
for opts in get_opts:
|
|
# We are setting ovn_const.DHCPV6_STATELESS_OPT to "true"
|
|
# in _get_ovn_dhcpv6_opts, so entries in DHCP_Options table
|
|
# should have unicode type 'true' if they were defined as
|
|
# dhcpv6 stateless.
|
|
if opts['options'].get(
|
|
ovn_const.DHCPV6_STATELESS_OPT) != 'true':
|
|
return opts
|
|
return get_opts[0]
|
|
|
|
def _get_port_dhcp_options(self, port, ip_version):
|
|
"""Return dhcp options for port.
|
|
|
|
In case the port is dhcp disabled, or IP addresses it has belong
|
|
to dhcp disabled subnets, returns None.
|
|
Otherwise, returns a dict:
|
|
- with content from a existing DHCP_Options row for subnet, if the
|
|
port has no extra dhcp options.
|
|
- with only one item ('cmd', AddDHCPOptionsCommand(..)), if the port
|
|
has extra dhcp options. The command should be processed in the same
|
|
transaction with port creating or updating command to avoid orphan
|
|
row issue happen.
|
|
"""
|
|
lsp_dhcp_disabled, lsp_dhcp_opts = utils.get_lsp_dhcp_opts(
|
|
port, ip_version)
|
|
|
|
if lsp_dhcp_disabled:
|
|
return
|
|
|
|
subnet_dhcp_options = self._get_subnet_dhcp_options_for_port(
|
|
port, ip_version)
|
|
|
|
if not subnet_dhcp_options:
|
|
# NOTE(lizk): It's possible for Neutron to configure a port with IP
|
|
# address belongs to subnet disabled dhcp. And no DHCP_Options row
|
|
# will be inserted for such a subnet. So in that case, the subnet
|
|
# dhcp options here will be None.
|
|
return
|
|
|
|
if not lsp_dhcp_opts:
|
|
return subnet_dhcp_options
|
|
|
|
# This port has extra DHCP options defined, so we will create a new
|
|
# row in DHCP_Options table for it.
|
|
subnet_dhcp_options['options'].update(lsp_dhcp_opts)
|
|
subnet_dhcp_options['external_ids'].update(
|
|
{'port_id': port['id']})
|
|
subnet_id = subnet_dhcp_options['external_ids']['subnet_id']
|
|
add_dhcp_opts_cmd = self._nb_idl.add_dhcp_options(
|
|
subnet_id, port_id=port['id'],
|
|
cidr=subnet_dhcp_options['cidr'],
|
|
options=subnet_dhcp_options['options'],
|
|
external_ids=subnet_dhcp_options['external_ids'])
|
|
return {'cmd': add_dhcp_opts_cmd}
|
|
|
|
def _get_port_options(self, port):
|
|
context = n_context.get_admin_context()
|
|
bp_info = utils.validate_and_get_data_from_binding_profile(port)
|
|
vtep_physical_switch = bp_info.bp_param.get('vtep-physical-switch')
|
|
|
|
port_type = ''
|
|
cidrs = ''
|
|
address4_scope_id = ""
|
|
address6_scope_id = ""
|
|
dhcpv4_options = self._get_port_dhcp_options(port, const.IP_VERSION_4)
|
|
dhcpv6_options = self._get_port_dhcp_options(port, const.IP_VERSION_6)
|
|
if vtep_physical_switch:
|
|
vtep_logical_switch = bp_info.bp_param.get('vtep-logical-switch')
|
|
port_type = 'vtep'
|
|
options = {'vtep-physical-switch': vtep_physical_switch,
|
|
'vtep-logical-switch': vtep_logical_switch}
|
|
addresses = [ovn_const.UNKNOWN_ADDR]
|
|
parent_name = []
|
|
tag = []
|
|
port_security = []
|
|
else:
|
|
options = {}
|
|
parent_name = bp_info.bp_param.get('parent_name', [])
|
|
tag = bp_info.bp_param.get('tag', [])
|
|
address = port['mac_address']
|
|
|
|
ip_subnets = port.get('fixed_ips', [])
|
|
subnet_ids = [
|
|
ip['subnet_id']
|
|
for ip in ip_subnets
|
|
if 'subnet_id' in ip
|
|
]
|
|
subnets = self._plugin.get_subnets(
|
|
context, filters={'id': subnet_ids})
|
|
address4_scope_id, address6_scope_id = (
|
|
utils.get_subnets_address_scopes(context, subnets, ip_subnets,
|
|
self._plugin))
|
|
p_type, virtual_ip, virtual_parents = (
|
|
utils.get_port_type_virtual_and_parents(
|
|
subnets, ip_subnets, port['network_id'], port['id'],
|
|
self._nb_idl))
|
|
if p_type:
|
|
port_type = ovn_const.LSP_TYPE_VIRTUAL
|
|
options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY] = virtual_ip
|
|
options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY] = (
|
|
virtual_parents)
|
|
|
|
if subnets:
|
|
for ip in ip_subnets:
|
|
ip_addr = ip['ip_address']
|
|
address += ' ' + ip_addr
|
|
|
|
try:
|
|
subnet = [
|
|
sub
|
|
for sub in subnets
|
|
if sub["id"] == ip["subnet_id"]
|
|
][0]
|
|
except IndexError:
|
|
LOG.debug('Subnet not found for ip address %s',
|
|
ip_addr)
|
|
continue
|
|
|
|
cidrs += ' {}/{}'.format(ip['ip_address'],
|
|
subnet['cidr'].split('/')[1])
|
|
|
|
# Metadata port.
|
|
if port['device_owner'] == const.DEVICE_OWNER_DISTRIBUTED:
|
|
port_type = ovn_const.LSP_TYPE_LOCALPORT
|
|
|
|
if utils.is_port_external(port):
|
|
if self.is_external_ports_supported():
|
|
port_type = ovn_const.LSP_TYPE_EXTERNAL
|
|
else:
|
|
LOG.warning('The version of OVN used does not support '
|
|
'the "external ports" feature used for '
|
|
'SR-IOV ports with OVN native DHCP')
|
|
addresses = []
|
|
port_security, new_macs = (
|
|
self._get_allowed_addresses_from_port(port))
|
|
# TODO(egarciar): OVN supports MAC learning from v21.03. This
|
|
# if-else block is stated so as to keep compatibility with older
|
|
# OVN versions and should be removed in the future.
|
|
if self._sb_idl.is_table_present('FDB'):
|
|
if (port_security or port_type or dhcpv4_options or
|
|
dhcpv6_options):
|
|
addresses.append(address)
|
|
addresses.extend(new_macs)
|
|
else:
|
|
addresses = [address]
|
|
addresses.extend(new_macs)
|
|
|
|
if not port_security and not port_type:
|
|
# Port security is disabled for this port.
|
|
# So this port can send traffic with any mac address.
|
|
# OVN allows any mac address from a port if "unknown"
|
|
# is added to the Logical_Switch_Port.addresses column.
|
|
# So add it.
|
|
addresses.append(ovn_const.UNKNOWN_ADDR)
|
|
|
|
# HA Chassis Group will bind the port to the highest
|
|
# priority Chassis
|
|
if port_type != ovn_const.LSP_TYPE_EXTERNAL:
|
|
if (bp_info.vnic_type == portbindings.VNIC_REMOTE_MANAGED and
|
|
ovn_const.VIF_DETAILS_PF_MAC_ADDRESS in bp_info.bp_param):
|
|
port_net = self._plugin.get_network(
|
|
context, port['network_id'])
|
|
options.update({
|
|
ovn_const.LSP_OPTIONS_VIF_PLUG_TYPE_KEY: 'representor',
|
|
ovn_const.LSP_OPTIONS_VIF_PLUG_MTU_REQUEST_KEY: str(
|
|
port_net['mtu']),
|
|
ovn_const.LSP_OPTIONS_VIF_PLUG_REPRESENTOR_PF_MAC_KEY: (
|
|
bp_info.bp_param.get(
|
|
ovn_const.VIF_DETAILS_PF_MAC_ADDRESS)),
|
|
ovn_const.LSP_OPTIONS_VIF_PLUG_REPRESENTOR_VF_NUM_KEY: str(
|
|
bp_info.bp_param.get(ovn_const.VIF_DETAILS_VF_NUM))})
|
|
chassis = utils.determine_bind_host(self._sb_idl, port)
|
|
if chassis:
|
|
# If OVN supports multi-chassis port bindings, use it for live
|
|
# migration to asynchronously configure destination port while
|
|
# VM is migrating
|
|
if self._sb_idl.is_col_present('Port_Binding',
|
|
'additional_chassis'):
|
|
mdst = port.get(
|
|
portbindings.PROFILE, {}).get(
|
|
ovn_const.MIGRATING_ATTR)
|
|
if mdst:
|
|
# Let OVN know that the port should be configured on
|
|
# destination too
|
|
chassis += ',%s' % mdst
|
|
# Block traffic on destination host until libvirt sends
|
|
# a RARP packet from it to inform network about the new
|
|
# location of the port
|
|
options['activation-strategy'] = 'rarp'
|
|
options[ovn_const.LSP_OPTIONS_REQUESTED_CHASSIS_KEY] = chassis
|
|
|
|
# TODO(lucasagomes): Enable the mcast_flood_reports by default,
|
|
# according to core OVN developers it shouldn't cause any harm
|
|
# and will be ignored when mcast_snoop is False. We can revise
|
|
# this once https://bugzilla.redhat.com/show_bug.cgi?id=1933990
|
|
# (see comment #3) is fixed in Core OVN.
|
|
if port_type not in ('vtep', ovn_const.LSP_TYPE_LOCALPORT, 'router'):
|
|
options.update({ovn_const.LSP_OPTIONS_MCAST_FLOOD_REPORTS: 'true'})
|
|
|
|
device_owner = port.get('device_owner', '')
|
|
sg_ids = ' '.join(utils.get_lsp_security_groups(port))
|
|
return OvnPortInfo(port_type, options, addresses, port_security,
|
|
parent_name, tag, dhcpv4_options, dhcpv6_options,
|
|
cidrs.strip(), device_owner, sg_ids,
|
|
address4_scope_id, address6_scope_id,
|
|
bp_info.vnic_type, bp_info.capabilities
|
|
)
|
|
|
|
def update_port_dhcp_options(self, port_info, txn):
|
|
dhcpv4_options = []
|
|
dhcpv6_options = []
|
|
if not port_info.dhcpv4_options:
|
|
dhcpv4_options = []
|
|
elif 'cmd' in port_info.dhcpv4_options:
|
|
dhcpv4_options = txn.add(port_info.dhcpv4_options['cmd'])
|
|
else:
|
|
dhcpv4_options = [port_info.dhcpv4_options['uuid']]
|
|
if not port_info.dhcpv6_options:
|
|
dhcpv6_options = []
|
|
elif 'cmd' in port_info.dhcpv6_options:
|
|
dhcpv6_options = txn.add(port_info.dhcpv6_options['cmd'])
|
|
else:
|
|
dhcpv6_options = [port_info.dhcpv6_options['uuid']]
|
|
|
|
return (dhcpv4_options, dhcpv6_options)
|
|
|
|
def get_external_ids_from_port(self, port):
|
|
port_info = self._get_port_options(port)
|
|
external_ids = {
|
|
ovn_const.OVN_PORT_NAME_EXT_ID_KEY: port['name'],
|
|
ovn_const.OVN_DEVID_EXT_ID_KEY: port['device_id'],
|
|
ovn_const.OVN_PROJID_EXT_ID_KEY: port['project_id'],
|
|
ovn_const.OVN_CIDRS_EXT_ID_KEY: port_info.cidrs,
|
|
ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: port_info.device_owner,
|
|
ovn_const.OVN_SUBNET_POOL_EXT_ADDR_SCOPE4_KEY:
|
|
port_info.address4_scope_id,
|
|
ovn_const.OVN_SUBNET_POOL_EXT_ADDR_SCOPE6_KEY:
|
|
port_info.address6_scope_id,
|
|
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY:
|
|
utils.ovn_name(port['network_id']),
|
|
ovn_const.OVN_SG_IDS_EXT_ID_KEY: port_info.security_group_ids,
|
|
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number(
|
|
port, ovn_const.TYPE_PORTS)),
|
|
ovn_const.OVN_PORT_VNIC_TYPE_KEY: port_info.vnic_type,
|
|
ovn_const.OVN_PORT_BP_CAPABILITIES_KEY:
|
|
';'.join(port_info.capabilities),
|
|
}
|
|
return port_info, external_ids
|
|
|
|
def create_port(self, context, port):
|
|
if utils.is_lsp_ignored(port):
|
|
return
|
|
|
|
port_info, external_ids = self.get_external_ids_from_port(port)
|
|
lswitch_name = utils.ovn_name(port['network_id'])
|
|
|
|
# It's possible to have a network created on one controller and then a
|
|
# port created on a different controller quickly enough that the second
|
|
# controller does not yet see that network in its local cache of the
|
|
# OVN northbound database. Check if the logical switch is present
|
|
# or not in the idl's local copy of the database before creating
|
|
# the lswitch port.
|
|
self._nb_idl.check_for_row_by_value_and_retry(
|
|
'Logical_Switch', 'name', lswitch_name)
|
|
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
dhcpv4_options, dhcpv6_options = self.update_port_dhcp_options(
|
|
port_info, txn=txn)
|
|
# The lport_name *must* be neutron port['id']. It must match the
|
|
# iface-id set in the Interfaces table of the Open_vSwitch
|
|
# database which nova sets to be the port ID.
|
|
|
|
kwargs = {
|
|
'lport_name': port['id'],
|
|
'lswitch_name': lswitch_name,
|
|
'addresses': port_info.addresses,
|
|
'external_ids': external_ids,
|
|
'parent_name': port_info.parent_name,
|
|
'tag': port_info.tag,
|
|
'enabled': port.get('admin_state_up'),
|
|
'options': port_info.options,
|
|
'type': port_info.type,
|
|
'port_security': port_info.port_security,
|
|
'dhcpv4_options': dhcpv4_options,
|
|
'dhcpv6_options': dhcpv6_options
|
|
}
|
|
|
|
if (self.is_external_ports_supported() and
|
|
port_info.type == ovn_const.LSP_TYPE_EXTERNAL):
|
|
kwargs['ha_chassis_group'] = utils.sync_ha_chassis_group(
|
|
context, port['network_id'], self._nb_idl, self._sb_idl,
|
|
txn)
|
|
|
|
# NOTE(mjozefcz): Do not set addresses if the port is not
|
|
# bound, has no device_owner and it is OVN LB VIP port.
|
|
# For more details check related bug #1789686.
|
|
if (port.get('name').startswith(ovn_const.LB_VIP_PORT_PREFIX) and
|
|
not port.get('device_owner') and
|
|
port.get(portbindings.VIF_TYPE) ==
|
|
portbindings.VIF_TYPE_UNBOUND):
|
|
kwargs['addresses'] = []
|
|
|
|
# Check if the parent port was created with the
|
|
# allowed_address_pairs already set
|
|
allowed_address_pairs = port.get('allowed_address_pairs', [])
|
|
if (allowed_address_pairs and
|
|
port_info.type != ovn_const.LSP_TYPE_VIRTUAL):
|
|
addrs = [addr['ip_address'] for addr in allowed_address_pairs]
|
|
self._set_unset_virtual_port_type(context, txn, port, addrs)
|
|
|
|
port_cmd = txn.add(self._nb_idl.create_lswitch_port(
|
|
**kwargs))
|
|
|
|
sg_ids = utils.get_lsp_security_groups(port)
|
|
# If this is not a trusted port and port security is enabled,
|
|
# add it to the default drop Port Group so that all traffic
|
|
# is dropped by default.
|
|
if not utils.is_lsp_trusted(port) and port_info.port_security:
|
|
self._add_port_to_drop_port_group(port_cmd, txn)
|
|
# Just add the port to its Port Group.
|
|
for sg in sg_ids:
|
|
txn.add(self._nb_idl.pg_add_ports(
|
|
utils.ovn_port_group_name(sg), port_cmd))
|
|
|
|
if self.is_dns_required_for_port(port):
|
|
self.add_txns_to_sync_port_dns_records(txn, port)
|
|
|
|
self._qos_driver.create_port(txn, port, port_cmd)
|
|
|
|
db_rev.bump_revision(context, port, ovn_const.TYPE_PORTS)
|
|
|
|
def _set_unset_virtual_port_type(self, context, txn, parent_port,
|
|
addresses, unset=False):
|
|
cmd = self._nb_idl.set_lswitch_port_to_virtual_type
|
|
if unset:
|
|
cmd = self._nb_idl.unset_lswitch_port_to_virtual_type
|
|
|
|
for addr in addresses:
|
|
virt_port = self._plugin.get_ports(context, filters={
|
|
portbindings.VIF_TYPE: portbindings.VIF_TYPE_UNBOUND,
|
|
'network_id': [parent_port['network_id']],
|
|
'fixed_ips': {'ip_address': [addr]}})
|
|
if not virt_port:
|
|
continue
|
|
virt_port = virt_port[0]
|
|
args = {'lport_name': virt_port['id'],
|
|
'virtual_parent': parent_port['id'],
|
|
'if_exists': True}
|
|
LOG.debug("Parent port %(virtual_parent)s found for "
|
|
"virtual port %(lport_name)s", args)
|
|
if not unset:
|
|
args['vip'] = addr
|
|
txn.add(cmd(**args))
|
|
|
|
# TODO(lucasagomes): The ``port_object`` parameter was added to
|
|
# keep things backward compatible. Remove it in the Rocky release.
|
|
def update_port(self, context, port, port_object=None):
|
|
if utils.is_lsp_ignored(port):
|
|
return
|
|
|
|
port_info, external_ids = self.get_external_ids_from_port(port)
|
|
|
|
check_rev_cmd = self._nb_idl.check_revision_number(
|
|
port['id'], port, ovn_const.TYPE_PORTS)
|
|
with self._nb_idl.transaction(check_error=True,
|
|
revision_mismatch_raise=True) as txn:
|
|
txn.add(check_rev_cmd)
|
|
columns_dict = {}
|
|
if utils.is_lsp_router_port(port):
|
|
# It is needed to specify the port type, if not specified
|
|
# the AddLSwitchPortCommand will trigger a change
|
|
# on the northd status column from UP to DOWN, triggering a
|
|
# LogicalSwitchPortUpdateDownEvent, that will most likely
|
|
# cause a revision conflict.
|
|
# https://bugs.launchpad.net/neutron/+bug/1955578
|
|
columns_dict['type'] = ovn_const.LSP_TYPE_ROUTER
|
|
port_info.options.update(
|
|
self._nb_idl.get_router_port_options(port['id']))
|
|
else:
|
|
columns_dict['type'] = port_info.type
|
|
columns_dict['addresses'] = port_info.addresses
|
|
|
|
dhcpv4_options, dhcpv6_options = self.update_port_dhcp_options(
|
|
port_info, txn=txn)
|
|
|
|
if self.is_metadata_port(port):
|
|
context = n_context.get_admin_context()
|
|
network = self._plugin.get_network(context, port['network_id'])
|
|
subnet_ids = [
|
|
_ip['subnet_id']
|
|
for _ip in port['fixed_ips']
|
|
if 'subnet_id' in _ip
|
|
]
|
|
|
|
for subnet in self._plugin.get_subnets(
|
|
context, filters={'id': subnet_ids}):
|
|
if not subnet['enable_dhcp']:
|
|
continue
|
|
self._update_subnet_dhcp_options(subnet, network, txn)
|
|
|
|
# NOTE(mjozefcz): Do not set addresses if the port is not
|
|
# bound, has no device_owner and it is OVN LB VIP port.
|
|
# For more details check related bug #1789686.
|
|
if (port.get('name').startswith(ovn_const.LB_VIP_PORT_PREFIX) and
|
|
not port.get('device_owner') and
|
|
port.get(portbindings.VIF_TYPE) ==
|
|
portbindings.VIF_TYPE_UNBOUND):
|
|
columns_dict['addresses'] = []
|
|
|
|
if self.is_external_ports_supported():
|
|
if port_info.type == ovn_const.LSP_TYPE_EXTERNAL:
|
|
columns_dict['ha_chassis_group'] = (
|
|
utils.sync_ha_chassis_group(
|
|
context, port['network_id'], self._nb_idl,
|
|
self._sb_idl, txn))
|
|
else:
|
|
# Clear the ha_chassis_group field
|
|
columns_dict['ha_chassis_group'] = []
|
|
|
|
ovn_port = self._nb_idl.lookup('Logical_Switch_Port', port['id'])
|
|
addr_pairs_diff = utils.compute_address_pairs_diff(ovn_port, port)
|
|
|
|
if port_info.type != ovn_const.LSP_TYPE_VIRTUAL:
|
|
self._set_unset_virtual_port_type(
|
|
context, txn, port, addr_pairs_diff.added)
|
|
self._set_unset_virtual_port_type(
|
|
context, txn, port, addr_pairs_diff.removed,
|
|
unset=True)
|
|
|
|
# Keep key value pairs that were in the original external ids
|
|
# of the ovn port and we did not touch.
|
|
for k, v in ovn_port.external_ids.items():
|
|
external_ids.setdefault(k, v)
|
|
|
|
# NOTE(lizk): Fail port updating if port doesn't exist. This
|
|
# prevents any new inserted resources to be orphan, such as port
|
|
# dhcp options or ACL rules for port, e.g. a port was created
|
|
# without extra dhcp options and security group, while updating
|
|
# includes the new attributes setting to port.
|
|
txn.add(self._nb_idl.set_lswitch_port(
|
|
lport_name=port['id'],
|
|
external_ids=external_ids,
|
|
parent_name=port_info.parent_name,
|
|
tag=port_info.tag,
|
|
options=port_info.options,
|
|
enabled=port['admin_state_up'],
|
|
port_security=port_info.port_security,
|
|
dhcpv4_options=dhcpv4_options,
|
|
dhcpv6_options=dhcpv6_options,
|
|
if_exists=False,
|
|
**columns_dict))
|
|
|
|
# Determine if security groups or fixed IPs are updated.
|
|
old_sg_ids = set(utils.get_ovn_port_security_groups(ovn_port))
|
|
new_sg_ids = set(utils.get_lsp_security_groups(port))
|
|
detached_sg_ids = old_sg_ids - new_sg_ids
|
|
attached_sg_ids = new_sg_ids - old_sg_ids
|
|
|
|
for sg in detached_sg_ids:
|
|
txn.add(self._nb_idl.pg_del_ports(
|
|
utils.ovn_port_group_name(sg), port['id']))
|
|
for sg in attached_sg_ids:
|
|
txn.add(self._nb_idl.pg_add_ports(
|
|
utils.ovn_port_group_name(sg), port['id']))
|
|
if (not utils.is_lsp_trusted(port) and
|
|
utils.is_port_security_enabled(port)):
|
|
self._add_port_to_drop_port_group(port['id'], txn)
|
|
# If the port doesn't belong to any security group and
|
|
# port_security is disabled, or it's a trusted port, then
|
|
# allow all traffic.
|
|
elif ((not new_sg_ids and
|
|
not utils.is_port_security_enabled(port)) or
|
|
utils.is_lsp_trusted(port)):
|
|
self._del_port_from_drop_port_group(port['id'], txn)
|
|
|
|
self._qos_driver.update_port(txn, port, port_object)
|
|
|
|
if self.is_dns_required_for_port(port):
|
|
self.add_txns_to_sync_port_dns_records(
|
|
txn, port, original_port=port_object)
|
|
elif port_object and self.is_dns_required_for_port(port_object):
|
|
# We need to remove the old entries
|
|
self.add_txns_to_remove_port_dns_records(txn, port_object)
|
|
|
|
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
|
|
db_rev.bump_revision(context, port, ovn_const.TYPE_PORTS)
|
|
|
|
def _delete_port(self, port_id, port_object=None):
|
|
ovn_port = self._nb_idl.lookup('Logical_Switch_Port', port_id)
|
|
ovn_network_name = ovn_port.external_ids.get(
|
|
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY)
|
|
network_id = ovn_network_name.replace('neutron-', '')
|
|
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
txn.add(self._nb_idl.delete_lswitch_port(
|
|
port_id, ovn_network_name))
|
|
|
|
p_object = ({'id': port_id, 'network_id': network_id}
|
|
if not port_object else port_object)
|
|
self._qos_driver.delete_port(txn, p_object)
|
|
|
|
if port_object and self.is_dns_required_for_port(port_object):
|
|
self.add_txns_to_remove_port_dns_records(txn, port_object)
|
|
|
|
# Check if the port being deleted is a virtual parent
|
|
if ovn_port.type != ovn_const.LSP_TYPE_VIRTUAL:
|
|
ls = self._nb_idl.ls_get(ovn_network_name).execute(
|
|
check_error=True)
|
|
cmd = self._nb_idl.unset_lswitch_port_to_virtual_type
|
|
for lsp in ls.ports:
|
|
if lsp.type != ovn_const.LSP_TYPE_VIRTUAL:
|
|
continue
|
|
if port_id in lsp.options.get(
|
|
ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY, ''):
|
|
txn.add(cmd(lsp.name, port_id, if_exists=True))
|
|
|
|
# TODO(lucasagomes): The ``port_object`` parameter was added to
|
|
# keep things backward compatible. Remove it in the Rocky release.
|
|
def delete_port(self, context, port_id, port_object=None):
|
|
try:
|
|
self._delete_port(port_id, port_object=port_object)
|
|
except idlutils.RowNotFound:
|
|
# NOTE(dalvarez): At this point the port doesn't exist in the OVN
|
|
# database or, most likely, this worker IDL hasn't been updated
|
|
# yet. See Bug #1960006 for more information. The approach here is
|
|
# to allow at least one maintenance cycle before we delete the
|
|
# revision number so that the port doesn't stale and eventually
|
|
# gets deleted by the maintenance task.
|
|
rev_row = db_rev.get_revision_row(context, port_id)
|
|
time_ = (timeutils.utcnow() - datetime.timedelta(
|
|
seconds=ovn_const.DB_CONSISTENCY_CHECK_INTERVAL + 30))
|
|
if rev_row and rev_row.created_at >= time_:
|
|
return
|
|
except Exception as e:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error('Failed to delete port %(port)s. Error: '
|
|
'%(error)s', {'port': port_id, 'error': e})
|
|
db_rev.delete_revision(context, port_id, ovn_const.TYPE_PORTS)
|
|
|
|
def _create_or_update_floatingip(self, floatingip, txn=None):
|
|
router_id = floatingip.get('router_id')
|
|
if not router_id:
|
|
return
|
|
|
|
# FIPs used for port forwarding have no fixed address
|
|
# configured. Also, OVN handler for port forwarding
|
|
# is delegated to OVNPortForwarding. Nothing further
|
|
# to do here.
|
|
if floatingip['fixed_ip_address'] is None:
|
|
LOG.debug("Skipping NAT for floating ip %(id)s, external ip "
|
|
"%(fip_ip)s on router %(rtr_id)s: no logical_ip",
|
|
{'id': floatingip['id'],
|
|
'fip_ip': floatingip['floating_ip_address'],
|
|
'rtr_id': router_id})
|
|
return
|
|
|
|
commands = []
|
|
admin_context = n_context.get_admin_context()
|
|
fip_db = self._l3_plugin._get_floatingip(
|
|
admin_context, floatingip['id'])
|
|
port_db = self._plugin.get_port(
|
|
admin_context, fip_db['floating_port_id'])
|
|
|
|
gw_lrouter_name = utils.ovn_name(router_id)
|
|
ext_ids = {
|
|
ovn_const.OVN_FIP_EXT_ID_KEY: floatingip['id'],
|
|
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number(
|
|
floatingip, ovn_const.TYPE_FLOATINGIPS)),
|
|
ovn_const.OVN_FIP_PORT_EXT_ID_KEY: floatingip['port_id'],
|
|
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: gw_lrouter_name,
|
|
ovn_const.OVN_FIP_EXT_MAC_KEY: port_db['mac_address'],
|
|
ovn_const.OVN_FIP_NET_ID: floatingip['floating_network_id']}
|
|
columns = {'type': 'dnat_and_snat',
|
|
'logical_ip': floatingip['fixed_ip_address'],
|
|
'external_ip': floatingip['floating_ip_address'],
|
|
'logical_port': floatingip['port_id'],
|
|
'external_ids': ext_ids}
|
|
|
|
if ovn_conf.is_ovn_distributed_floating_ip():
|
|
if self._nb_idl.lsp_get_up(floatingip['port_id']).execute():
|
|
columns['external_mac'] = port_db['mac_address']
|
|
|
|
# TODO(mjozefcz): Remove this workaround when OVN LB
|
|
# will support both decentralized FIPs on LB and member.
|
|
lb_member_fip = self._is_lb_member_fip(admin_context, floatingip)
|
|
if (ovn_conf.is_ovn_distributed_floating_ip() and
|
|
lb_member_fip):
|
|
LOG.warning("Port %s is configured as a member "
|
|
"of one of OVN Load_Balancers and "
|
|
"Load_Balancer has FIP assigned. "
|
|
"In order to make traffic work member "
|
|
"FIP needs to be centralized, even if "
|
|
"this environment is configured as DVR. "
|
|
"Removing logical_port and external_mac from "
|
|
"NAT entry.", floatingip['port_id'])
|
|
columns.pop('logical_port', None)
|
|
columns.pop('external_mac', None)
|
|
commands.append(self._nb_idl.add_nat_rule_in_lrouter(gw_lrouter_name,
|
|
**columns))
|
|
|
|
# Get the logical port (of the private network) and set the field
|
|
# external_ids:fip=<FIP>. This will be used by the ovn octavia driver
|
|
# to add the floating ip as vip in the Load_Balancer.vips column.
|
|
private_lsp = self._nb_idl.get_lswitch_port(floatingip['port_id'])
|
|
|
|
if private_lsp:
|
|
port_fip = {
|
|
ovn_const.OVN_PORT_FIP_EXT_ID_KEY:
|
|
floatingip['floating_ip_address']}
|
|
commands.append(
|
|
self._nb_idl.db_set('Logical_Switch_Port', private_lsp.uuid,
|
|
('external_ids', port_fip))
|
|
)
|
|
if not lb_member_fip:
|
|
commands.extend(
|
|
self._handle_lb_fip_cmds(
|
|
admin_context, private_lsp,
|
|
action=ovn_const.FIP_ACTION_ASSOCIATE))
|
|
else:
|
|
LOG.warning("LSP for floatingip %s, has not been found! "
|
|
"Cannot set FIP on VIP.",
|
|
floatingip['id'])
|
|
self._transaction(commands, txn=txn)
|
|
|
|
def _is_lb_member_fip(self, context, fip):
|
|
port = self._plugin.get_port(
|
|
context, fip['port_id'])
|
|
member_subnet = [ip['subnet_id'] for ip in port['fixed_ips']
|
|
if ip['ip_address'] == fip['fixed_ip_address']]
|
|
if not member_subnet:
|
|
return False
|
|
member_subnet = member_subnet[0]
|
|
|
|
ls = self._nb_idl.lookup(
|
|
'Logical_Switch', utils.ovn_name(port['network_id']))
|
|
for lb in ls.load_balancer:
|
|
for ext_id in lb.external_ids.keys():
|
|
if ext_id.startswith(ovn_const.LB_EXT_IDS_POOL_PREFIX):
|
|
members = lb.external_ids[ext_id]
|
|
if not members:
|
|
continue
|
|
for member in members.split(','):
|
|
if ('%s:' % fip['fixed_ip_address'] in member and
|
|
'_%s' % member_subnet in member):
|
|
return True
|
|
return False
|
|
|
|
def _handle_lb_fip_cmds(self, context, lb_lsp,
|
|
action=ovn_const.FIP_ACTION_ASSOCIATE):
|
|
commands = []
|
|
if not ovn_conf.is_ovn_distributed_floating_ip():
|
|
return commands
|
|
|
|
lb_lsp_fip_port = lb_lsp.external_ids.get(
|
|
ovn_const.OVN_PORT_NAME_EXT_ID_KEY, '')
|
|
|
|
if not lb_lsp_fip_port.startswith(ovn_const.LB_VIP_PORT_PREFIX):
|
|
return commands
|
|
|
|
# This is a FIP on LB VIP.
|
|
# Loop over members and delete FIP external_mac/logical_port enteries.
|
|
# Find all LBs with this LSP as VIP.
|
|
lbs = self._nb_idl.db_find_rows(
|
|
'Load_Balancer',
|
|
('external_ids', '=', {
|
|
ovn_const.LB_EXT_IDS_VIP_PORT_ID_KEY: lb_lsp.name})
|
|
).execute(check_error=True)
|
|
for lb in lbs:
|
|
# GET all LS where given LB is linked.
|
|
ls_linked = [
|
|
item
|
|
for item in self._nb_idl.db_find_rows(
|
|
'Logical_Switch').execute(check_error=True)
|
|
if lb in item.load_balancer]
|
|
|
|
if not ls_linked:
|
|
return
|
|
|
|
# Find out IP addresses and subnets of configured members.
|
|
members_to_verify = []
|
|
for ext_id in lb.external_ids.keys():
|
|
if ext_id.startswith(ovn_const.LB_EXT_IDS_POOL_PREFIX):
|
|
members = lb.external_ids[ext_id]
|
|
if not members:
|
|
continue
|
|
for member in members.split(','):
|
|
# NOTE(mjozefcz): Remove this workaround in W release.
|
|
# Last argument of member info is a subnet_id from
|
|
# from which member comes from.
|
|
# member_`id`_`ip`:`port`_`subnet_ip`
|
|
member_info = member.split('_')
|
|
if len(member_info) >= 4:
|
|
m = {}
|
|
m['id'] = member_info[1]
|
|
m['ip'] = member_info[2].split(':')[0]
|
|
m['subnet_id'] = member_info[3]
|
|
try:
|
|
subnet = self._plugin.get_subnet(
|
|
context, m['subnet_id'])
|
|
m['network_id'] = subnet['network_id']
|
|
members_to_verify.append(m)
|
|
except n_exc.SubnetNotFound:
|
|
LOG.debug("Cannot find subnet details "
|
|
"for OVN LB member "
|
|
"%s.", m['id'])
|
|
|
|
# Find a member LSPs from all linked LS to this LB.
|
|
for member in members_to_verify:
|
|
ls = self._nb_idl.lookup(
|
|
'Logical_Switch', utils.ovn_name(member['network_id']))
|
|
for lsp in ls.ports:
|
|
if not lsp.addresses:
|
|
continue
|
|
if member['ip'] in utils.remove_macs_from_lsp_addresses(
|
|
lsp.addresses):
|
|
member['lsp'] = lsp
|
|
nats = self._nb_idl.db_find_rows(
|
|
'NAT',
|
|
('external_ids', '=', {
|
|
ovn_const.OVN_FIP_PORT_EXT_ID_KEY: lsp.name})
|
|
).execute(check_error=True)
|
|
|
|
for nat in nats:
|
|
if action == ovn_const.FIP_ACTION_ASSOCIATE:
|
|
# NOTE(mjozefcz): We should delete logical_port
|
|
# and external_mac entries from member NAT in
|
|
# order to make traffic work.
|
|
LOG.warning(
|
|
"Port %s is configured as a member "
|
|
"of one of OVN Load_Balancers and "
|
|
"Load_Balancer has FIP assigned. "
|
|
"In order to make traffic work member "
|
|
"FIP needs to be centralized, even if "
|
|
"this environment is configured as "
|
|
"DVR. Removing logical_port and "
|
|
"external_mac from NAT entry.",
|
|
lsp.name)
|
|
commands.extend([
|
|
self._nb_idl.db_clear(
|
|
'NAT', nat.uuid, 'external_mac'),
|
|
self._nb_idl.db_clear(
|
|
'NAT', nat.uuid, 'logical_port')])
|
|
else:
|
|
# NOTE(mjozefcz): The FIP from LB VIP is
|
|
# disassociated now. We can decentralize
|
|
# member FIPs now.
|
|
LOG.warning(
|
|
"Port %s is configured as a member "
|
|
"of one of OVN Load_Balancers and "
|
|
"Load_Balancer has FIP disassociated. "
|
|
"DVR for this port can be enabled back.",
|
|
lsp.name)
|
|
commands.append(self._nb_idl.db_set(
|
|
'NAT', nat.uuid,
|
|
('logical_port', lsp.name)))
|
|
port = self._plugin.get_port(context, lsp.name)
|
|
if port['status'] == const.PORT_STATUS_ACTIVE:
|
|
commands.append(
|
|
self._nb_idl.db_set(
|
|
'NAT', nat.uuid,
|
|
('external_mac',
|
|
port['mac_address'])))
|
|
|
|
return commands
|
|
|
|
def _delete_floatingip(self, fip, lrouter, txn=None):
|
|
commands = [self._nb_idl.delete_nat_rule_in_lrouter(
|
|
lrouter, type='dnat_and_snat',
|
|
logical_ip=fip['logical_ip'],
|
|
external_ip=fip['external_ip'])]
|
|
try:
|
|
port_id = (
|
|
fip['external_ids'].get(ovn_const.OVN_FIP_PORT_EXT_ID_KEY))
|
|
if port_id:
|
|
private_lsp = self._nb_idl.get_lswitch_port(port_id)
|
|
if private_lsp:
|
|
commands.append(
|
|
self._nb_idl.db_remove(
|
|
'Logical_Switch_Port', private_lsp.uuid,
|
|
'external_ids',
|
|
(ovn_const.OVN_PORT_FIP_EXT_ID_KEY)))
|
|
commands.extend(
|
|
self._handle_lb_fip_cmds(
|
|
n_context.get_admin_context(),
|
|
private_lsp,
|
|
action=ovn_const.FIP_ACTION_DISASSOCIATE))
|
|
except KeyError:
|
|
LOG.debug("FIP %s doesn't have external_ids.", fip)
|
|
self._transaction(commands, txn=txn)
|
|
|
|
def update_floatingip_status(self, context, floatingip):
|
|
# NOTE(lucasagomes): OVN doesn't care about the floating ip
|
|
# status, this method just bumps the revision number
|
|
check_rev_cmd = self._nb_idl.check_revision_number(
|
|
floatingip['id'], floatingip, ovn_const.TYPE_FLOATINGIPS)
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
txn.add(check_rev_cmd)
|
|
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
|
|
db_rev.bump_revision(
|
|
context, floatingip, ovn_const.TYPE_FLOATINGIPS)
|
|
|
|
def create_floatingip(self, context, floatingip):
|
|
try:
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
self._create_or_update_floatingip(floatingip, txn=txn)
|
|
self._qos_driver.create_floatingip(txn, floatingip)
|
|
except Exception as e:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error('Unable to create floating ip in gateway '
|
|
'router. Error: %s', e)
|
|
|
|
db_rev.bump_revision(context, floatingip, ovn_const.TYPE_FLOATINGIPS)
|
|
|
|
# NOTE(lucasagomes): Revise the expected status
|
|
# of floating ips, setting it to ACTIVE here doesn't
|
|
# see consistent with other drivers (ODL here), see:
|
|
# https://bugs.launchpad.net/networking-ovn/+bug/1657693
|
|
if floatingip.get('router_id'):
|
|
self._l3_plugin.update_floatingip_status(
|
|
n_context.get_admin_context(), floatingip['id'],
|
|
const.FLOATINGIP_STATUS_ACTIVE)
|
|
|
|
def update_floatingip(self, context, floatingip):
|
|
fip_status = None
|
|
router_id = None
|
|
ovn_fip = self._nb_idl.get_floatingip(floatingip['id'])
|
|
|
|
check_rev_cmd = self._nb_idl.check_revision_number(
|
|
floatingip['id'], floatingip, ovn_const.TYPE_FLOATINGIPS)
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
txn.add(check_rev_cmd)
|
|
if ovn_fip:
|
|
lrouter = ovn_fip['external_ids'].get(
|
|
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY,
|
|
utils.ovn_name(router_id))
|
|
self._delete_floatingip(ovn_fip, lrouter, txn=txn)
|
|
fip_status = const.FLOATINGIP_STATUS_DOWN
|
|
|
|
if floatingip.get('port_id'):
|
|
self._create_or_update_floatingip(floatingip, txn=txn)
|
|
fip_status = const.FLOATINGIP_STATUS_ACTIVE
|
|
|
|
self._qos_driver.update_floatingip(txn, floatingip)
|
|
|
|
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
|
|
db_rev.bump_revision(
|
|
context, floatingip, ovn_const.TYPE_FLOATINGIPS)
|
|
|
|
if fip_status:
|
|
self._l3_plugin.update_floatingip_status(
|
|
context, floatingip['id'], fip_status)
|
|
|
|
def delete_floatingip(self, context, fip_id):
|
|
router_id = None
|
|
ovn_fip = self._nb_idl.get_floatingip(fip_id)
|
|
|
|
if ovn_fip:
|
|
lrouter = ovn_fip['external_ids'].get(
|
|
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY,
|
|
utils.ovn_name(router_id))
|
|
fip_net_id = ovn_fip['external_ids'].get(ovn_const.OVN_FIP_NET_ID)
|
|
fip_dict = {'floating_network_id': fip_net_id, 'id': fip_id}
|
|
try:
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
self._delete_floatingip(ovn_fip, lrouter, txn=txn)
|
|
self._qos_driver.delete_floatingip(txn, fip_dict)
|
|
except Exception as e:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error('Unable to delete floating ip in gateway '
|
|
'router. Error: %s', e)
|
|
db_rev.delete_revision(context, fip_id, ovn_const.TYPE_FLOATINGIPS)
|
|
|
|
def disassociate_floatingip(self, floatingip, router_id):
|
|
lrouter = utils.ovn_name(router_id)
|
|
try:
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
self._delete_floatingip(floatingip, lrouter, txn=txn)
|
|
self._qos_driver.delete_floatingip(txn, floatingip)
|
|
except Exception as e:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error('Unable to disassociate floating ip in gateway '
|
|
'router. Error: %s', e)
|
|
|
|
def _get_gw_info(self, context, router):
|
|
gateways_info = []
|
|
ext_gw_info = router.get(l3.EXTERNAL_GW_INFO, {})
|
|
network_id = ext_gw_info.get('network_id', '')
|
|
for ext_fixed_ip in ext_gw_info.get('external_fixed_ips', []):
|
|
subnet_id = ext_fixed_ip['subnet_id']
|
|
subnet = self._plugin.get_subnet(context, subnet_id)
|
|
gateways_info.append(GW_INFO(
|
|
network_id, subnet_id, ext_fixed_ip['ip_address'],
|
|
subnet.get('gateway_ip'), subnet['ip_version'],
|
|
const.IPv4_ANY if subnet['ip_version'] == const.IP_VERSION_4
|
|
else const.IPv6_ANY))
|
|
return gateways_info
|
|
|
|
def _delete_router_ext_gw(self, router, networks, txn):
|
|
context = n_context.get_admin_context()
|
|
if not networks:
|
|
networks = []
|
|
router_id = router['id']
|
|
gw_port_id = router['gw_port_id']
|
|
gw_lrouter_name = utils.ovn_name(router_id)
|
|
gateways = self._get_gw_info(context, router)
|
|
for gw_info in gateways:
|
|
if gw_info.ip_version == const.IP_VERSION_4:
|
|
for network in networks:
|
|
txn.add(self._nb_idl.delete_nat_rule_in_lrouter(
|
|
gw_lrouter_name, type='snat', logical_ip=network,
|
|
external_ip=gw_info.router_ip))
|
|
txn.add(self._nb_idl.delete_static_route(
|
|
gw_lrouter_name, ip_prefix=gw_info.ip_prefix,
|
|
nexthop=gw_info.gateway_ip))
|
|
txn.add(self._nb_idl.delete_lrouter_port(
|
|
utils.ovn_lrouter_port_name(gw_port_id),
|
|
gw_lrouter_name))
|
|
|
|
def _get_nets_and_ipv6_ra_confs_for_router_port(self, context, port):
|
|
port_fixed_ips = port['fixed_ips']
|
|
networks = set()
|
|
ipv6_ra_configs = {}
|
|
ipv6_ra_configs_supported = self._nb_idl.is_col_present(
|
|
'Logical_Router_Port', 'ipv6_ra_configs')
|
|
is_gw_port = const.DEVICE_OWNER_ROUTER_GW == port.get(
|
|
'device_owner')
|
|
|
|
for fixed_ip in port_fixed_ips:
|
|
subnet_id = fixed_ip['subnet_id']
|
|
subnet = self._plugin.get_subnet(context, subnet_id)
|
|
cidr = netaddr.IPNetwork(subnet['cidr'])
|
|
networks.add("%s/%s" % (fixed_ip['ip_address'],
|
|
str(cidr.prefixlen)))
|
|
|
|
if subnet.get('ipv6_address_mode') and not ipv6_ra_configs and (
|
|
ipv6_ra_configs_supported):
|
|
ipv6_ra_configs['address_mode'] = (
|
|
utils.get_ovn_ipv6_address_mode(
|
|
subnet['ipv6_address_mode']))
|
|
net = self._plugin.get_network(context, subnet['network_id'])
|
|
# If it's a gateway port and connected to a provider
|
|
# network set send_periodic to False, that way we do not
|
|
# leak the RAs generated for the tenant networks via the
|
|
# provider network
|
|
ipv6_ra_configs['send_periodic'] = 'true'
|
|
if is_gw_port and utils.is_external_network(net):
|
|
ipv6_ra_configs['send_periodic'] = 'false'
|
|
ipv6_ra_configs['mtu'] = str(net['mtu'])
|
|
|
|
return list(networks), ipv6_ra_configs
|
|
|
|
def _add_router_ext_gw(self, context, router, networks, txn):
|
|
# 1. Add the external gateway router port.
|
|
admin_context = context.elevated()
|
|
gateways = self._get_gw_info(admin_context, router)
|
|
gw_port_id = router['gw_port_id']
|
|
port = self._plugin.get_port(admin_context, gw_port_id)
|
|
self._create_lrouter_port(admin_context, router, port, txn=txn)
|
|
|
|
# 2. Add default route with nexthop as gateway ip
|
|
lrouter_name = utils.ovn_name(router['id'])
|
|
for gw_info in gateways:
|
|
if gw_info.gateway_ip is None:
|
|
continue
|
|
columns = {'external_ids': {
|
|
ovn_const.OVN_ROUTER_IS_EXT_GW: 'true',
|
|
ovn_const.OVN_SUBNET_EXT_ID_KEY: gw_info.subnet_id}}
|
|
txn.add(self._nb_idl.add_static_route(
|
|
lrouter_name, ip_prefix=gw_info.ip_prefix,
|
|
nexthop=gw_info.gateway_ip, **columns))
|
|
|
|
# 3. Add snat rules for tenant networks in lrouter if snat is enabled
|
|
if utils.is_snat_enabled(router) and networks:
|
|
self.update_nat_rules(router, networks, enable_snat=True, txn=txn)
|
|
return port
|
|
|
|
def _check_external_ips_changed(self, ovn_snats,
|
|
ovn_static_routes, router):
|
|
context = n_context.get_admin_context()
|
|
gateways = self._get_gw_info(context, router)
|
|
ovn_gw_subnets = None
|
|
if self._nb_idl.is_col_present('Logical_Router_Static_Route',
|
|
'external_ids'):
|
|
ovn_gw_subnets = [
|
|
getattr(route, 'external_ids', {}).get(
|
|
ovn_const.OVN_SUBNET_EXT_ID_KEY) for route in
|
|
ovn_static_routes]
|
|
|
|
for gw_info in gateways:
|
|
if ovn_gw_subnets and gw_info.subnet_id not in ovn_gw_subnets:
|
|
return True
|
|
if gw_info.ip_version == const.IP_VERSION_6:
|
|
continue
|
|
for snat in ovn_snats:
|
|
if snat.external_ip != gw_info.router_ip:
|
|
return True
|
|
|
|
return False
|
|
|
|
def update_router_routes(self, context, router_id, add, remove,
|
|
txn=None):
|
|
if not any([add, remove]):
|
|
return
|
|
lrouter_name = utils.ovn_name(router_id)
|
|
commands = []
|
|
for route in add:
|
|
commands.append(
|
|
self._nb_idl.add_static_route(
|
|
lrouter_name, ip_prefix=route['destination'],
|
|
nexthop=route['nexthop']))
|
|
for route in remove:
|
|
commands.append(
|
|
self._nb_idl.delete_static_route(
|
|
lrouter_name, ip_prefix=route['destination'],
|
|
nexthop=route['nexthop']))
|
|
self._transaction(commands, txn=txn)
|
|
|
|
def _get_router_gw_ports(self, context, router_id):
|
|
return self._plugin.get_ports(context, filters={
|
|
'device_owner': [const.DEVICE_OWNER_ROUTER_GW],
|
|
'device_id': [router_id]})
|
|
|
|
def _get_router_ports(self, context, router_id):
|
|
# _get_router() will raise a RouterNotFound error if there's no router
|
|
# with the router_id
|
|
router_db = self._l3_plugin._get_router(context, router_id)
|
|
# When the existing deployment is migrated to OVN
|
|
# we may need to consider other port types - DVR_INTERFACE/HA_INTF.
|
|
return [p.port for p in router_db.attached_ports
|
|
if p.port_type in [const.DEVICE_OWNER_ROUTER_INTF,
|
|
const.DEVICE_OWNER_DVR_INTERFACE,
|
|
const.DEVICE_OWNER_HA_REPLICATED_INT,
|
|
const.DEVICE_OWNER_ROUTER_HA_INTF]]
|
|
|
|
def _get_v4_network_for_router_port(self, context, port):
|
|
cidr = None
|
|
for fixed_ip in port['fixed_ips']:
|
|
subnet_id = fixed_ip['subnet_id']
|
|
subnet = self._plugin.get_subnet(context, subnet_id)
|
|
if subnet['ip_version'] != const.IP_VERSION_4:
|
|
continue
|
|
cidr = subnet['cidr']
|
|
return cidr
|
|
|
|
def _get_v4_network_of_all_router_ports(self, context, router_id,
|
|
ports=None):
|
|
networks = []
|
|
ports = ports or self._get_router_ports(context, router_id)
|
|
for port in ports:
|
|
network = self._get_v4_network_for_router_port(context, port)
|
|
if network:
|
|
networks.append(network)
|
|
|
|
return networks
|
|
|
|
def _gen_router_ext_ids(self, router):
|
|
return {
|
|
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY:
|
|
router.get('name', 'no_router_name'),
|
|
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number(
|
|
router, ovn_const.TYPE_ROUTERS)),
|
|
ovn_const.OVN_AZ_HINTS_EXT_ID_KEY:
|
|
','.join(common_utils.get_az_hints(router)),
|
|
}
|
|
|
|
def create_router(self, context, router, add_external_gateway=True):
|
|
"""Create a logical router."""
|
|
external_ids = self._gen_router_ext_ids(router)
|
|
enabled = router.get('admin_state_up')
|
|
lrouter_name = utils.ovn_name(router['id'])
|
|
added_gw_port = None
|
|
options = {'always_learn_from_arp_request': 'false',
|
|
'dynamic_neigh_routers': 'true'}
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
txn.add(self._nb_idl.create_lrouter(lrouter_name,
|
|
external_ids=external_ids,
|
|
enabled=enabled,
|
|
options=options))
|
|
# TODO(lucasagomes): add_external_gateway is being only used
|
|
# by the ovn_db_sync.py script, remove it after the database
|
|
# synchronization work
|
|
if add_external_gateway:
|
|
networks = self._get_v4_network_of_all_router_ports(
|
|
context, router['id'])
|
|
if router.get(l3.EXTERNAL_GW_INFO) and networks is not None:
|
|
added_gw_port = self._add_router_ext_gw(
|
|
context, router, networks, txn)
|
|
|
|
self._qos_driver.create_router(txn, router)
|
|
|
|
if added_gw_port:
|
|
db_rev.bump_revision(context, added_gw_port,
|
|
ovn_const.TYPE_ROUTER_PORTS)
|
|
db_rev.bump_revision(context, router, ovn_const.TYPE_ROUTERS)
|
|
|
|
# TODO(lucasagomes): The ``router_object`` parameter was added to
|
|
# keep things backward compatible with old routers created prior to
|
|
# the database sync work. Remove it in the Rocky release.
|
|
def update_router(self, context, new_router, router_object=None):
|
|
"""Update a logical router."""
|
|
router_id = new_router['id']
|
|
router_name = utils.ovn_name(router_id)
|
|
ovn_router = self._nb_idl.get_lrouter(router_name)
|
|
gateway_new = new_router.get(l3.EXTERNAL_GW_INFO)
|
|
gateway_old = utils.get_lrouter_ext_gw_static_route(ovn_router)
|
|
added_gw_port = None
|
|
deleted_gw_port_id = None
|
|
|
|
if router_object:
|
|
gateway_old = gateway_old or router_object.get(l3.EXTERNAL_GW_INFO)
|
|
ovn_snats = utils.get_lrouter_snats(ovn_router)
|
|
networks = self._get_v4_network_of_all_router_ports(context, router_id)
|
|
try:
|
|
check_rev_cmd = self._nb_idl.check_revision_number(
|
|
router_name, new_router, ovn_const.TYPE_ROUTERS)
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
txn.add(check_rev_cmd)
|
|
if gateway_new and not gateway_old:
|
|
# Route gateway is set
|
|
added_gw_port = self._add_router_ext_gw(
|
|
context, new_router, networks, txn)
|
|
elif gateway_old and not gateway_new:
|
|
# router gateway is removed
|
|
txn.add(self._nb_idl.delete_lrouter_ext_gw(router_name))
|
|
if router_object:
|
|
self._delete_router_ext_gw(
|
|
router_object, networks, txn)
|
|
deleted_gw_port_id = router_object['gw_port_id']
|
|
elif gateway_new and gateway_old:
|
|
# Check if external gateway has changed, if yes, delete
|
|
# the old gateway and add the new gateway
|
|
if self._check_external_ips_changed(
|
|
ovn_snats, gateway_old, new_router):
|
|
txn.add(self._nb_idl.delete_lrouter_ext_gw(
|
|
router_name))
|
|
if router_object:
|
|
self._delete_router_ext_gw(
|
|
router_object, networks, txn)
|
|
deleted_gw_port_id = router_object['gw_port_id']
|
|
added_gw_port = self._add_router_ext_gw(
|
|
context, new_router, networks, txn)
|
|
else:
|
|
# Check if snat has been enabled/disabled and update
|
|
new_snat_state = gateway_new.get('enable_snat', True)
|
|
if bool(ovn_snats) != new_snat_state and networks:
|
|
self.update_nat_rules(
|
|
new_router, networks,
|
|
enable_snat=new_snat_state, txn=txn)
|
|
|
|
update = {'external_ids': self._gen_router_ext_ids(new_router)}
|
|
update['enabled'] = new_router.get('admin_state_up') or False
|
|
txn.add(self._nb_idl.update_lrouter(router_name, **update))
|
|
|
|
# Check for route updates
|
|
routes = new_router.get('routes', [])
|
|
old_routes = utils.get_lrouter_non_gw_routes(ovn_router)
|
|
added, removed = helpers.diff_list_of_dict(
|
|
old_routes, routes)
|
|
self.update_router_routes(
|
|
context, router_id, added, removed, txn=txn)
|
|
self._qos_driver.update_router(txn, new_router)
|
|
|
|
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
|
|
db_rev.bump_revision(context, new_router,
|
|
ovn_const.TYPE_ROUTERS)
|
|
|
|
if added_gw_port:
|
|
db_rev.bump_revision(context, added_gw_port,
|
|
ovn_const.TYPE_ROUTER_PORTS)
|
|
|
|
if deleted_gw_port_id:
|
|
db_rev.delete_revision(context, deleted_gw_port_id,
|
|
ovn_const.TYPE_ROUTER_PORTS)
|
|
|
|
except Exception as e:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.error('Unable to update router %(router)s. '
|
|
'Error: %(error)s', {'router': router_id,
|
|
'error': e})
|
|
|
|
def delete_router(self, context, router_id):
|
|
"""Delete a logical router."""
|
|
lrouter_name = utils.ovn_name(router_id)
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
txn.add(self._nb_idl.delete_lrouter(lrouter_name))
|
|
db_rev.delete_revision(context, router_id, ovn_const.TYPE_ROUTERS)
|
|
|
|
def get_candidates_for_scheduling(self, physnet, cms=None,
|
|
chassis_physnets=None,
|
|
availability_zone_hints=None):
|
|
"""Return chassis for scheduling gateway router.
|
|
|
|
Criteria for selecting chassis as candidates
|
|
1) chassis from cms with proper bridge mappings
|
|
2) if no chassis is available from 1) then,
|
|
select chassis with proper bridge mappings
|
|
3) Filter the available chassis accordingly to the routers
|
|
availability zone hints (if present)
|
|
"""
|
|
# TODO(lucasagomes): Simplify the logic here, the CMS option has
|
|
# been introduced long ago and by now all gateway chassis should
|
|
# include it. This will match the logic in the is_gateway_chassis()
|
|
# (utils.py)
|
|
cms = cms or self._sb_idl.get_gateway_chassis_from_cms_options()
|
|
chassis_physnets = (chassis_physnets or
|
|
self._sb_idl.get_chassis_and_physnets())
|
|
cms_bmaps = []
|
|
bmaps = []
|
|
for chassis, physnets in chassis_physnets.items():
|
|
if physnet and physnet in physnets:
|
|
if chassis in cms:
|
|
cms_bmaps.append(chassis)
|
|
else:
|
|
bmaps.append(chassis)
|
|
candidates = cms_bmaps or bmaps or cms
|
|
|
|
# Filter for availability zones
|
|
if availability_zone_hints:
|
|
LOG.debug('Filtering Chassis candidates by availability zone '
|
|
'hints: %s', ', '.join(availability_zone_hints))
|
|
candidates = [ch for ch in candidates
|
|
for az in availability_zone_hints
|
|
if az in utils.get_chassis_availability_zones(
|
|
self._sb_idl.lookup('Chassis', ch, None))]
|
|
|
|
if not cms_bmaps:
|
|
LOG.debug("No eligible chassis with external connectivity"
|
|
" through ovn-cms-options for %s", physnet)
|
|
LOG.debug("Chassis candidates for scheduling gateway router ports: %s",
|
|
candidates)
|
|
return candidates
|
|
|
|
def _get_physnet(self, network):
|
|
if network.get(pnet.NETWORK_TYPE) in [const.TYPE_FLAT,
|
|
const.TYPE_VLAN]:
|
|
return network.get(pnet.PHYSICAL_NETWORK)
|
|
|
|
def _gen_router_port_ext_ids(self, port):
|
|
ext_ids = {
|
|
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number(
|
|
port, ovn_const.TYPE_ROUTER_PORTS)),
|
|
ovn_const.OVN_SUBNET_EXT_IDS_KEY:
|
|
' '.join(utils.get_port_subnet_ids(port)),
|
|
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY:
|
|
utils.ovn_name(port['network_id'])}
|
|
|
|
router_id = port.get('device_id')
|
|
if router_id:
|
|
ext_ids[ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY] = router_id
|
|
|
|
return ext_ids
|
|
|
|
def _get_reside_redir_for_gateway_port(self, device_id):
|
|
admin_context = n_context.get_admin_context()
|
|
reside_redir_ch = 'true'
|
|
if ovn_conf.is_ovn_distributed_floating_ip():
|
|
reside_redir_ch = 'false'
|
|
try:
|
|
router_ports = self._get_router_ports(admin_context, device_id)
|
|
except l3_exc.RouterNotFound:
|
|
LOG.debug("No Router %s not found", device_id)
|
|
else:
|
|
network_ids = {port['network_id'] for port in router_ports}
|
|
networks = self._plugin.get_networks(
|
|
admin_context, filters={'id': network_ids})
|
|
|
|
# NOTE(ltomasbo): not all the networks connected to the router
|
|
# are of vlan type, so we won't set the redirect-type=bridged
|
|
# on the router gateway port, therefore we need to centralized
|
|
# the vlan traffic to avoid tunneling
|
|
if networks:
|
|
reside_redir_ch = 'true' if any(
|
|
net.get(pnet.NETWORK_TYPE) not in [const.TYPE_VLAN,
|
|
const.TYPE_FLAT]
|
|
for net in networks) else 'false'
|
|
return reside_redir_ch
|
|
|
|
def _gen_router_port_options(self, port, network=None):
|
|
options = {}
|
|
admin_context = n_context.get_admin_context()
|
|
if network is None:
|
|
network = self._plugin.get_network(admin_context,
|
|
port['network_id'])
|
|
# For VLAN type networks we need to set the
|
|
# "reside-on-redirect-chassis" option so the routing for this
|
|
# logical router port is centralized in the chassis hosting the
|
|
# distributed gateway port.
|
|
# https://github.com/openvswitch/ovs/commit/85706c34d53d4810f54bec1de662392a3c06a996
|
|
# FIXME(ltomasbo): Once Bugzilla 2162756 is fixed the
|
|
# is_provider_network check should be removed
|
|
if network.get(pnet.NETWORK_TYPE) == const.TYPE_VLAN:
|
|
reside_redir_ch = self._get_reside_redir_for_gateway_port(
|
|
port['device_id'])
|
|
options[ovn_const.LRP_OPTIONS_RESIDE_REDIR_CH] = reside_redir_ch
|
|
|
|
is_gw_port = const.DEVICE_OWNER_ROUTER_GW == port.get(
|
|
'device_owner')
|
|
|
|
if is_gw_port and (ovn_conf.is_ovn_distributed_floating_ip() or
|
|
ovn_conf.is_ovn_emit_need_to_frag_enabled()):
|
|
try:
|
|
router_ports = self._get_router_ports(admin_context,
|
|
port['device_id'])
|
|
except l3_exc.RouterNotFound:
|
|
# Don't add any mtu info if the router no longer exists
|
|
LOG.debug("Router %s not found", port['device_id'])
|
|
else:
|
|
network_ids = {port['network_id'] for port in router_ports}
|
|
networks = self._plugin.get_networks(
|
|
admin_context, filters={'id': network_ids})
|
|
if ovn_conf.is_ovn_emit_need_to_frag_enabled():
|
|
for net in networks:
|
|
if net['mtu'] > network['mtu']:
|
|
options[
|
|
ovn_const.OVN_ROUTER_PORT_GW_MTU_OPTION] = str(
|
|
network['mtu'])
|
|
break
|
|
if ovn_conf.is_ovn_distributed_floating_ip():
|
|
# NOTE(ltomasbo): For VLAN type networks connected through
|
|
# the gateway port there is a need to set the redirect-type
|
|
# option to bridge to ensure traffic is not centralized
|
|
# through the controller.
|
|
# If there are no VLAN type networks attached we need to
|
|
# still make it centralized.
|
|
enable_redirect = False
|
|
if networks:
|
|
enable_redirect = all(
|
|
net.get(pnet.NETWORK_TYPE) in [const.TYPE_VLAN,
|
|
const.TYPE_FLAT]
|
|
for net in networks)
|
|
if enable_redirect:
|
|
options[ovn_const.LRP_OPTIONS_REDIRECT_TYPE] = (
|
|
ovn_const.BRIDGE_REDIRECT_TYPE)
|
|
|
|
return options
|
|
|
|
def _create_lrouter_port(self, context, router, port, txn=None):
|
|
"""Create a logical router port."""
|
|
lrouter = utils.ovn_name(router['id'])
|
|
networks, ipv6_ra_configs = (
|
|
self._get_nets_and_ipv6_ra_confs_for_router_port(context, port))
|
|
lrouter_port_name = utils.ovn_lrouter_port_name(port['id'])
|
|
is_gw_port = const.DEVICE_OWNER_ROUTER_GW == port.get(
|
|
'device_owner')
|
|
columns = {}
|
|
columns['options'] = self._gen_router_port_options(port)
|
|
|
|
if is_gw_port:
|
|
port_net = self._plugin.get_network(n_context.get_admin_context(),
|
|
port['network_id'])
|
|
physnet = self._get_physnet(port_net)
|
|
candidates = self.get_candidates_for_scheduling(
|
|
physnet, availability_zone_hints=common_utils.get_az_hints(
|
|
router))
|
|
selected_chassis = self._ovn_scheduler.select(
|
|
self._nb_idl, lrouter_port_name, candidates=candidates)
|
|
if selected_chassis:
|
|
columns['gateway_chassis'] = selected_chassis
|
|
|
|
lsp_address = ovn_const.DEFAULT_ADDR_FOR_LSP_WITH_PEER
|
|
if ipv6_ra_configs:
|
|
columns['ipv6_ra_configs'] = ipv6_ra_configs
|
|
|
|
commands = [
|
|
self._nb_idl.add_lrouter_port(
|
|
name=lrouter_port_name,
|
|
lrouter=lrouter,
|
|
mac=port['mac_address'],
|
|
networks=networks,
|
|
may_exist=True,
|
|
external_ids=self._gen_router_port_ext_ids(port),
|
|
**columns),
|
|
self._nb_idl.set_lrouter_port_in_lswitch_port(
|
|
port['id'], lrouter_port_name, is_gw_port=is_gw_port,
|
|
lsp_address=lsp_address)]
|
|
self._transaction(commands, txn=txn)
|
|
|
|
def create_router_port(self, context, router_id, router_interface):
|
|
port = self._plugin.get_port(context, router_interface['port_id'])
|
|
router = self._l3_plugin.get_router(context, router_id)
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
multi_prefix = False
|
|
if (len(router_interface.get('subnet_ids', [])) == 1 and
|
|
len(port['fixed_ips']) > 1):
|
|
|
|
# NOTE(lizk) It's adding a subnet onto an already
|
|
# existing router interface port, try to update lrouter port
|
|
# 'networks' column.
|
|
self._update_lrouter_port(context, port, txn=txn)
|
|
multi_prefix = True
|
|
else:
|
|
self._create_lrouter_port(context, router, port, txn=txn)
|
|
|
|
if router.get(l3.EXTERNAL_GW_INFO):
|
|
cidr = None
|
|
for fixed_ip in port['fixed_ips']:
|
|
subnet = self._plugin.get_subnet(context,
|
|
fixed_ip['subnet_id'])
|
|
if multi_prefix:
|
|
if 'subnet_id' in router_interface:
|
|
if subnet['id'] != router_interface['subnet_id']:
|
|
continue
|
|
if subnet['ip_version'] == const.IP_VERSION_4:
|
|
cidr = subnet['cidr']
|
|
|
|
if ovn_conf.is_ovn_emit_need_to_frag_enabled():
|
|
provider_net = self._plugin.get_network(
|
|
context, router[l3.EXTERNAL_GW_INFO]['network_id'])
|
|
self.set_gateway_mtu(context, provider_net)
|
|
|
|
if utils.is_snat_enabled(router) and cidr:
|
|
self.update_nat_rules(router, networks=[cidr],
|
|
enable_snat=True, txn=txn)
|
|
if ovn_conf.is_ovn_distributed_floating_ip():
|
|
router_gw_ports = self._get_router_gw_ports(context,
|
|
router_id)
|
|
for router_port in router_gw_ports:
|
|
self._update_lrouter_port(context, router_port,
|
|
txn=txn)
|
|
|
|
db_rev.bump_revision(context, port, ovn_const.TYPE_ROUTER_PORTS)
|
|
|
|
def _update_lrouter_port(self, context, port, if_exists=False, txn=None):
|
|
"""Update a logical router port."""
|
|
networks, ipv6_ra_configs = (
|
|
self._get_nets_and_ipv6_ra_confs_for_router_port(context, port))
|
|
|
|
lsp_address = ovn_const.DEFAULT_ADDR_FOR_LSP_WITH_PEER
|
|
lrp_name = utils.ovn_lrouter_port_name(port['id'])
|
|
update = {'networks': networks, 'ipv6_ra_configs': ipv6_ra_configs}
|
|
is_gw_port = const.DEVICE_OWNER_ROUTER_GW == port.get(
|
|
'device_owner')
|
|
commands = [
|
|
self._nb_idl.update_lrouter_port(
|
|
name=lrp_name,
|
|
external_ids=self._gen_router_port_ext_ids(port),
|
|
options=self._gen_router_port_options(port),
|
|
if_exists=if_exists,
|
|
**update),
|
|
self._nb_idl.set_lrouter_port_in_lswitch_port(
|
|
port['id'], lrp_name, is_gw_port=is_gw_port,
|
|
lsp_address=lsp_address)]
|
|
|
|
self._transaction(commands, txn=txn)
|
|
|
|
def update_router_port(self, context, port, if_exists=False):
|
|
lrp_name = utils.ovn_lrouter_port_name(port['id'])
|
|
check_rev_cmd = self._nb_idl.check_revision_number(
|
|
lrp_name, port, ovn_const.TYPE_ROUTER_PORTS)
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
txn.add(check_rev_cmd)
|
|
self._update_lrouter_port(context, port, if_exists=if_exists,
|
|
txn=txn)
|
|
|
|
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
|
|
db_rev.bump_revision(
|
|
context, port, ovn_const.TYPE_ROUTER_PORTS)
|
|
|
|
@tenacity.retry(wait=tenacity.wait_random(min=2, max=3),
|
|
stop=tenacity.stop_after_attempt(3))
|
|
def delete_mac_binding_entries_by_mac(self, mac):
|
|
"""Delete all MAC_Binding entries associated to this mac address
|
|
|
|
The reason for using ovsdb-client intead of sb_ovn.db_destroy
|
|
is refer to patch:
|
|
https://review.opendev.org/c/openstack/neutron/+/812805
|
|
"""
|
|
cmd = [
|
|
"OVN_Southbound", {
|
|
"op": "delete",
|
|
"table": "MAC_Binding",
|
|
"where": [
|
|
["mac", "==", mac]
|
|
]
|
|
}
|
|
]
|
|
return utils.OvsdbClientTransactCommand.run(cmd)
|
|
|
|
def _delete_lrouter_port(self, context, port_id, router_id=None, txn=None):
|
|
"""Delete a logical router port."""
|
|
commands = [self._nb_idl.lrp_del(
|
|
utils.ovn_lrouter_port_name(port_id),
|
|
utils.ovn_name(router_id) if router_id else None,
|
|
if_exists=True)]
|
|
self._transaction(commands, txn=txn)
|
|
db_rev.delete_revision(context, port_id, ovn_const.TYPE_ROUTER_PORTS)
|
|
|
|
def delete_router_port(self, context, port_id, router_id=None,
|
|
subnet_ids=None):
|
|
try:
|
|
ovn_port = self._nb_idl.lookup(
|
|
'Logical_Router_Port', utils.ovn_lrouter_port_name(port_id))
|
|
except idlutils.RowNotFound:
|
|
return
|
|
|
|
subnet_ids = subnet_ids or []
|
|
port_removed = False
|
|
port_mac = ovn_port.mac
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
port = None
|
|
try:
|
|
port = self._plugin.get_port(context, port_id)
|
|
# The router interface port still exists, call ovn to
|
|
# update it
|
|
self._update_lrouter_port(context, port, txn=txn)
|
|
except n_exc.PortNotFound:
|
|
# The router interface port doesn't exist any more,
|
|
# we will call ovn to delete it once we remove the snat
|
|
# rules in the router itself if we have to
|
|
port_removed = True
|
|
|
|
router_id = router_id or ovn_port.external_ids.get(
|
|
ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY)
|
|
if port and not router_id:
|
|
router_id = port.get('device_id')
|
|
|
|
router = None
|
|
if router_id:
|
|
try:
|
|
router = self._l3_plugin.get_router(context, router_id)
|
|
except l3_exc.RouterNotFound:
|
|
# If the router is gone, the router port is also gone
|
|
port_removed = True
|
|
|
|
if not router or not router.get(l3.EXTERNAL_GW_INFO):
|
|
if port_removed:
|
|
self._delete_lrouter_port(context, port_id, router_id,
|
|
txn=txn)
|
|
if port_mac:
|
|
self.delete_mac_binding_entries_by_mac(port_mac)
|
|
return
|
|
|
|
if not subnet_ids:
|
|
subnet_ids = ovn_port.external_ids.get(
|
|
ovn_const.OVN_SUBNET_EXT_IDS_KEY, [])
|
|
subnet_ids = subnet_ids.split()
|
|
elif port:
|
|
subnet_ids = utils.get_port_subnet_ids(port)
|
|
|
|
if (ovn_conf.is_ovn_emit_need_to_frag_enabled() and
|
|
router.get('gw_port_id')):
|
|
provider_net = self._plugin.get_network(
|
|
context, router[l3.EXTERNAL_GW_INFO]['network_id'])
|
|
self.set_gateway_mtu(context, provider_net, txn=txn)
|
|
|
|
cidr = None
|
|
for sid in subnet_ids:
|
|
try:
|
|
subnet = self._plugin.get_subnet(context, sid)
|
|
except n_exc.SubnetNotFound:
|
|
continue
|
|
if subnet['ip_version'] == const.IP_VERSION_4:
|
|
cidr = subnet['cidr']
|
|
break
|
|
|
|
if utils.is_snat_enabled(router) and cidr:
|
|
self.update_nat_rules(
|
|
router, networks=[cidr], enable_snat=False, txn=txn)
|
|
|
|
if ovn_conf.is_ovn_distributed_floating_ip():
|
|
router_gw_ports = self._get_router_gw_ports(context, router_id)
|
|
for router_port in router_gw_ports:
|
|
self._update_lrouter_port(context, router_port, txn=txn)
|
|
|
|
# NOTE(mangelajo): If the port doesn't exist anymore, we
|
|
# delete the router port as the last operation and update the
|
|
# revision database to ensure consistency
|
|
if port_removed:
|
|
self._delete_lrouter_port(context, port_id, router_id, txn=txn)
|
|
if port_mac:
|
|
self.delete_mac_binding_entries_by_mac(port_mac)
|
|
else:
|
|
# otherwise, we just update the revision database
|
|
db_rev.bump_revision(
|
|
context, port, ovn_const.TYPE_ROUTER_PORTS)
|
|
|
|
def update_nat_rules(self, router, networks, enable_snat, txn=None):
|
|
"""Update the NAT rules in a logical router."""
|
|
context = n_context.get_admin_context()
|
|
func = (self._nb_idl.add_nat_rule_in_lrouter if enable_snat else
|
|
self._nb_idl.delete_nat_rule_in_lrouter)
|
|
gw_lrouter_name = utils.ovn_name(router['id'])
|
|
gateways = self._get_gw_info(context, router)
|
|
# Update NAT rules only for IPv4 subnets
|
|
commands = [func(gw_lrouter_name, type='snat', logical_ip=network,
|
|
external_ip=gw_info.router_ip) for gw_info in gateways
|
|
if gw_info.ip_version != const.IP_VERSION_6
|
|
for network in networks]
|
|
self._transaction(commands, txn=txn)
|
|
|
|
def create_provnet_port(self, network_id, segment, txn=None):
|
|
tag = segment.get(segment_def.SEGMENTATION_ID, [])
|
|
physnet = segment.get(segment_def.PHYSICAL_NETWORK)
|
|
fdb_enabled = ('true' if ovn_conf.is_learn_fdb_enabled()
|
|
else 'false')
|
|
options = {'network_name': physnet,
|
|
ovn_const.LSP_OPTIONS_MCAST_FLOOD_REPORTS: 'true',
|
|
ovn_const.LSP_OPTIONS_MCAST_FLOOD: 'false',
|
|
ovn_const.LSP_OPTIONS_LOCALNET_LEARN_FDB: fdb_enabled}
|
|
cmd = self._nb_idl.create_lswitch_port(
|
|
lport_name=utils.ovn_provnet_port_name(segment['id']),
|
|
lswitch_name=utils.ovn_name(network_id),
|
|
addresses=[ovn_const.UNKNOWN_ADDR],
|
|
external_ids={},
|
|
type=ovn_const.LSP_TYPE_LOCALNET,
|
|
tag=tag,
|
|
options=options)
|
|
self._transaction([cmd], txn=txn)
|
|
|
|
def delete_provnet_port(self, network_id, segment):
|
|
port_to_del = utils.ovn_provnet_port_name(segment['id'])
|
|
legacy_port_name = utils.ovn_provnet_port_name(network_id)
|
|
physnet = segment.get(segment_def.PHYSICAL_NETWORK)
|
|
lswitch = self._nb_idl.get_lswitch(utils.ovn_name(network_id))
|
|
lports = [lp.name for lp in lswitch.ports]
|
|
|
|
# Cover the situation where localnet ports
|
|
# were named after network_id and not segment_id.
|
|
# TODO(mjozefcz): Remove this in w-release.
|
|
if (port_to_del not in lports and
|
|
legacy_port_name in lports):
|
|
for lport in lswitch.ports:
|
|
if (legacy_port_name == lport.name and
|
|
lport.options['network_name'] == physnet):
|
|
port_to_del = legacy_port_name
|
|
break
|
|
|
|
cmd = self._nb_idl.delete_lswitch_port(
|
|
lport_name=port_to_del,
|
|
lswitch_name=utils.ovn_name(network_id))
|
|
self._transaction([cmd])
|
|
|
|
def _gen_network_parameters(self, network):
|
|
params = {'external_ids': {
|
|
ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: network['name'],
|
|
ovn_const.OVN_NETWORK_MTU_EXT_ID_KEY: str(network['mtu']),
|
|
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(
|
|
utils.get_revision_number(network, ovn_const.TYPE_NETWORKS)),
|
|
ovn_const.OVN_AZ_HINTS_EXT_ID_KEY:
|
|
','.join(common_utils.get_az_hints(network))}}
|
|
|
|
# Enable IGMP snooping if igmp_snooping_enable is enabled in Neutron
|
|
value = 'true' if ovn_conf.is_igmp_snooping_enabled() else 'false'
|
|
vlan_transparent = (
|
|
'true' if network.get('vlan_transparent') else 'false')
|
|
params['other_config'] = {ovn_const.MCAST_SNOOP: value,
|
|
ovn_const.MCAST_FLOOD_UNREGISTERED: 'false',
|
|
ovn_const.VLAN_PASSTHRU: vlan_transparent}
|
|
return params
|
|
|
|
def create_network(self, context, network):
|
|
# Create a logical switch with a name equal to the Neutron network
|
|
# UUID. This provides an easy way to refer to the logical switch
|
|
# without having to track what UUID OVN assigned to it.
|
|
lswitch_params = self._gen_network_parameters(network)
|
|
lswitch_name = utils.ovn_name(network['id'])
|
|
# NOTE(mjozefcz): Remove this workaround when bug
|
|
# 1869877 will be fixed.
|
|
segments = segments_db.get_network_segments(
|
|
context, network['id'])
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
txn.add(self._nb_idl.ls_add(lswitch_name, **lswitch_params,
|
|
may_exist=True))
|
|
for segment in segments:
|
|
if segment.get(segment_def.PHYSICAL_NETWORK):
|
|
self.create_provnet_port(network['id'], segment, txn=txn)
|
|
db_rev.bump_revision(context, network, ovn_const.TYPE_NETWORKS)
|
|
self.create_metadata_port(context, network)
|
|
return network
|
|
|
|
def delete_network(self, context, network_id):
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
ls_name = utils.ovn_name(network_id)
|
|
ls, ls_dns_record = self._nb_idl.get_ls_and_dns_record(ls_name)
|
|
|
|
txn.add(self._nb_idl.ls_del(ls_name, if_exists=True))
|
|
if ls_dns_record:
|
|
txn.add(self._nb_idl.dns_del(ls_dns_record.uuid))
|
|
txn.add(self._nb_idl.ha_chassis_group_del(ls_name, if_exists=True))
|
|
db_rev.delete_revision(
|
|
context, network_id, ovn_const.TYPE_NETWORKS)
|
|
|
|
def set_gateway_mtu(self, context, prov_net, txn=None):
|
|
ports = self._plugin.get_ports(
|
|
context, filters=dict(network_id=[prov_net['id']],
|
|
device_owner=[const.DEVICE_OWNER_ROUTER_GW]))
|
|
commands = []
|
|
for port in ports:
|
|
lrp_name = utils.ovn_lrouter_port_name(port['id'])
|
|
options = self._gen_router_port_options(port, prov_net)
|
|
commands.append(self._nb_idl.lrp_set_options(lrp_name, **options))
|
|
self._transaction(commands, txn=txn)
|
|
|
|
def update_network(self, context, network, original_network=None):
|
|
lswitch_name = utils.ovn_name(network['id'])
|
|
check_rev_cmd = self._nb_idl.check_revision_number(
|
|
lswitch_name, network, ovn_const.TYPE_NETWORKS)
|
|
|
|
# TODO(numans) - When a network's dns domain name is updated, we need
|
|
# to update the DNS records for this network in DNS OVN NB DB table.
|
|
# (https://bugs.launchpad.net/networking-ovn/+bug/1777978)
|
|
# Eg. if the network n1's dns domain name was "test1" and if it has
|
|
# 2 bound ports - p1 and p2, we would have created the below dns
|
|
# records
|
|
# ===========================
|
|
# p1 = P1_IP
|
|
# p1.test1 = P1_IP
|
|
# p1.default_domain = P1_IP
|
|
# p2 = P2_IP
|
|
# p2.test1 = P2_IP
|
|
# p2.default_domain = P2_IP
|
|
# ===========================
|
|
# if the network n1's dns domain name is updated to test2, then we need
|
|
# to delete the below DNS records
|
|
# ===========================
|
|
# p1.test1 = P1_IP
|
|
# p2.test1 = P2_IP
|
|
# ===========================
|
|
# and add the new ones
|
|
# ===========================
|
|
# p1.test2 = P1_IP
|
|
# p2.test2 = P2_IP
|
|
# ===========================
|
|
# in the DNS row for this network.
|
|
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
txn.add(check_rev_cmd)
|
|
lswitch_params = self._gen_network_parameters(network)
|
|
lswitch = self._nb_idl.get_lswitch(lswitch_name)
|
|
txn.add(self._nb_idl.db_set(
|
|
'Logical_Switch', lswitch_name, *lswitch_params.items()))
|
|
# Check if previous mtu is different than current one,
|
|
# checking will help reduce number of operations
|
|
if (not lswitch or
|
|
lswitch.external_ids.get(
|
|
ovn_const.OVN_NETWORK_MTU_EXT_ID_KEY) !=
|
|
str(network['mtu'])):
|
|
subnets = self._plugin.get_subnets_by_network(
|
|
context, network['id'])
|
|
for subnet in subnets:
|
|
self.update_subnet(context, subnet, network, txn)
|
|
|
|
if utils.is_external_network(network):
|
|
# make sure to use admin context as this is a external
|
|
# network
|
|
self.set_gateway_mtu(n_context.get_admin_context(),
|
|
network, txn)
|
|
|
|
if self.is_external_ports_supported():
|
|
# If there are no external ports in this network, there's
|
|
# no need to check the AZs
|
|
if any([p for p in lswitch.ports if
|
|
p.type == ovn_const.LSP_TYPE_EXTERNAL]):
|
|
# Check for changes in the network Availability Zones
|
|
ovn_ls_azs = lswitch.external_ids.get(
|
|
ovn_const.OVN_AZ_HINTS_EXT_ID_KEY, '')
|
|
neutron_net_azs = lswitch_params['external_ids'].get(
|
|
ovn_const.OVN_AZ_HINTS_EXT_ID_KEY, '')
|
|
if ovn_ls_azs != neutron_net_azs:
|
|
utils.sync_ha_chassis_group(
|
|
context, network['id'], self._nb_idl,
|
|
self._sb_idl, txn)
|
|
|
|
# Update the segment tags, if any
|
|
segments = segments_db.get_network_segments(context, network['id'])
|
|
for segment in segments:
|
|
tag = segment.get(segment_def.SEGMENTATION_ID)
|
|
tag = [] if tag is None else tag
|
|
lport_name = utils.ovn_provnet_port_name(segment['id'])
|
|
txn.add(self._nb_idl.set_lswitch_port(lport_name=lport_name,
|
|
tag=tag, if_exists=True))
|
|
|
|
self._qos_driver.update_network(txn, network, original_network)
|
|
|
|
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
|
|
db_rev.bump_revision(context, network, ovn_const.TYPE_NETWORKS)
|
|
|
|
def _add_subnet_dhcp_options(self, subnet, network,
|
|
ovn_dhcp_options=None):
|
|
if utils.is_dhcp_options_ignored(subnet):
|
|
return
|
|
|
|
if not ovn_dhcp_options:
|
|
ovn_dhcp_options = self._get_ovn_dhcp_options(subnet, network)
|
|
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
rev_num = {ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(
|
|
utils.get_revision_number(subnet, ovn_const.TYPE_SUBNETS))}
|
|
ovn_dhcp_options['external_ids'].update(rev_num)
|
|
txn.add(self._nb_idl.add_dhcp_options(subnet['id'],
|
|
**ovn_dhcp_options))
|
|
|
|
def _get_ovn_dhcp_options(self, subnet, network, server_mac=None):
|
|
external_ids = {
|
|
'subnet_id': subnet['id'],
|
|
ovn_const.OVN_REV_NUM_EXT_ID_KEY: str(utils.get_revision_number(
|
|
subnet, ovn_const.TYPE_SUBNETS))}
|
|
dhcp_options = {'cidr': subnet['cidr'], 'options': {},
|
|
'external_ids': external_ids}
|
|
|
|
if subnet['enable_dhcp']:
|
|
if subnet['ip_version'] == const.IP_VERSION_4:
|
|
dhcp_options['options'] = self._get_ovn_dhcpv4_opts(
|
|
subnet, network, server_mac=server_mac)
|
|
else:
|
|
dhcp_options['options'] = self._get_ovn_dhcpv6_opts(
|
|
subnet, server_id=server_mac)
|
|
|
|
return dhcp_options
|
|
|
|
def _process_global_dhcp_opts(self, options, ip_version):
|
|
if ip_version == const.IP_VERSION_4:
|
|
global_options = ovn_conf.get_global_dhcpv4_opts()
|
|
else:
|
|
global_options = ovn_conf.get_global_dhcpv6_opts()
|
|
|
|
for option, value in global_options.items():
|
|
if option in ovn_const.GLOBAL_DHCP_OPTS_PROHIBIT_LIST[ip_version]:
|
|
# This option is not allowed to be set with a global setting
|
|
LOG.debug('DHCP option %s is not permitted to be set in '
|
|
'global options. This option will be ignored.',
|
|
option)
|
|
continue
|
|
# If the value is null (i.e. config ntp_server:), treat it as
|
|
# a request to remove the option
|
|
if value:
|
|
options[option] = value
|
|
else:
|
|
try:
|
|
del(options[option])
|
|
except KeyError:
|
|
# Option not present, job done
|
|
pass
|
|
|
|
def _get_ovn_dhcpv4_opts(self, subnet, network, server_mac=None):
|
|
metadata_port_ip = self._find_metadata_port_ip(
|
|
n_context.get_admin_context(), subnet)
|
|
# TODO(dongj): Currently the metadata port is created only when
|
|
# ovn_metadata_enabled is true, therefore this is a restriction for
|
|
# supporting DHCP of subnet without gateway IP.
|
|
# We will remove this restriction later.
|
|
service_id = subnet['gateway_ip'] or metadata_port_ip
|
|
if not service_id:
|
|
return {}
|
|
|
|
default_lease_time = str(ovn_conf.get_ovn_dhcp_default_lease_time())
|
|
mtu = network['mtu']
|
|
options = {
|
|
'server_id': service_id,
|
|
'lease_time': default_lease_time,
|
|
'mtu': str(mtu),
|
|
}
|
|
|
|
if cfg.CONF.dns_domain and cfg.CONF.dns_domain != 'openstacklocal':
|
|
# NOTE(mjozefcz): String field should be with quotes,
|
|
# otherwise ovn will try to resolve it as variable.
|
|
options['domain_name'] = '"%s"' % cfg.CONF.dns_domain
|
|
|
|
if subnet['gateway_ip']:
|
|
options['router'] = subnet['gateway_ip']
|
|
|
|
if server_mac:
|
|
options['server_mac'] = server_mac
|
|
else:
|
|
options['server_mac'] = n_net.get_random_mac(
|
|
cfg.CONF.base_mac.split(':'))
|
|
|
|
dns_servers = utils.get_dhcp_dns_servers(subnet)
|
|
if dns_servers:
|
|
options['dns_server'] = '{%s}' % ', '.join(dns_servers)
|
|
else:
|
|
LOG.warning("No relevant dns_servers defined for subnet %s. Check "
|
|
"the /etc/resolv.conf file",
|
|
subnet['id'])
|
|
|
|
routes = []
|
|
if metadata_port_ip:
|
|
routes.append('%s,%s' % (
|
|
const.METADATA_V4_CIDR, metadata_port_ip))
|
|
|
|
# Add subnet host_routes to 'classless_static_route' dhcp option
|
|
routes.extend(['%s,%s' % (route['destination'], route['nexthop'])
|
|
for route in subnet['host_routes']])
|
|
|
|
if routes:
|
|
# if there are static routes, then we need to add the
|
|
# default route in this option. As per RFC 3442 dhcp clients
|
|
# should ignore 'router' dhcp option (option 3)
|
|
# if option 121 is present.
|
|
if subnet['gateway_ip']:
|
|
routes.append('0.0.0.0/0,%s' % subnet['gateway_ip'])
|
|
|
|
options['classless_static_route'] = '{' + ', '.join(routes) + '}'
|
|
|
|
self._process_global_dhcp_opts(options, ip_version=const.IP_VERSION_4)
|
|
|
|
return options
|
|
|
|
def _get_ovn_dhcpv6_opts(self, subnet, server_id=None):
|
|
"""Returns the DHCPv6 options"""
|
|
|
|
dhcpv6_opts = {
|
|
'server_id': server_id or n_net.get_random_mac(
|
|
cfg.CONF.base_mac.split(':'))
|
|
}
|
|
|
|
dns_servers = utils.get_dhcp_dns_servers(subnet,
|
|
ip_version=const.IP_VERSION_6)
|
|
if dns_servers:
|
|
dhcpv6_opts['dns_server'] = '{%s}' % ', '.join(dns_servers)
|
|
|
|
if subnet.get('ipv6_address_mode') == const.DHCPV6_STATELESS:
|
|
dhcpv6_opts[ovn_const.DHCPV6_STATELESS_OPT] = 'true'
|
|
|
|
self._process_global_dhcp_opts(dhcpv6_opts,
|
|
ip_version=const.IP_VERSION_6)
|
|
|
|
return dhcpv6_opts
|
|
|
|
def _remove_subnet_dhcp_options(self, subnet_id, txn):
|
|
dhcp_options = self._nb_idl.get_subnet_dhcp_options(
|
|
subnet_id, with_ports=True)
|
|
|
|
if dhcp_options['subnet']:
|
|
txn.add(self._nb_idl.delete_dhcp_options(
|
|
dhcp_options['subnet']['uuid']))
|
|
|
|
# Remove subnet and port DHCP_Options rows, the DHCP options in
|
|
# lsp rows will be removed by related UUID
|
|
for opt in dhcp_options['ports']:
|
|
txn.add(self._nb_idl.delete_dhcp_options(opt['uuid']))
|
|
|
|
def _enable_subnet_dhcp_options(self, subnet, network, txn):
|
|
if utils.is_dhcp_options_ignored(subnet):
|
|
return
|
|
|
|
filters = {'fixed_ips': {'subnet_id': [subnet['id']]}}
|
|
all_ports = self._plugin.get_ports(n_context.get_admin_context(),
|
|
filters=filters)
|
|
ports = [p for p in all_ports if not utils.is_network_device_port(p)]
|
|
|
|
dhcp_options = self._get_ovn_dhcp_options(subnet, network)
|
|
subnet_dhcp_cmd = self._nb_idl.add_dhcp_options(subnet['id'],
|
|
**dhcp_options)
|
|
subnet_dhcp_option = txn.add(subnet_dhcp_cmd)
|
|
# Traverse ports to add port DHCP_Options rows
|
|
for port in ports:
|
|
lsp_dhcp_disabled, lsp_dhcp_opts = utils.get_lsp_dhcp_opts(
|
|
port, subnet['ip_version'])
|
|
if lsp_dhcp_disabled:
|
|
continue
|
|
if not lsp_dhcp_opts:
|
|
lsp_dhcp_options = subnet_dhcp_option
|
|
else:
|
|
port_dhcp_options = copy.deepcopy(dhcp_options)
|
|
port_dhcp_options['options'].update(lsp_dhcp_opts)
|
|
port_dhcp_options['external_ids'].update(
|
|
{'port_id': port['id']})
|
|
lsp_dhcp_options = txn.add(self._nb_idl.add_dhcp_options(
|
|
subnet['id'], port_id=port['id'],
|
|
**port_dhcp_options))
|
|
columns = ({'dhcpv6_options': lsp_dhcp_options} if
|
|
subnet['ip_version'] == const.IP_VERSION_6 else {
|
|
'dhcpv4_options': lsp_dhcp_options})
|
|
|
|
# Set lsp DHCP options
|
|
txn.add(self._nb_idl.set_lswitch_port(
|
|
lport_name=port['id'], **columns))
|
|
|
|
def _update_subnet_dhcp_options(self, subnet, network, txn):
|
|
if utils.is_dhcp_options_ignored(subnet):
|
|
return
|
|
original_options = self._nb_idl.get_subnet_dhcp_options(
|
|
subnet['id'])['subnet']
|
|
mac = None
|
|
if original_options:
|
|
if subnet['ip_version'] == const.IP_VERSION_6:
|
|
mac = original_options['options'].get('server_id')
|
|
else:
|
|
mac = original_options['options'].get('server_mac')
|
|
new_options = self._get_ovn_dhcp_options(subnet, network, mac)
|
|
# Check whether DHCP changed
|
|
if (original_options and
|
|
original_options['cidr'] == new_options['cidr'] and
|
|
original_options['options'] == new_options['options']):
|
|
return
|
|
txn.add(self._nb_idl.add_dhcp_options(subnet['id'], **new_options))
|
|
dhcp_options = self._nb_idl.get_subnet_dhcp_options(
|
|
subnet['id'], with_ports=True)
|
|
|
|
# When a subnet dns_nameserver is updated, then we should update
|
|
# the port dhcp options for ports (with no port specific dns_server
|
|
# defined).
|
|
if 'options' in new_options and 'options' in original_options:
|
|
orig_dns_server = original_options['options'].get('dns_server')
|
|
new_dns_server = new_options['options'].get('dns_server')
|
|
dns_server_changed = (orig_dns_server != new_dns_server)
|
|
else:
|
|
dns_server_changed = False
|
|
|
|
for opt in dhcp_options['ports']:
|
|
if not new_options.get('options'):
|
|
continue
|
|
options = dict(new_options['options'])
|
|
p_dns_server = opt['options'].get('dns_server')
|
|
if dns_server_changed and (orig_dns_server == p_dns_server):
|
|
# If port has its own dns_server option defined, then
|
|
# orig_dns_server and p_dns_server will not match.
|
|
opt['options']['dns_server'] = new_dns_server
|
|
options.update(opt['options'])
|
|
|
|
port_id = opt['external_ids']['port_id']
|
|
txn.add(self._nb_idl.add_dhcp_options(
|
|
subnet['id'], port_id=port_id, options=options))
|
|
|
|
def create_subnet(self, context, subnet, network):
|
|
if subnet['enable_dhcp']:
|
|
mport_updated = False
|
|
if subnet['ip_version'] == const.IP_VERSION_4:
|
|
mport_updated = self.update_metadata_port(
|
|
context, network, subnet=subnet)
|
|
if subnet['ip_version'] == const.IP_VERSION_6 or not mport_updated:
|
|
# NOTE(ralonsoh): if IPv4 but the metadata port has not been
|
|
# updated, the DHPC options register has not been created.
|
|
self._add_subnet_dhcp_options(subnet, network)
|
|
db_rev.bump_revision(context, subnet, ovn_const.TYPE_SUBNETS)
|
|
|
|
def _modify_subnet_dhcp_options(self, subnet, ovn_subnet, network, txn):
|
|
if subnet['enable_dhcp'] and not ovn_subnet:
|
|
self._enable_subnet_dhcp_options(subnet, network, txn)
|
|
elif subnet['enable_dhcp'] and ovn_subnet:
|
|
self._update_subnet_dhcp_options(subnet, network, txn)
|
|
elif not subnet['enable_dhcp'] and ovn_subnet:
|
|
self._remove_subnet_dhcp_options(subnet['id'], txn)
|
|
|
|
def update_subnet(self, context, subnet, network, txn=None):
|
|
ovn_subnet = self._nb_idl.get_subnet_dhcp_options(
|
|
subnet['id'])['subnet']
|
|
|
|
if subnet['enable_dhcp'] or ovn_subnet:
|
|
self.update_metadata_port(context, network, subnet=subnet)
|
|
|
|
check_rev_cmd = self._nb_idl.check_revision_number(
|
|
subnet['id'], subnet, ovn_const.TYPE_SUBNETS)
|
|
if not txn:
|
|
with self._nb_idl.transaction(check_error=True) as txn_n:
|
|
txn_n.add(check_rev_cmd)
|
|
self._modify_subnet_dhcp_options(subnet, ovn_subnet, network,
|
|
txn_n)
|
|
else:
|
|
self._modify_subnet_dhcp_options(subnet, ovn_subnet, network, txn)
|
|
if check_rev_cmd.result == ovn_const.TXN_COMMITTED:
|
|
db_rev.bump_revision(context, subnet, ovn_const.TYPE_SUBNETS)
|
|
|
|
def delete_subnet(self, context, subnet_id):
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
self._remove_subnet_dhcp_options(subnet_id, txn)
|
|
db_rev.delete_revision(
|
|
context, subnet_id, ovn_const.TYPE_SUBNETS)
|
|
|
|
def create_security_group(self, context, security_group):
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: security_group['id']}
|
|
name = utils.ovn_port_group_name(security_group['id'])
|
|
txn.add(self._nb_idl.pg_add(
|
|
name=name, acls=[], external_ids=ext_ids))
|
|
# When a SG is created, it comes with some default rules,
|
|
# so we'll apply them to the Port Group.
|
|
ovn_acl.add_acls_for_sg_port_group(
|
|
self._nb_idl, security_group, txn,
|
|
self.is_allow_stateless_supported())
|
|
db_rev.bump_revision(
|
|
context, security_group, ovn_const.TYPE_SECURITY_GROUPS)
|
|
|
|
def _add_port_to_drop_port_group(self, port, txn):
|
|
txn.add(self._nb_idl.pg_add_ports(ovn_const.OVN_DROP_PORT_GROUP_NAME,
|
|
port))
|
|
|
|
def _del_port_from_drop_port_group(self, port, txn):
|
|
pg_name = ovn_const.OVN_DROP_PORT_GROUP_NAME
|
|
if self._nb_idl.get_port_group(pg_name):
|
|
txn.add(self._nb_idl.pg_del_ports(pg_name, port))
|
|
|
|
def delete_security_group(self, context, security_group_id):
|
|
with self._nb_idl.transaction(check_error=True) as txn:
|
|
name = utils.ovn_port_group_name(security_group_id)
|
|
txn.add(self._nb_idl.pg_del(name=name, if_exists=True))
|
|
db_rev.delete_revision(context, security_group_id,
|
|
ovn_const.TYPE_SECURITY_GROUPS)
|
|
|
|
def _process_security_group_rule(self, rule, is_add_acl=True):
|
|
admin_context = n_context.get_admin_context()
|
|
ovn_acl.update_acls_for_security_group(
|
|
self._plugin, admin_context, self._nb_idl,
|
|
rule['security_group_id'], rule,
|
|
is_add_acl=is_add_acl,
|
|
stateless_supported=self.is_allow_stateless_supported())
|
|
|
|
def create_security_group_rule(self, context, rule):
|
|
self._process_security_group_rule(rule)
|
|
db_rev.bump_revision(
|
|
context, rule, ovn_const.TYPE_SECURITY_GROUP_RULES)
|
|
|
|
def delete_security_group_rule(self, context, rule):
|
|
self._process_security_group_rule(rule, is_add_acl=False)
|
|
db_rev.delete_revision(
|
|
context, rule['id'], ovn_const.TYPE_SECURITY_GROUP_RULES)
|
|
|
|
@staticmethod
|
|
def is_metadata_port(port):
|
|
return port['device_owner'] == const.DEVICE_OWNER_DISTRIBUTED
|
|
|
|
def _find_metadata_port(self, context, network_id):
|
|
if not ovn_conf.is_ovn_metadata_enabled():
|
|
return
|
|
|
|
ports = self._plugin.get_ports(
|
|
context, filters=dict(
|
|
network_id=[network_id],
|
|
device_owner=[const.DEVICE_OWNER_DISTRIBUTED]),
|
|
limit=1)
|
|
|
|
if ports:
|
|
return ports[0]
|
|
|
|
def _find_metadata_port_ip(self, context, subnet):
|
|
metadata_port = self._find_metadata_port(context, subnet['network_id'])
|
|
if metadata_port:
|
|
for fixed_ip in metadata_port['fixed_ips']:
|
|
if fixed_ip['subnet_id'] == subnet['id']:
|
|
return fixed_ip['ip_address']
|
|
|
|
def create_metadata_port(self, context, network):
|
|
if not ovn_conf.is_ovn_metadata_enabled():
|
|
return
|
|
|
|
metadata_port = self._find_metadata_port(context, network['id'])
|
|
if metadata_port:
|
|
return metadata_port
|
|
|
|
# Create a neutron port for DHCP/metadata services
|
|
filters = {'network_id': [network['id']]}
|
|
subnets = self._plugin.get_subnets(context, filters=filters)
|
|
fixed_ips = [{'subnet_id': s['id']}
|
|
for s in subnets if s['enable_dhcp']]
|
|
port = {'port': {'network_id': network['id'],
|
|
'tenant_id': network['project_id'],
|
|
'device_owner': const.DEVICE_OWNER_DISTRIBUTED,
|
|
'device_id': 'ovnmeta-%s' % network['id'],
|
|
'fixed_ips': fixed_ips,
|
|
}
|
|
}
|
|
# TODO(boden): rehome create_port into neutron-lib
|
|
return p_utils.create_port(self._plugin, context, port)
|
|
|
|
def update_metadata_port(self, context, network, subnet=None):
|
|
"""Update metadata port.
|
|
|
|
This function will allocate an IP address for the metadata port of
|
|
the given network in all its IPv4 subnets or the given subnet. Returns
|
|
"True" if the metadata port has been updated and "False" if OVN
|
|
metadata is disabled or the metadata port does not exist or
|
|
cannot be created.
|
|
"""
|
|
network_id = network['id']
|
|
|
|
def update_metadata_port_fixed_ips(metadata_port, add_subnet_ids,
|
|
del_subnet_ids):
|
|
wanted_fixed_ips = [
|
|
{'subnet_id': fixed_ip['subnet_id'],
|
|
'ip_address': fixed_ip['ip_address']} for fixed_ip in
|
|
metadata_port['fixed_ips'] if
|
|
fixed_ip['subnet_id'] not in del_subnet_ids]
|
|
wanted_fixed_ips.extend({'subnet_id': s_id} for s_id in
|
|
add_subnet_ids)
|
|
port = {'id': metadata_port['id'],
|
|
'port': {'network_id': network_id,
|
|
'fixed_ips': wanted_fixed_ips}}
|
|
self._plugin.update_port(n_context.get_admin_context(),
|
|
metadata_port['id'], port)
|
|
|
|
if not ovn_conf.is_ovn_metadata_enabled():
|
|
return False
|
|
|
|
# Retrieve or create the metadata port of this network
|
|
metadata_port = self.create_metadata_port(context, network)
|
|
if not metadata_port:
|
|
LOG.error("Metadata port could not be found or created "
|
|
"for network %s", network_id)
|
|
return False
|
|
|
|
port_subnet_ids = set(ip['subnet_id'] for ip in
|
|
metadata_port['fixed_ips'])
|
|
|
|
# If this method is called from "create_subnet" or "update_subnet",
|
|
# only the fixed IP address from this subnet should be updated in the
|
|
# metadata port.
|
|
if subnet and subnet['id']:
|
|
if subnet['enable_dhcp'] and subnet['id'] not in port_subnet_ids:
|
|
update_metadata_port_fixed_ips(metadata_port,
|
|
[subnet['id']], [])
|
|
elif not subnet['enable_dhcp'] and subnet['id'] in port_subnet_ids:
|
|
update_metadata_port_fixed_ips(metadata_port,
|
|
[], [subnet['id']])
|
|
return True
|
|
|
|
# Retrieve all subnets in this network
|
|
subnets = self._plugin.get_subnets(context, filters=dict(
|
|
network_id=[network_id], ip_version=[const.IP_VERSION_4],
|
|
enable_dhcp=[True]))
|
|
|
|
subnet_ids = set(s['id'] for s in subnets)
|
|
|
|
# Find all subnets where metadata port doesn't have an IP in and
|
|
# allocate one.
|
|
if subnet_ids != port_subnet_ids:
|
|
update_metadata_port_fixed_ips(metadata_port,
|
|
subnet_ids - port_subnet_ids,
|
|
port_subnet_ids - subnet_ids)
|
|
|
|
return True
|
|
|
|
def get_parent_port(self, port_id):
|
|
return self._nb_idl.get_parent_port(port_id)
|
|
|
|
def is_dns_required_for_port(self, port):
|
|
try:
|
|
if not all([port['dns_name'], port['dns_assignment'],
|
|
port['device_id']]):
|
|
return False
|
|
except KeyError:
|
|
# Possible that dns extension is not enabled.
|
|
return False
|
|
|
|
if not self._nb_idl.is_table_present('DNS'):
|
|
return False
|
|
|
|
return True
|
|
|
|
def get_port_dns_records(self, port):
|
|
port_dns_records = {}
|
|
net = port.get('network', {})
|
|
net_dns_domain = net.get('dns_domain', '').rstrip('.')
|
|
|
|
for dns_assignment in port.get('dns_assignment', []):
|
|
hostname = dns_assignment['hostname']
|
|
fqdn = dns_assignment['fqdn'].rstrip('.')
|
|
net_dns_fqdn = hostname + '.' + net_dns_domain
|
|
if hostname not in port_dns_records:
|
|
port_dns_records[hostname] = dns_assignment['ip_address']
|
|
if net_dns_domain and net_dns_fqdn != fqdn:
|
|
port_dns_records[net_dns_fqdn] = (
|
|
dns_assignment['ip_address'])
|
|
else:
|
|
port_dns_records[hostname] += " " + (
|
|
dns_assignment['ip_address'])
|
|
if net_dns_domain and net_dns_fqdn != fqdn:
|
|
port_dns_records[hostname + '.' + net_dns_domain] += (
|
|
" " + dns_assignment['ip_address'])
|
|
|
|
if fqdn not in port_dns_records:
|
|
port_dns_records[fqdn] = dns_assignment['ip_address']
|
|