From 65692127f6e6d6e4980febb2107e867119a6d5c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20J=C3=B3zefczyk?= Date: Tue, 26 Nov 2019 17:01:45 +0100 Subject: [PATCH] [OVN] Move OVN commons to neutron tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move OVN related commons to neutron tree. Previous paths in networking-ovn tree: ./networking_ovn/common/constants.py -> ./neutron/common/ovn/constants.py ./networking_ovn/common/exceptions.py -> ./neutron/common/ovn/exceptions.py ./networking_ovn/common/utils.py -> ./neutron/common/ovn/utils.py ./networking_ovn/common/hash_ring_manager.py -> neutron/common/ovn/hash_ring_manager.py ./networking_ovn/common/config.py -> ./neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py Co-Authored-By: Gal Sagie Co-Authored-By: Boden R Co-Authored-By: Daniel Alvarez Co-Authored-By: Amitabha Biswas Co-Authored-By: Chandra S Vejendla Co-Authored-By: Babu Shanmugam Co-Authored-By: Lucas Alvares Gomes Co-Authored-By: Terry Wilson Co-Authored-By: Ramu Ramamurthy Co-Authored-By: Maciej Józefczyk Co-Authored-By: Gary Kotton Co-Authored-By: Andrew Austin Co-Authored-By: Miguel Angel Ajo Co-Authored-By: Brian Haley Co-Authored-By: Dong Jun Co-Authored-By: xurong00037997 Co-Authored-By: Rodolfo Alonso Hernandez Change-Id: Ib46bfdd14a150a324dbf28c6a50c839c5c824e35 Related-Blueprint: neutron-ovn-merge --- doc/source/configuration/config.rst | 1 + doc/source/configuration/ovn.rst | 6 + etc/oslo-config-generator/ovn.ini | 6 + neutron/common/ovn/__init__.py | 0 neutron/common/ovn/constants.py | 185 +++++++ neutron/common/ovn/exceptions.py | 38 ++ neutron/common/ovn/hash_ring_manager.py | 100 ++++ neutron/common/ovn/utils.py | 460 ++++++++++++++++++ .../conf/plugins/ml2/drivers/ovn/__init__.py | 0 .../conf/plugins/ml2/drivers/ovn/ovn_conf.py | 293 +++++++++++ neutron/tests/unit/common/ovn/__init__.py | 0 .../unit/common/ovn/test_hash_ring_manager.py | 134 +++++ neutron/tests/unit/common/ovn/test_utils.py | 105 ++++ setup.cfg | 1 + 14 files changed, 1329 insertions(+) create mode 100644 doc/source/configuration/ovn.rst create mode 100644 etc/oslo-config-generator/ovn.ini create mode 100644 neutron/common/ovn/__init__.py create mode 100644 neutron/common/ovn/constants.py create mode 100644 neutron/common/ovn/exceptions.py create mode 100644 neutron/common/ovn/hash_ring_manager.py create mode 100644 neutron/common/ovn/utils.py create mode 100644 neutron/conf/plugins/ml2/drivers/ovn/__init__.py create mode 100644 neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py create mode 100644 neutron/tests/unit/common/ovn/__init__.py create mode 100644 neutron/tests/unit/common/ovn/test_hash_ring_manager.py create mode 100644 neutron/tests/unit/common/ovn/test_utils.py diff --git a/doc/source/configuration/config.rst b/doc/source/configuration/config.rst index 3d8b3b7c3e9..78e119886ec 100644 --- a/doc/source/configuration/config.rst +++ b/doc/source/configuration/config.rst @@ -32,6 +32,7 @@ arbitrary file names. macvtap-agent.rst openvswitch-agent.rst sriov-agent.rst + ovn.rst .. toctree:: :maxdepth: 1 diff --git a/doc/source/configuration/ovn.rst b/doc/source/configuration/ovn.rst new file mode 100644 index 00000000000..d98c06424be --- /dev/null +++ b/doc/source/configuration/ovn.rst @@ -0,0 +1,6 @@ +======= +ovn.ini +======= + +.. show-options:: + :config-file: etc/oslo-config-generator/ovn.ini diff --git a/etc/oslo-config-generator/ovn.ini b/etc/oslo-config-generator/ovn.ini new file mode 100644 index 00000000000..004b032aa68 --- /dev/null +++ b/etc/oslo-config-generator/ovn.ini @@ -0,0 +1,6 @@ +[DEFAULT] +output_file = etc/neutron/ovn.ini.sample +wrap_width = 79 + +namespace = neutron.ml2.ovn +namespace = oslo.log diff --git a/neutron/common/ovn/__init__.py b/neutron/common/ovn/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/common/ovn/constants.py b/neutron/common/ovn/constants.py new file mode 100644 index 00000000000..c63a503ba04 --- /dev/null +++ b/neutron/common/ovn/constants.py @@ -0,0 +1,185 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_lib.api.definitions import portbindings +from neutron_lib import constants as const +import six + +# TODO(lucasagomes): Remove OVN_SG_NAME_EXT_ID_KEY in the Rocky release +OVN_SG_NAME_EXT_ID_KEY = 'neutron:security_group_name' +OVN_SG_EXT_ID_KEY = 'neutron:security_group_id' +OVN_SG_RULE_EXT_ID_KEY = 'neutron:security_group_rule_id' +OVN_ML2_MECH_DRIVER_NAME = 'ovn' +OVN_NETWORK_NAME_EXT_ID_KEY = 'neutron:network_name' +OVN_NETWORK_MTU_EXT_ID_KEY = 'neutron:mtu' +OVN_PORT_NAME_EXT_ID_KEY = 'neutron:port_name' +OVN_PORT_FIP_EXT_ID_KEY = 'neutron:port_fip' +OVN_ROUTER_NAME_EXT_ID_KEY = 'neutron:router_name' +OVN_ROUTER_IS_EXT_GW = 'neutron:is_ext_gw' +OVN_GW_PORT_EXT_ID_KEY = 'neutron:gw_port_id' +OVN_SUBNET_EXT_ID_KEY = 'neutron:subnet_id' +OVN_SUBNET_EXT_IDS_KEY = 'neutron:subnet_ids' +OVN_PHYSNET_EXT_ID_KEY = 'neutron:provnet-physical-network' +OVN_NETTYPE_EXT_ID_KEY = 'neutron:provnet-network-type' +OVN_SEGID_EXT_ID_KEY = 'neutron:provnet-segmentation-id' +OVN_PROJID_EXT_ID_KEY = 'neutron:project_id' +OVN_DEVID_EXT_ID_KEY = 'neutron:device_id' +OVN_CIDRS_EXT_ID_KEY = 'neutron:cidrs' +OVN_FIP_EXT_ID_KEY = 'neutron:fip_id' +OVN_FIP_PORT_EXT_ID_KEY = 'neutron:fip_port_id' +OVN_FIP_EXT_MAC_KEY = 'neutron:fip_external_mac' +OVN_REV_NUM_EXT_ID_KEY = 'neutron:revision_number' +OVN_QOS_POLICY_EXT_ID_KEY = 'neutron:qos_policy_id' +OVN_SG_IDS_EXT_ID_KEY = 'neutron:security_group_ids' +OVN_DEVICE_OWNER_EXT_ID_KEY = 'neutron:device_owner' +OVN_LIVENESS_CHECK_EXT_ID_KEY = 'neutron:liveness_check_at' +METADATA_LIVENESS_CHECK_EXT_ID_KEY = 'neutron:metadata_liveness_check_at' +OVN_PORT_BINDING_PROFILE = portbindings.PROFILE +OVN_PORT_BINDING_PROFILE_PARAMS = [{'parent_name': six.string_types, + 'tag': six.integer_types}, + {'vtep-physical-switch': six.string_types, + 'vtep-logical-switch': six.string_types}] +MIGRATING_ATTR = 'migrating_to' +OVN_ROUTER_PORT_OPTION_KEYS = ['router-port', 'nat-addresses'] +OVN_GATEWAY_CHASSIS_KEY = 'redirect-chassis' +OVN_CHASSIS_REDIRECT = 'chassisredirect' +OVN_GATEWAY_NAT_ADDRESSES_KEY = 'nat-addresses' +OVN_DROP_PORT_GROUP_NAME = 'neutron_pg_drop' +OVN_ROUTER_PORT_GW_MTU_OPTION = 'gateway_mtu' + +OVN_PROVNET_PORT_NAME_PREFIX = 'provnet-' + +# Agent extension constants +OVN_AGENT_DESC_KEY = 'neutron:description' +OVN_AGENT_METADATA_SB_CFG_KEY = 'neutron:ovn-metadata-sb-cfg' +OVN_AGENT_METADATA_DESC_KEY = 'neutron:description-metadata' +OVN_AGENT_METADATA_ID_KEY = 'neutron:ovn-metadata-id' +OVN_CONTROLLER_AGENT = 'OVN Controller agent' +OVN_CONTROLLER_GW_AGENT = 'OVN Controller Gateway agent' +OVN_METADATA_AGENT = 'OVN Metadata agent' + +# OVN ACLs have priorities. The highest priority ACL that matches is the one +# that takes effect. Our choice of priority numbers is arbitrary, but it +# leaves room above and below the ACLs we create. We only need two priorities. +# The first is for all the things we allow. The second is for dropping traffic +# by default. +ACL_PRIORITY_ALLOW = 1002 +ACL_PRIORITY_DROP = 1001 + +ACL_ACTION_DROP = 'drop' +ACL_ACTION_ALLOW_RELATED = 'allow-related' +ACL_ACTION_ALLOW = 'allow' + +# When a OVN L3 gateway is created, it needs to be bound to a chassis. In +# case a chassis is not found OVN_GATEWAY_INVALID_CHASSIS will be set in +# the options column of the Logical Router. This value is used to detect +# unhosted router gateways to schedule. +OVN_GATEWAY_INVALID_CHASSIS = 'neutron-ovn-invalid-chassis' + +SUPPORTED_DHCP_OPTS = { + 4: ['netmask', 'router', 'dns-server', 'log-server', + 'lpr-server', 'swap-server', 'ip-forward-enable', + 'policy-filter', 'default-ttl', 'mtu', 'router-discovery', + 'router-solicitation', 'arp-timeout', 'ethernet-encap', + 'tcp-ttl', 'tcp-keepalive', 'nis-server', 'ntp-server', + 'tftp-server'], + 6: ['server-id', 'dns-server', 'domain-search']} +DHCPV6_STATELESS_OPT = 'dhcpv6_stateless' + +# When setting global DHCP options, these options will be ignored +# as they are required for basic network functions and will be +# set by Neutron. +GLOBAL_DHCP_OPTS_BLACKLIST = { + 4: ['server_id', 'lease_time', 'mtu', 'router', 'server_mac', + 'dns_server', 'classless_static_route'], + 6: ['dhcpv6_stateless', 'dns_server', 'server_id']} + +CHASSIS_DATAPATH_NETDEV = 'netdev' +CHASSIS_IFACE_DPDKVHOSTUSER = 'dpdkvhostuser' + +OVN_IPV6_ADDRESS_MODES = { + const.IPV6_SLAAC: const.IPV6_SLAAC, + const.DHCPV6_STATEFUL: const.DHCPV6_STATEFUL.replace('-', '_'), + const.DHCPV6_STATELESS: const.DHCPV6_STATELESS.replace('-', '_') +} + +DB_MAX_RETRIES = 60 +DB_INITIAL_RETRY_INTERVAL = 0.5 +DB_MAX_RETRY_INTERVAL = 1 + +TXN_COMMITTED = 'committed' +INITIAL_REV_NUM = -1 + +ACL_EXPECTED_COLUMNS_NBDB = ( + 'external_ids', 'direction', 'log', 'priority', + 'name', 'action', 'severity', 'match') + +# Resource types +TYPE_NETWORKS = 'networks' +TYPE_PORTS = 'ports' +TYPE_SECURITY_GROUP_RULES = 'security_group_rules' +TYPE_ROUTERS = 'routers' +TYPE_ROUTER_PORTS = 'router_ports' +TYPE_SECURITY_GROUPS = 'security_groups' +TYPE_FLOATINGIPS = 'floatingips' +TYPE_SUBNETS = 'subnets' + +_TYPES_PRIORITY_ORDER = ( + TYPE_NETWORKS, + TYPE_SECURITY_GROUPS, + TYPE_SUBNETS, + TYPE_ROUTERS, + TYPE_PORTS, + TYPE_ROUTER_PORTS, + TYPE_FLOATINGIPS, + TYPE_SECURITY_GROUP_RULES) + +# The order in which the resources should be created or updated by the +# maintenance task: Root ones first and leafs at the end. +MAINTENANCE_CREATE_UPDATE_TYPE_ORDER = { + t: n for n, t in enumerate(_TYPES_PRIORITY_ORDER, 1)} + +# The order in which the resources should be deleted by the maintenance +# task: Leaf ones first and roots at the end. +MAINTENANCE_DELETE_TYPE_ORDER = { + t: n for n, t in enumerate(reversed(_TYPES_PRIORITY_ORDER), 1)} + +# The addresses field to set in the logical switch port which has a +# peer router port (connecting to the logical router). +DEFAULT_ADDR_FOR_LSP_WITH_PEER = 'router' + +# Loadbalancer constants +LRP_PREFIX = "lrp-" +LB_VIP_PORT_PREFIX = "ovn-lb-vip-" + +# Hash Ring constants +HASH_RING_NODES_TIMEOUT = 60 +HASH_RING_TOUCH_INTERVAL = 30 +HASH_RING_CACHE_TIMEOUT = 30 +HASH_RING_ML2_GROUP = 'mechanism_driver' + +# Maximum chassis count where a gateway port can be hosted +MAX_GW_CHASSIS = 5 + +UNKNOWN_ADDR = 'unknown' + +PORT_CAP_SWITCHDEV = 'switchdev' + +# TODO(lucasagomes): Create constants for other LSP types +LSP_TYPE_LOCALNET = 'localnet' +LSP_TYPE_VIRTUAL = 'virtual' +LSP_TYPE_EXTERNAL = 'external' +LSP_OPTIONS_VIRTUAL_PARENTS_KEY = 'virtual-parents' +LSP_OPTIONS_VIRTUAL_IP_KEY = 'virtual-ip' + +HA_CHASSIS_GROUP_DEFAULT_NAME = 'default_ha_chassis_group' +HA_CHASSIS_GROUP_HIGHEST_PRIORITY = 32767 diff --git a/neutron/common/ovn/exceptions.py b/neutron/common/ovn/exceptions.py new file mode 100644 index 00000000000..c5b4ae4f802 --- /dev/null +++ b/neutron/common/ovn/exceptions.py @@ -0,0 +1,38 @@ +# 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. + +from neutron_lib import exceptions as n_exc + +from neutron._i18n import _ + + +class RevisionConflict(n_exc.NeutronException): + message = _('OVN revision number for %(resource_id)s (type: ' + '%(resource_type)s) is equal or higher than the given ' + 'resource. Skipping update') + + +class UnknownResourceType(n_exc.NeutronException): + message = _('Uknown resource type: %(resource_type)s') + + +class StandardAttributeIDNotFound(n_exc.NeutronException): + message = _('Standard attribute ID not found for %(resource_uuid)s') + + +class HashRingIsEmpty(n_exc.NeutronException): + message = _('Hash Ring returned empty when hashing "%(key)s". ' + 'This should never happen in a normal situation, please ' + 'check the status of your cluster') diff --git a/neutron/common/ovn/hash_ring_manager.py b/neutron/common/ovn/hash_ring_manager.py new file mode 100644 index 00000000000..1e55e5f0daa --- /dev/null +++ b/neutron/common/ovn/hash_ring_manager.py @@ -0,0 +1,100 @@ +# 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 datetime + +from oslo_log import log +from oslo_utils import timeutils +import six +from tooz import hashring + +from neutron.common.ovn import constants +from neutron.common.ovn import exceptions +from neutron.db import ovn_hash_ring_db as db_hash_ring +from neutron_lib import context + +LOG = log.getLogger(__name__) + + +class HashRingManager(object): + + def __init__(self, group_name): + self._hash_ring = None + self._last_time_loaded = None + self._cache_startup_timeout = True + self._group = group_name + self.admin_ctx = context.get_admin_context() + + @property + def _wait_startup_before_caching(self): + # NOTE(lucasagomes): Some events are processed at the service's + # startup time and since many services may be started concurrently + # we do not want to use a cached hash ring at that point. This + # method checks if the created_at and updated_at columns from the + # nodes in the ring from this host is equal, and if so it means + # that the service just started. + + # If the startup timeout already expired, there's no reason to + # keep reading from the DB. At this point this will always + # return False + if not self._cache_startup_timeout: + return False + + nodes = db_hash_ring.get_active_nodes( + self.admin_ctx, + constants.HASH_RING_CACHE_TIMEOUT, self._group, from_host=True) + dont_cache = nodes and nodes[0].created_at == nodes[0].updated_at + if not dont_cache: + self._cache_startup_timeout = False + + return dont_cache + + def _load_hash_ring(self, refresh=False): + cache_timeout = timeutils.utcnow() - datetime.timedelta( + seconds=constants.HASH_RING_CACHE_TIMEOUT) + + # Refresh the cache if: + # - Refreshed is forced (refresh=True) + # - Service just started (_wait_startup_before_caching) + # - Hash Ring is not yet instantiated + # - Cache has timed out + if (refresh or + self._wait_startup_before_caching or + self._hash_ring is None or + not self._hash_ring.nodes or + cache_timeout >= self._last_time_loaded): + nodes = db_hash_ring.get_active_nodes( + self.admin_ctx, + constants.HASH_RING_NODES_TIMEOUT, self._group) + self._hash_ring = hashring.HashRing({node.node_uuid + for node in nodes}) + self._last_time_loaded = timeutils.utcnow() + + def refresh(self): + self._load_hash_ring(refresh=True) + + def get_node(self, key): + self._load_hash_ring() + + # tooz expects a byte string for the hash + if isinstance(key, six.string_types): + key = key.encode('utf-8') + + try: + # We need to pop the value from the set. If empty, + # KeyError is raised + return self._hash_ring[key].pop() + except KeyError: + raise exceptions.HashRingIsEmpty(key=key) diff --git a/neutron/common/ovn/utils.py b/neutron/common/ovn/utils.py new file mode 100644 index 00000000000..e3ae0e3b413 --- /dev/null +++ b/neutron/common/ovn/utils.py @@ -0,0 +1,460 @@ +# 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 inspect +import os +import re + +import netaddr +from neutron_lib.api.definitions import external_net +from neutron_lib.api.definitions import extra_dhcp_opt as edo_ext +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 import validators +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.plugins import directory +from neutron_lib.utils import net as n_utils +from oslo_utils import netutils +from oslo_utils import strutils +from ovs.db import idl +from ovsdbapp.backend.ovs_idl import connection +from ovsdbapp.backend.ovs_idl import idlutils + +from neutron._i18n import _ +from neutron.common.ovn import constants +from neutron.common.ovn import exceptions as ovn_exc + + +DNS_RESOLVER_FILE = "/etc/resolv.conf" + +AddrPairsDiff = collections.namedtuple( + 'AddrPairsDiff', ['added', 'removed', 'changed']) + + +def ovn_name(id): + # The name of the OVN entry will be neutron- + # This is due to the fact that the OVN application checks if the name + # is a UUID. If so then there will be no matches. + # We prefix the UUID to enable us to use the Neutron UUID when + # updating, deleting etc. + return 'neutron-%s' % id + + +def ovn_lrouter_port_name(id): + # The name of the OVN lrouter port entry will be lrp- + # This is to distinguish with the name of the connected lswitch patch port, + # which is named with neutron port uuid, so that OVS patch ports are + # generated properly. The pairing patch port names will be: + # - patch-lrp--to- + # - patch--to-lrp- + # lrp stands for Logical Router Port + return constants.LRP_PREFIX + '%s' % id + + +def ovn_provnet_port_name(network_id): + # The name of OVN lswitch provider network port entry will be + # provnet-. The port is created for network having + # provider:physical_network attribute. + return constants.OVN_PROVNET_PORT_NAME_PREFIX + '%s' % network_id + + +def ovn_vhu_sockpath(sock_dir, port_id): + # Frame the socket path of a virtio socket + return os.path.join( + sock_dir, + # this parameter will become the virtio port name, + # so it should not exceed IFNAMSIZ(16). + (const.VHOST_USER_DEVICE_PREFIX + port_id)[:14]) + + +def ovn_addrset_name(sg_id, ip_version): + # The name of the address set for the given security group id and ip + # version. The format is: + # as-- + # with all '-' replaced with '_'. This replacement is necessary + # because OVN doesn't support '-' in an address set name. + return ('as-%s-%s' % (ip_version, sg_id)).replace('-', '_') + + +def ovn_pg_addrset_name(sg_id, ip_version): + # The name of the address set for the given security group id modelled as a + # Port Group and ip version. The format is: + # pg-- + # with all '-' replaced with '_'. This replacement is necessary + # because OVN doesn't support '-' in an address set name. + return ('pg-%s-%s' % (sg_id, ip_version)).replace('-', '_') + + +def ovn_port_group_name(sg_id): + # The name of the port group for the given security group id. + # The format is: pg-. + return ('pg-%s' % sg_id).replace('-', '_') + + +def is_network_device_port(port): + return port.get('device_owner', '').startswith( + const.DEVICE_OWNER_PREFIXES) + + +def get_lsp_dhcp_opts(port, ip_version): + # Get dhcp options from Neutron port, for setting DHCP_Options row + # in OVN. + lsp_dhcp_disabled = False + lsp_dhcp_opts = {} + if is_network_device_port(port): + lsp_dhcp_disabled = True + else: + for edo in port.get(edo_ext.EXTRADHCPOPTS, []): + if edo['ip_version'] != ip_version: + continue + + if edo['opt_name'] == 'dhcp_disabled' and ( + edo['opt_value'] in ['True', 'true']): + # OVN native DHCP is disabled on this port + lsp_dhcp_disabled = True + # Make sure return value behavior not depends on the order and + # content of the extra DHCP options for the port + lsp_dhcp_opts.clear() + break + + if edo['opt_name'] not in ( + constants.SUPPORTED_DHCP_OPTS[ip_version]): + continue + + opt = edo['opt_name'].replace('-', '_') + lsp_dhcp_opts[opt] = edo['opt_value'] + + return (lsp_dhcp_disabled, lsp_dhcp_opts) + + +def is_lsp_trusted(port): + return n_utils.is_port_trusted(port) if port.get('device_owner') else False + + +def is_lsp_ignored(port): + # Since the floating IP port is not bound to any chassis, packets from vm + # destined to floating IP will be dropped. To overcome this, we do not + # create/update floating IP port in OVN. + return port.get('device_owner') in [const.DEVICE_OWNER_FLOATINGIP] + + +def get_lsp_security_groups(port, skip_trusted_port=True): + # In other agent link OVS, skipping trusted port is processed in security + # groups RPC. We haven't that step, so we do it here. + return [] if (skip_trusted_port and is_lsp_trusted(port) + ) else port.get('security_groups', []) + + +def is_snat_enabled(router): + return router.get(l3.EXTERNAL_GW_INFO, {}).get('enable_snat', True) + + +def is_port_security_enabled(port): + return port.get(psec.PORTSECURITY) + + +def validate_and_get_data_from_binding_profile(port): + if (constants.OVN_PORT_BINDING_PROFILE not in port or + not validators.is_attr_set( + port[constants.OVN_PORT_BINDING_PROFILE])): + return {} + param_set = {} + param_dict = {} + for param_set in constants.OVN_PORT_BINDING_PROFILE_PARAMS: + param_keys = param_set.keys() + for param_key in param_keys: + try: + param_dict[param_key] = (port[ + constants.OVN_PORT_BINDING_PROFILE][param_key]) + except KeyError: + pass + if len(param_dict) == 0: + continue + if len(param_dict) != len(param_keys): + msg = _('Invalid binding:profile. %s are all ' + 'required.') % param_keys + raise n_exc.InvalidInput(error_message=msg) + if (len(port[constants.OVN_PORT_BINDING_PROFILE]) != len( + param_keys)): + msg = _('Invalid binding:profile. too many parameters') + raise n_exc.InvalidInput(error_message=msg) + break + + if not param_dict: + return {} + + for param_key, param_type in param_set.items(): + if param_type is None: + continue + param_value = param_dict[param_key] + if not isinstance(param_value, param_type): + msg = _('Invalid binding:profile. %(key)s %(value)s ' + 'value invalid type') % {'key': param_key, + 'value': param_value} + raise n_exc.InvalidInput(error_message=msg) + + # Make sure we can successfully look up the port indicated by + # parent_name. Just let it raise the right exception if there is a + # problem. + if 'parent_name' in param_set: + plugin = directory.get_plugin() + plugin.get_port(n_context.get_admin_context(), + param_dict['parent_name']) + + if 'tag' in param_set: + tag = int(param_dict['tag']) + if tag < 0 or tag > 4095: + msg = _('Invalid binding:profile. tag "%s" must be ' + 'an integer between 0 and 4095, inclusive') % tag + raise n_exc.InvalidInput(error_message=msg) + + return param_dict + + +def is_dhcp_options_ignored(subnet): + # Don't insert DHCP_Options entry for v6 subnet with 'SLAAC' as + # 'ipv6_address_mode', since DHCPv6 shouldn't work for this mode. + return (subnet['ip_version'] == const.IP_VERSION_6 and + subnet.get('ipv6_address_mode') == const.IPV6_SLAAC) + + +def get_ovn_ipv6_address_mode(address_mode): + return constants.OVN_IPV6_ADDRESS_MODES[address_mode] + + +def get_revision_number(resource, resource_type): + """Get the resource's revision number based on its type.""" + if resource_type in (constants.TYPE_NETWORKS, + constants.TYPE_PORTS, + constants.TYPE_SECURITY_GROUP_RULES, + constants.TYPE_ROUTERS, + constants.TYPE_ROUTER_PORTS, + constants.TYPE_SECURITY_GROUPS, + constants.TYPE_FLOATINGIPS, constants.TYPE_SUBNETS): + return resource['revision_number'] + else: + raise ovn_exc.UnknownResourceType(resource_type=resource_type) + + +def remove_macs_from_lsp_addresses(addresses): + """Remove the mac addreses from the Logical_Switch_Port addresses column. + + :param addresses: The list of addresses from the Logical_Switch_Port. + Example: ["80:fa:5b:06:72:b7 158.36.44.22", + "ff:ff:ff:ff:ff:ff 10.0.0.2"] + :returns: A list of IP addesses (v4 and v6) + """ + ip_list = [] + for addr in addresses: + ip_list.extend([x for x in addr.split() if + (netutils.is_valid_ipv4(x) or + netutils.is_valid_ipv6(x))]) + return ip_list + + +def get_allowed_address_pairs_ip_addresses(port): + """Return a list of IP addresses from port's allowed_address_pairs. + + :param port: A neutron port + :returns: A list of IP addesses (v4 and v6) + """ + return [x['ip_address'] for x in port.get('allowed_address_pairs', []) + if 'ip_address' in x] + + +def get_allowed_address_pairs_ip_addresses_from_ovn_port(ovn_port): + """Return a list of IP addresses from ovn port. + + Return a list of IP addresses equivalent of Neutron's port + allowed_address_pairs column using the data in the OVN port. + + :param ovn_port: A OVN port + :returns: A list of IP addesses (v4 and v6) + """ + addresses = remove_macs_from_lsp_addresses(ovn_port.addresses) + port_security = remove_macs_from_lsp_addresses(ovn_port.port_security) + return [x for x in port_security if x not in addresses] + + +def get_ovn_port_security_groups(ovn_port, skip_trusted_port=True): + info = {'security_groups': ovn_port.external_ids.get( + constants.OVN_SG_IDS_EXT_ID_KEY, '').split(), + 'device_owner': ovn_port.external_ids.get( + constants.OVN_DEVICE_OWNER_EXT_ID_KEY, '')} + return get_lsp_security_groups(info, skip_trusted_port=skip_trusted_port) + + +def get_ovn_port_addresses(ovn_port): + addresses = remove_macs_from_lsp_addresses(ovn_port.addresses) + port_security = remove_macs_from_lsp_addresses(ovn_port.port_security) + return list(set(addresses + port_security)) + + +def sort_ips_by_version(addresses): + ip_map = {'ip4': [], 'ip6': []} + for addr in addresses: + ip_version = netaddr.IPNetwork(addr).version + ip_map['ip%d' % ip_version].append(addr) + return ip_map + + +def is_lsp_router_port(port): + return port.get('device_owner') in [const.DEVICE_OWNER_ROUTER_INTF, + const.DEVICE_OWNER_ROUTER_GW] + + +def get_lrouter_ext_gw_static_route(ovn_router): + # TODO(lucasagomes): Remove the try...except block after OVS 2.8.2 + # is tagged. + try: + return [route for route in getattr(ovn_router, 'static_routes', []) if + strutils.bool_from_string(getattr( + route, 'external_ids', {}).get( + constants.OVN_ROUTER_IS_EXT_GW, 'false'))] + except KeyError: + pass + + +def get_lrouter_snats(ovn_router): + return [n for n in getattr(ovn_router, 'nat', []) if n.type == 'snat'] + + +def get_lrouter_non_gw_routes(ovn_router): + routes = [] + # TODO(lucasagomes): Remove the try...except block after OVS 2.8.2 + # is tagged. + try: + for route in getattr(ovn_router, 'static_routes', []): + external_ids = getattr(route, 'external_ids', {}) + if strutils.bool_from_string( + external_ids.get(constants.OVN_ROUTER_IS_EXT_GW, 'false')): + continue + + routes.append({'destination': route.ip_prefix, + 'nexthop': route.nexthop}) + except KeyError: + pass + return routes + + +def is_ovn_l3(l3_plugin): + return hasattr(l3_plugin, '_ovn_client_inst') + + +def get_system_dns_resolvers(resolver_file=DNS_RESOLVER_FILE): + resolvers = [] + if not os.path.exists(resolver_file): + return resolvers + + with open(resolver_file, 'r') as rconf: + for line in rconf.readlines(): + if not line.startswith('nameserver'): + continue + + line = line.split('nameserver')[1].strip() + ipv4 = re.search(r'^(?:[0-9]{1,3}\.){3}[0-9]{1,3}', line) + if ipv4: + resolvers.append(ipv4.group(0)) + return resolvers + + +def get_port_subnet_ids(port): + fixed_ips = [ip for ip in port['fixed_ips']] + return [f['subnet_id'] for f in fixed_ips] + + +def get_ovsdb_connection(connection_string, schema, timeout, tables=None): + helper = idlutils.get_schema_helper(connection_string, schema) + if tables: + for table in tables: + helper.register_table(table) + else: + helper.register_all() + return connection.Connection(idl.Idl(connection_string, helper), timeout) + + +def get_method_class(method): + if not inspect.ismethod(method): + return + return method.__self__.__class__ + + +def ovn_metadata_name(id_): + """Return the OVN metadata name based on an id.""" + return 'metadata-%s' % id_ + + +def is_gateway_chassis_invalid(chassis_name, gw_chassis, + physnet, chassis_physnets): + """Check if gateway chassis is invalid + + @param chassis_name: gateway chassis name + @type chassis_name: string + @param gw_chassis: List of gateway chassis in the system + @type gw_chassis: [] + @param physnet: physical network associated to chassis_name + @type physnet: string + @param chassis_physnets: Dictionary linking chassis with their physnets + @type chassis_physnets: {} + @return Boolean + """ + + if chassis_name == constants.OVN_GATEWAY_INVALID_CHASSIS: + return True + elif chassis_name not in chassis_physnets: + return True + elif physnet and physnet not in chassis_physnets.get(chassis_name): + return True + elif gw_chassis and chassis_name not in gw_chassis: + return True + return False + + +def is_provider_network(network): + return external_net.EXTERNAL in network + + +def is_neutron_dhcp_agent_port(port): + """Check if the given DHCP port belongs to Neutron DHCP agents + + The DHCP ports with the device_id equals to 'reserved_dhcp_port' + or starting with the word 'dhcp' belongs to the Neutron DHCP agents. + """ + return (port['device_owner'] == const.DEVICE_OWNER_DHCP and + (port['device_id'] == const.DEVICE_ID_RESERVED_DHCP_PORT or + port['device_id'].startswith('dhcp'))) + + +def compute_address_pairs_diff(ovn_port, neutron_port): + """Compute the differences in the allowed_address_pairs field.""" + ovn_ap = get_allowed_address_pairs_ip_addresses_from_ovn_port( + ovn_port) + neutron_ap = get_allowed_address_pairs_ip_addresses(neutron_port) + added = set(neutron_ap) - set(ovn_ap) + removed = set(ovn_ap) - set(neutron_ap) + return AddrPairsDiff(added, removed, changed=any(added or removed)) + + +def is_gateway_chassis(chassis): + """Check if the given chassis is a gateway chassis""" + external_ids = getattr(chassis, 'external_ids', {}) + return ('enable-chassis-as-gw' in external_ids.get( + 'ovn-cms-options', '').split(',')) + + +def get_port_capabilities(port): + """Return a list of port's capabilities""" + return port.get(portbindings.PROFILE, {}).get('capabilities', []) diff --git a/neutron/conf/plugins/ml2/drivers/ovn/__init__.py b/neutron/conf/plugins/ml2/drivers/ovn/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py b/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py new file mode 100644 index 00000000000..fe3f7957157 --- /dev/null +++ b/neutron/conf/plugins/ml2/drivers/ovn/ovn_conf.py @@ -0,0 +1,293 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from neutron_lib.api.definitions import portbindings +from oslo_config import cfg +from oslo_log import log as logging +from ovsdbapp.backend.ovs_idl import vlog + +from neutron._i18n import _ + +LOG = logging.getLogger(__name__) + +EXTRA_LOG_LEVEL_DEFAULTS = [ +] + +VLOG_LEVELS = {'CRITICAL': vlog.CRITICAL, 'ERROR': vlog.ERROR, 'WARNING': + vlog.WARN, 'INFO': vlog.INFO, 'DEBUG': vlog.DEBUG} + +ovn_opts = [ + cfg.StrOpt('ovn_nb_connection', + default='tcp:127.0.0.1:6641', + help=_('The connection string for the OVN_Northbound OVSDB.\n' + 'Use tcp:IP:PORT for TCP connection.\n' + 'Use ssl:IP:PORT for SSL connection. The ' + 'ovn_nb_private_key, ovn_nb_certificate and ' + 'ovn_nb_ca_cert are mandatory.\n' + 'Use unix:FILE for unix domain socket connection.')), + cfg.StrOpt('ovn_nb_private_key', + default='', + help=_('The PEM file with private key for SSL connection to ' + 'OVN-NB-DB')), + cfg.StrOpt('ovn_nb_certificate', + default='', + help=_('The PEM file with certificate that certifies the ' + 'private key specified in ovn_nb_private_key')), + cfg.StrOpt('ovn_nb_ca_cert', + default='', + help=_('The PEM file with CA certificate that OVN should use to' + ' verify certificates presented to it by SSL peers')), + cfg.StrOpt('ovn_sb_connection', + default='tcp:127.0.0.1:6642', + help=_('The connection string for the OVN_Southbound OVSDB.\n' + 'Use tcp:IP:PORT for TCP connection.\n' + 'Use ssl:IP:PORT for SSL connection. The ' + 'ovn_sb_private_key, ovn_sb_certificate and ' + 'ovn_sb_ca_cert are mandatory.\n' + 'Use unix:FILE for unix domain socket connection.')), + cfg.StrOpt('ovn_sb_private_key', + default='', + help=_('The PEM file with private key for SSL connection to ' + 'OVN-SB-DB')), + cfg.StrOpt('ovn_sb_certificate', + default='', + help=_('The PEM file with certificate that certifies the ' + 'private key specified in ovn_sb_private_key')), + cfg.StrOpt('ovn_sb_ca_cert', + default='', + help=_('The PEM file with CA certificate that OVN should use to' + ' verify certificates presented to it by SSL peers')), + cfg.IntOpt('ovsdb_connection_timeout', + default=180, + help=_('Timeout in seconds for the OVSDB ' + 'connection transaction')), + cfg.IntOpt('ovsdb_retry_max_interval', + default=180, + help=_('Max interval in seconds between ' + 'each retry to get the OVN NB and SB IDLs')), + cfg.IntOpt('ovsdb_probe_interval', + min=0, + default=60000, + help=_('The probe interval in for the OVSDB session in ' + 'milliseconds. If this is zero, it disables the ' + 'connection keepalive feature. If non-zero the value ' + 'will be forced to at least 1000 milliseconds. Defaults ' + 'to 60 seconds.')), + cfg.StrOpt('neutron_sync_mode', + default='log', + choices=('off', 'log', 'repair'), + help=_('The synchronization mode of OVN_Northbound OVSDB ' + 'with Neutron DB.\n' + 'off - synchronization is off \n' + 'log - during neutron-server startup, ' + 'check to see if OVN is in sync with ' + 'the Neutron database. ' + ' Log warnings for any inconsistencies found so' + ' that an admin can investigate \n' + 'repair - during neutron-server startup, automatically' + ' create resources found in Neutron but not in OVN.' + ' Also remove resources from OVN' + ' that are no longer in Neutron.')), + cfg.BoolOpt('ovn_l3_mode', + default=True, + deprecated_for_removal=True, + deprecated_reason="This option is no longer used. Native L3 " + "support in OVN is always used.", + help=_('Whether to use OVN native L3 support. Do not change ' + 'the value for existing deployments that contain ' + 'routers.')), + cfg.StrOpt("ovn_l3_scheduler", + default='leastloaded', + choices=('leastloaded', 'chance'), + help=_('The OVN L3 Scheduler type used to schedule router ' + 'gateway ports on hypervisors/chassis. \n' + 'leastloaded - chassis with fewest gateway ports ' + 'selected \n' + 'chance - chassis randomly selected')), + cfg.BoolOpt('enable_distributed_floating_ip', + default=False, + help=_('Enable distributed floating IP support.\n' + 'If True, the NAT action for floating IPs will be done ' + 'locally and not in the centralized gateway. This ' + 'saves the path to the external network. This requires ' + 'the user to configure the physical network map ' + '(i.e. ovn-bridge-mappings) on each compute node.')), + cfg.StrOpt("vif_type", + deprecated_for_removal=True, + deprecated_reason="The port VIF type is now determined based " + "on the OVN chassis information when the " + "port is bound to a host.", + default=portbindings.VIF_TYPE_OVS, + help=_("Type of VIF to be used for ports valid values are " + "(%(ovs)s, %(dpdk)s) default %(ovs)s") % { + "ovs": portbindings.VIF_TYPE_OVS, + "dpdk": portbindings.VIF_TYPE_VHOST_USER}, + choices=[portbindings.VIF_TYPE_OVS, + portbindings.VIF_TYPE_VHOST_USER]), + cfg.StrOpt("vhost_sock_dir", + default="/var/run/openvswitch", + help=_("The directory in which vhost virtio socket " + "is created by all the vswitch daemons")), + cfg.IntOpt('dhcp_default_lease_time', + default=(12 * 60 * 60), + help=_('Default least time (in seconds) to use with ' + 'OVN\'s native DHCP service.')), + cfg.StrOpt("ovsdb_log_level", + default="INFO", + choices=list(VLOG_LEVELS.keys()), + help=_("The log level used for OVSDB")), + cfg.BoolOpt('ovn_metadata_enabled', + default=False, + help=_('Whether to use metadata service.')), + cfg.ListOpt('dns_servers', + default=[], + help=_("Comma-separated list of the DNS servers which will be " + "used as forwarders if a subnet's dns_nameservers " + "field is empty. If both subnet's dns_nameservers and " + "this option is empty, then the DNS resolvers on the " + "host running the neutron server will be used.")), + cfg.DictOpt('ovn_dhcp4_global_options', + default={}, + help=_("Dictionary of global DHCPv4 options which will be " + "automatically set on each subnet upon creation and " + "on all existing subnets when Neutron starts.\n" + "An empty value for a DHCP option will cause that " + "option to be unset globally.\n" + "EXAMPLES:\n" + "- ntp_server:1.2.3.4,wpad:1.2.3.5 - Set ntp_server " + "and wpad\n" + "- ntp_server:,wpad:1.2.3.5 - Unset ntp_server and " + "set wpad\n" + "See the ovn-nb(5) man page for available options.")), + cfg.DictOpt('ovn_dhcp6_global_options', + default={}, + help=_("Dictionary of global DHCPv6 options which will be " + "automatically set on each subnet upon creation and " + "on all existing subnets when Neutron starts.\n" + "An empty value for a DHCP option will cause that " + "option to be unset globally.\n" + "EXAMPLES:\n" + "- ntp_server:1.2.3.4,wpad:1.2.3.5 - Set ntp_server " + "and wpad\n" + "- ntp_server:,wpad:1.2.3.5 - Unset ntp_server and " + "set wpad\n" + "See the ovn-nb(5) man page for available options.")), + cfg.BoolOpt('ovn_emit_need_to_frag', + default=False, + help=_('Configure OVN to emit "need to frag" packets in ' + 'case of MTU mismatch.\n' + 'Before enabling this configuration make sure that ' + 'its supported by the host kernel (version >= 5.2) ' + 'or by checking the output of the following command: \n' + 'ovs-appctl -t ovs-vswitchd dpif/show-dp-features ' + 'br-int | grep "Check pkt length action".')), +] + +cfg.CONF.register_opts(ovn_opts, group='ovn') + + +def list_opts(): + return [ + ('ovn', ovn_opts), + ] + + +def get_ovn_nb_connection(): + return cfg.CONF.ovn.ovn_nb_connection + + +def get_ovn_nb_private_key(): + return cfg.CONF.ovn.ovn_nb_private_key + + +def get_ovn_nb_certificate(): + return cfg.CONF.ovn.ovn_nb_certificate + + +def get_ovn_nb_ca_cert(): + return cfg.CONF.ovn.ovn_nb_ca_cert + + +def get_ovn_sb_connection(): + return cfg.CONF.ovn.ovn_sb_connection + + +def get_ovn_sb_private_key(): + return cfg.CONF.ovn.ovn_sb_private_key + + +def get_ovn_sb_certificate(): + return cfg.CONF.ovn.ovn_sb_certificate + + +def get_ovn_sb_ca_cert(): + return cfg.CONF.ovn.ovn_sb_ca_cert + + +def get_ovn_ovsdb_timeout(): + return cfg.CONF.ovn.ovsdb_connection_timeout + + +def get_ovn_ovsdb_retry_max_interval(): + return cfg.CONF.ovn.ovsdb_retry_max_interval + + +def get_ovn_ovsdb_probe_interval(): + return cfg.CONF.ovn.ovsdb_probe_interval + + +def get_ovn_neutron_sync_mode(): + return cfg.CONF.ovn.neutron_sync_mode + + +def is_ovn_l3(): + return cfg.CONF.ovn.ovn_l3_mode + + +def get_ovn_l3_scheduler(): + return cfg.CONF.ovn.ovn_l3_scheduler + + +def is_ovn_distributed_floating_ip(): + return cfg.CONF.ovn.enable_distributed_floating_ip + + +def get_ovn_vhost_sock_dir(): + return cfg.CONF.ovn.vhost_sock_dir + + +def get_ovn_dhcp_default_lease_time(): + return cfg.CONF.ovn.dhcp_default_lease_time + + +def get_ovn_ovsdb_log_level(): + return VLOG_LEVELS[cfg.CONF.ovn.ovsdb_log_level] + + +def is_ovn_metadata_enabled(): + return cfg.CONF.ovn.ovn_metadata_enabled + + +def get_dns_servers(): + return cfg.CONF.ovn.dns_servers + + +def get_global_dhcpv4_opts(): + return cfg.CONF.ovn.ovn_dhcp4_global_options + + +def get_global_dhcpv6_opts(): + return cfg.CONF.ovn.ovn_dhcp6_global_options + + +def is_ovn_emit_need_to_frag_enabled(): + return cfg.CONF.ovn.ovn_emit_need_to_frag diff --git a/neutron/tests/unit/common/ovn/__init__.py b/neutron/tests/unit/common/ovn/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/common/ovn/test_hash_ring_manager.py b/neutron/tests/unit/common/ovn/test_hash_ring_manager.py new file mode 100644 index 00000000000..54260549c63 --- /dev/null +++ b/neutron/tests/unit/common/ovn/test_hash_ring_manager.py @@ -0,0 +1,134 @@ +# 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 datetime + +import mock +from neutron_lib import context +from oslo_utils import timeutils + +from neutron.common.ovn import constants +from neutron.common.ovn import exceptions +from neutron.common.ovn import hash_ring_manager +from neutron.db import ovn_hash_ring_db as db_hash_ring +from neutron.tests.unit import testlib_api + +HASH_RING_TEST_GROUP = 'test_group' + + +class TestHashRingManager(testlib_api.SqlTestCaseLight): + + def setUp(self): + super(TestHashRingManager, self).setUp() + self.hash_ring_manager = hash_ring_manager.HashRingManager( + HASH_RING_TEST_GROUP) + self.admin_ctx = context.get_admin_context() + + def _verify_hashes(self, hash_dict): + for target_node, uuid_ in hash_dict.items(): + self.assertEqual(target_node, + self.hash_ring_manager.get_node(uuid_)) + + def test_get_node(self): + # Use pre-defined UUIDs to make the hashes predictable + node_1_uuid = db_hash_ring.add_node( + self.admin_ctx, HASH_RING_TEST_GROUP, 'node-1') + node_2_uuid = db_hash_ring.add_node( + self.admin_ctx, HASH_RING_TEST_GROUP, 'node-2') + + hash_dict_before = {node_1_uuid: 'fake-uuid', + node_2_uuid: 'fake-uuid-0'} + self._verify_hashes(hash_dict_before) + + def test_get_node_no_active_nodes(self): + self.assertRaises( + exceptions.HashRingIsEmpty, self.hash_ring_manager.get_node, + 'fake-uuid') + + def test_ring_rebalance(self): + # Use pre-defined UUIDs to make the hashes predictable + node_1_uuid = db_hash_ring.add_node( + self.admin_ctx, HASH_RING_TEST_GROUP, 'node-1') + node_2_uuid = db_hash_ring.add_node( + self.admin_ctx, HASH_RING_TEST_GROUP, 'node-2') + + # Add another node from a different host + with mock.patch.object(db_hash_ring, 'CONF') as mock_conf: + mock_conf.host = 'another-host-52359446-c366' + another_host_node = db_hash_ring.add_node( + self.admin_ctx, HASH_RING_TEST_GROUP, 'another-host') + + # Assert all nodes are alive in the ring + self.hash_ring_manager.refresh() + self.assertEqual(3, len(self.hash_ring_manager._hash_ring.nodes)) + + # Hash certain values against the nodes + hash_dict_before = {node_1_uuid: 'fake-uuid', + node_2_uuid: 'fake-uuid-0', + another_host_node: 'fake-uuid-ABCDE'} + self._verify_hashes(hash_dict_before) + + # Mock utcnow() as the HASH_RING_NODES_TIMEOUT have expired + # already and touch the nodes from our host + fake_utcnow = timeutils.utcnow() - datetime.timedelta( + seconds=constants.HASH_RING_NODES_TIMEOUT) + with mock.patch.object(timeutils, 'utcnow') as mock_utcnow: + mock_utcnow.return_value = fake_utcnow + db_hash_ring.touch_nodes_from_host( + self.admin_ctx, HASH_RING_TEST_GROUP) + + # Now assert that the ring was re-balanced and only the node from + # another host is marked as alive + self.hash_ring_manager.refresh() + self.assertEqual([another_host_node], + list(self.hash_ring_manager._hash_ring.nodes.keys())) + + # Now only "another_host_node" is alive, all values should hash to it + hash_dict_after_rebalance = {another_host_node: 'fake-uuid', + another_host_node: 'fake-uuid-0', + another_host_node: 'fake-uuid-ABCDE'} + self._verify_hashes(hash_dict_after_rebalance) + + # Now touch the nodes so they appear active again + db_hash_ring.touch_nodes_from_host( + self.admin_ctx, HASH_RING_TEST_GROUP) + self.hash_ring_manager.refresh() + + # The ring should re-balance and as it was before + self._verify_hashes(hash_dict_before) + + def test__wait_startup_before_caching(self): + db_hash_ring.add_node(self.admin_ctx, HASH_RING_TEST_GROUP, 'node-1') + db_hash_ring.add_node(self.admin_ctx, HASH_RING_TEST_GROUP, 'node-2') + + # Assert it will return True until created_at != updated_at + self.assertTrue(self.hash_ring_manager._wait_startup_before_caching) + self.assertTrue(self.hash_ring_manager._cache_startup_timeout) + + # Touch the nodes (== update the updated_at column) + db_hash_ring.touch_nodes_from_host( + self.admin_ctx, HASH_RING_TEST_GROUP) + + # Assert it's now False. Waiting is not needed anymore + self.assertFalse(self.hash_ring_manager._wait_startup_before_caching) + self.assertFalse(self.hash_ring_manager._cache_startup_timeout) + + # Now assert that since the _cache_startup_timeout has been + # flipped, we no longer will read from the database + with mock.patch.object(hash_ring_manager.db_hash_ring, + 'get_active_nodes') as get_nodes_mock: + self.assertFalse( + self.hash_ring_manager._wait_startup_before_caching) + self.assertFalse(get_nodes_mock.called) diff --git a/neutron/tests/unit/common/ovn/test_utils.py b/neutron/tests/unit/common/ovn/test_utils.py new file mode 100644 index 00000000000..ea5e3ad9a74 --- /dev/null +++ b/neutron/tests/unit/common/ovn/test_utils.py @@ -0,0 +1,105 @@ +# Copyright 2018 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 fixtures + +from neutron.common.ovn import constants +from neutron.common.ovn import utils +from neutron.tests import base + +RESOLV_CONF_TEMPLATE = """# TEST TEST TEST +# Geneated by OVN test +nameserver 10.0.0.1 +#nameserver 10.0.0.2 +nameserver 10.0.0.3 +nameserver foo 10.0.0.4 +nameserver aef0::4 +foo 10.0.0.5 +""" + + +class TestUtils(base.BaseTestCase): + + def test_get_system_dns_resolvers(self): + tempdir = self.useFixture(fixtures.TempDir()).path + resolver_file_name = tempdir + '/resolv.conf' + tmp_resolv_file = open(resolver_file_name, 'w') + tmp_resolv_file.writelines(RESOLV_CONF_TEMPLATE) + tmp_resolv_file.close() + expected_dns_resolvers = ['10.0.0.1', '10.0.0.3'] + observed_dns_resolvers = utils.get_system_dns_resolvers( + resolver_file=resolver_file_name) + self.assertEqual(expected_dns_resolvers, observed_dns_resolvers) + + +class TestGateWayChassisValidity(base.BaseTestCase): + + def setUp(self): + super(TestGateWayChassisValidity, self).setUp() + self.gw_chassis = ['host1', 'host2'] + self.chassis_name = self.gw_chassis[0] + self.physnet = 'physical-nw-1' + self.chassis_physnets = {self.chassis_name: [self.physnet]} + + def test_gateway_chassis_valid(self): + # Return False, since everything is valid + self.assertFalse(utils.is_gateway_chassis_invalid( + self.chassis_name, self.gw_chassis, self.physnet, + self.chassis_physnets)) + + def test_gateway_chassis_due_to_invalid_chassis_name(self): + # Return True since chassis is invalid + self.chassis_name = constants.OVN_GATEWAY_INVALID_CHASSIS + self.assertTrue(utils.is_gateway_chassis_invalid( + self.chassis_name, self.gw_chassis, self.physnet, + self.chassis_physnets)) + + def test_gateway_chassis_for_chassis_not_in_chassis_physnets(self): + # Return True since chassis is not in chassis_physnets + self.chassis_name = 'host-2' + self.assertTrue(utils.is_gateway_chassis_invalid( + self.chassis_name, self.gw_chassis, self.physnet, + self.chassis_physnets)) + + def test_gateway_chassis_for_undefined_physnet(self): + # Return True since physnet is not defined + self.chassis_name = 'host-1' + self.physnet = None + self.assertTrue(utils.is_gateway_chassis_invalid( + self.chassis_name, self.gw_chassis, self.physnet, + self.chassis_physnets)) + + def test_gateway_chassis_for_physnet_not_in_chassis_physnets(self): + # Return True since physnet is not in chassis_physnets + self.physnet = 'physical-nw-2' + self.assertTrue(utils.is_gateway_chassis_invalid( + self.chassis_name, self.gw_chassis, self.physnet, + self.chassis_physnets)) + + def test_gateway_chassis_for_gw_chassis_empty(self): + # Return False if gw_chassis is [] + # This condition states that the chassis is valid, has valid + # physnets and there are no gw_chassis present in the system. + self.gw_chassis = [] + self.assertFalse(utils.is_gateway_chassis_invalid( + self.chassis_name, self.gw_chassis, self.physnet, + self.chassis_physnets)) + + def test_gateway_chassis_for_chassis_not_in_gw_chassis_list(self): + # Return True since chassis_name not in gw_chassis + self.gw_chassis = ['host-2'] + self.assertTrue(utils.is_gateway_chassis_invalid( + self.chassis_name, self.gw_chassis, self.physnet, + self.chassis_physnets)) diff --git a/setup.cfg b/setup.cfg index 2dce148b107..113e9a63c3b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -143,6 +143,7 @@ oslo.config.opts = neutron.ml2 = neutron.opts:list_ml2_conf_opts neutron.ml2.linuxbridge.agent = neutron.opts:list_linux_bridge_opts neutron.ml2.macvtap.agent = neutron.opts:list_macvtap_opts + neutron.ml2.ovn = neutron.conf.plugins.ml2.drivers.ovn.ovn_conf:list_opts neutron.ml2.ovs.agent = neutron.opts:list_ovs_opts neutron.ml2.sriov.agent = neutron.opts:list_sriov_agent_opts neutron.ml2.xenapi = neutron.opts:list_xenapi_opts