From 4b5cf9e5fbdba90f4218fff9ba558077cfbddb96 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Fri, 29 Nov 2019 11:11:13 +0000 Subject: [PATCH] [OVN] Import ovsdb related code This patch imports ovsdb related code from networking_ovn. Previous paths in networking-ovn tree: ./networking_ovn/ovsdb/commands.py -> ./neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py ./networking_ovn/ovsdb/worker.py -> ./neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/worker.py ./networking_ovn/ovsdb/ovn_api.py -> ./neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py ACL commands will be implemented when the related code is merged and could be tested properly. Related-Blueprint: neutron-ovn-merge Co-Authored-By: Reedip Co-Authored-By: Numan Siddique Co-Authored-By: Flavio Fernandes Co-Authored-By: Terry Wilson Co-Authored-By: Daniel Alvarez Co-Authored-By: Changxun Zhou Co-Authored-By: Gal Sagie Co-Authored-By: Amitabha Biswas Co-Authored-By: Richard Theis Co-Authored-By: lzklibj Co-Authored-By: zhufl Co-Authored-By: Na Co-Authored-By: Chandra S Vejendla Co-Authored-By: Gary Kotton Co-Authored-By: Aaron Rosen Co-Authored-By: Rodolfo Alonso Hernandez Change-Id: I9fe64f954d227efaab5e96c6150df44f36a2530a --- neutron/plugins/ml2/drivers/ovn/__init__.py | 0 .../ml2/drivers/ovn/mech_driver/__init__.py | 0 .../drivers/ovn/mech_driver/ovsdb/__init__.py | 0 .../ml2/drivers/ovn/mech_driver/ovsdb/api.py | 702 ++++++++ .../drivers/ovn/mech_driver/ovsdb/commands.py | 973 +++++++++++ .../drivers/ovn/mech_driver/ovsdb/worker.py | 38 + neutron/tests/unit/fake_resources.py | 752 +++++++++ .../unit/plugins/ml2/drivers/ovn/__init__.py | 0 .../ml2/drivers/ovn/mech_driver/__init__.py | 0 .../drivers/ovn/mech_driver/ovsdb/__init__.py | 0 .../ovn/mech_driver/ovsdb/test_commands.py | 1428 +++++++++++++++++ 11 files changed, 3893 insertions(+) create mode 100644 neutron/plugins/ml2/drivers/ovn/__init__.py create mode 100644 neutron/plugins/ml2/drivers/ovn/mech_driver/__init__.py create mode 100644 neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/__init__.py create mode 100644 neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py create mode 100644 neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py create mode 100644 neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/worker.py create mode 100644 neutron/tests/unit/fake_resources.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/ovn/__init__.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/__init__.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/__init__.py create mode 100644 neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_commands.py diff --git a/neutron/plugins/ml2/drivers/ovn/__init__.py b/neutron/plugins/ml2/drivers/ovn/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/__init__.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/__init__.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py new file mode 100644 index 00000000000..b33143e5521 --- /dev/null +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/api.py @@ -0,0 +1,702 @@ +# Copyright 2019 Red Hat, Inc. +# +# 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 abc + +from ovsdbapp import api +import six + +from neutron.common.ovn import constants as ovn_const + + +@six.add_metaclass(abc.ABCMeta) +class API(api.API): + + @abc.abstractmethod + def set_lswitch_ext_ids(self, name, ext_ids, if_exists=True): + """Create a command to set OVN lswitch external ids + + :param name: The name of the lswitch + :type name: string + :param ext_ids The external ids to set for the lswitch + :type ext_ids: dictionary + :param if_exists: Do not fail if lswitch does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def create_lswitch_port(self, lport_name, lswitch_name, may_exist=True, + **columns): + """Create a command to add an OVN logical switch port + + :param lport_name: The name of the lport + :type lport_name: string + :param lswitch_name: The name of the lswitch the lport is created on + :type lswitch_name: string + :param may_exist: Do not fail if lport already exists + :type may_exist: bool + :param columns: Dictionary of port columns + Supported columns: macs, external_ids, + parent_name, tag, enabled + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def set_lswitch_port(self, lport_name, if_exists=True, **columns): + """Create a command to set OVN logical switch port fields + + :param lport_name: The name of the lport + :type lport_name: string + :param columns: Dictionary of port columns + Supported columns: macs, external_ids, + parent_name, tag, enabled + :param if_exists: Do not fail if lport does not exist + :type if_exists: bool + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def delete_lswitch_port(self, lport_name=None, lswitch_name=None, + ext_id=None, if_exists=True): + """Create a command to delete an OVN logical switch port + + :param lport_name: The name of the lport + :type lport_name: string + :param lswitch_name: The name of the lswitch + :type lswitch_name: string + :param ext_id: The external id of the lport + :type ext_id: pair of + :param if_exists: Do not fail if the lport does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def create_lrouter(self, name, may_exist=True, **columns): + """Create a command to add an OVN lrouter + + :param name: The id of the lrouter + :type name: string + :param may_exist: Do not fail if lrouter already exists + :type may_exist: bool + :param columns: Dictionary of lrouter columns + Supported columns: external_ids, default_gw, ip + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def update_lrouter(self, name, if_exists=True, **columns): + """Update a command to add an OVN lrouter + + :param name: The id of the lrouter + :type name: string + :param if_exists: Do not fail if the lrouter does not exist + :type if_exists: bool + :param columns: Dictionary of lrouter columns + Supported columns: external_ids, default_gw, ip + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def delete_lrouter(self, name, if_exists=True): + """Create a command to delete an OVN lrouter + + :param name: The id of the lrouter + :type name: string + :param if_exists: Do not fail if the lrouter does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def add_lrouter_port(self, name, lrouter, may_exist=True, + **columns): + """Create a command to add an OVN lrouter port + + :param name: The unique name of the lrouter port + :type name: string + :param lrouter: The unique name of the lrouter + :type lrouter: string + :param lswitch: The unique name of the lswitch + :type lswitch: string + :param may_exist: If true, do not fail if lrouter port set + already exists. + :type may_exist: bool + :param columns: Dictionary of lrouter columns + Supported columns: external_ids, mac, network + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def update_lrouter_port(self, name, if_exists=True, **columns): + """Update a command to add an OVN lrouter port + + :param name: The unique name of the lrouter port + :type name: string + :param if_exists: Do not fail if the lrouter port does not exist + :type if_exists: bool + :param columns: Dictionary of lrouter columns + Supported columns: networks + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def delete_lrouter_port(self, name, lrouter, if_exists=True): + """Create a command to delete an OVN lrouter port + + :param name: The unique name of the lport + :type name: string + :param lrouter: The unique name of the lrouter + :type lrouter: string + :param if_exists: Do not fail if the lrouter port does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def set_lrouter_port_in_lswitch_port( + self, lswitch_port, lrouter_port, is_gw_port=False, if_exists=True, + lsp_address=ovn_const.DEFAULT_ADDR_FOR_LSP_WITH_PEER): + """Create a command to set lswitch_port as lrouter_port + + :param lswitch_port: The name of logical switch port + :type lswitch_port: string + :param lrouter_port: The name of logical router port + :type lrouter_port: string + :param is_gw_port: True if logical router port is gw port + :type is_gw_port: bool + :param if_exists: Do not fail if the lswitch port does not exist + :type if_exists: bool + :param lsp_address: logical switch port's addresses to set + :type lsp_address: string or list of strings + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def add_acl(self, lswitch, lport, **columns): + """Create an ACL for a logical port. + + :param lswitch: The logical switch the port is attached to. + :type lswitch: string + :param lport: The logical port this ACL is associated with. + :type lport: string + :param columns: Dictionary of ACL columns + Supported columns: see ACL table in OVN_Northbound + :type columns: dictionary + """ + + @abc.abstractmethod + def delete_acl(self, lswitch, lport, if_exists=True): + """Delete all ACLs for a logical port. + + :param lswitch: The logical switch the port is attached to. + :type lswitch: string + :param lport: The logical port this ACL is associated with. + :type lport: string + :param if_exists: Do not fail if the ACL for this lport does not + exist + :type if_exists: bool + """ + + @abc.abstractmethod + def update_acls(self, lswitch_names, port_list, acl_new_values_dict, + need_compare=True, is_add_acl=True): + """Update the list of acls on logical switches with new values. + + :param lswitch_names: List of logical switch names + :type lswitch_name: [] + :param port_list: Iterator of list of ports + :type port_list: [] + :param acl_new_values_dict: Dictionary of acls indexed by port id + :type acl_new_values_dict: {} + :param need_compare: If acl_new_values_dict need compare + with existing acls + :type need_compare: bool + :is_add_acl: If updating is caused by adding acl + :type is_add_acl: bool + """ + + @abc.abstractmethod + def get_acl_by_id(self, acl_id): + """Get an ACL by its ID. + + :param acl_id: ID of the ACL to lookup + :type acl_id: string + :returns The ACL row or None: + """ + + @abc.abstractmethod + def add_static_route(self, lrouter, **columns): + """Add static route to logical router. + + :param lrouter: The unique name of the lrouter + :type lrouter: string + :param columns: Dictionary of static columns + Supported columns: prefix, nexthop, valid + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def delete_static_route(self, lrouter, ip_prefix, nexthop, if_exists=True): + """Delete static route from logical router. + + :param lrouter: The unique name of the lrouter + :type lrouter: string + :param ip_prefix: The prefix of the static route + :type ip_prefix: string + :param nexthop: The nexthop of the static route + :type nexthop: string + :param if_exists: Do not fail if router does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def create_address_set(self, name, may_exist=True, **columns): + """Create an address set + + :param name: The name of the address set + :type name: string + :param may_exist: Do not fail if address set already exists + :type may_exist: bool + :param columns: Dictionary of address set columns + Supported columns: external_ids, addresses + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def delete_address_set(self, name, if_exists=True): + """Delete an address set + + :param name: The name of the address set + :type name: string + :param if_exists: Do not fail if the address set does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def update_address_set(self, name, addrs_add, addrs_remove, + if_exists=True): + """Updates addresses in an address set + + :param name: The name of the address set + :type name: string + :param addrs_add: The addresses to be added + :type addrs_add: [] + :param addrs_remove: The addresses to be removed + :type addrs_remove: [] + :param if_exists: Do not fail if the address set does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def update_address_set_ext_ids(self, name, external_ids, if_exists=True): + """Update external IDs for an address set + + :param name: The name of the address set + :type name: string + :param external_ids: The external IDs for the address set + :type external_ids: dict + :param if_exists: Do not fail if the address set does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def get_all_chassis_gateway_bindings(self, + chassis_candidate_list=None): + """Return a dictionary of chassis name:list of gateways + + :param chassis_candidate_list: List of possible chassis candidates + :type chassis_candidate_list: [] + :returns: {} of chassis to routers mapping + """ + + @abc.abstractmethod + def get_gateway_chassis_binding(self, gateway_id): + """Return the list of chassis to which the gateway is bound to + + As one gateway can be hosted by multiple chassis, this method is + returning a list of those chassis ordered by priority. This means + that the first element of the list is the chassis hosting the + gateway with the highest priority (which will likely be where + the router port is going to be active). + + :param gateway_id: The gateway id + :type gateway_id: string + :returns: a list of strings with the chassis names + """ + + @abc.abstractmethod + def get_unhosted_gateways(self, port_physnet_dict, chassis_physnets, + gw_chassis): + """Return a list of gateways not hosted on chassis + + :param port_physnet_dict: Dictionary of gateway ports and their physnet + :param chassis_physnets: Dictionary of chassis and physnets + :param gw_chassis: List of gateway chassis provided by admin + through ovn-cms-options + :returns: List of gateways not hosted on a valid + chassis + """ + + @abc.abstractmethod + def add_dhcp_options(self, subnet_id, port_id=None, may_exist=True, + **columns): + """Adds the DHCP options specified in the @columns in DHCP_Options + + If the DHCP options already exist in the DHCP_Options table for + the @subnet_id (and @lsp_name), updates the row, else creates a new + row. + + :param subnet_id: The subnet id to which the DHCP options belong + to + :type subnet_id: string + :param port_id: The port id to which the DHCP options belong to + if specified + :type port_id: string + :param may_exist: If true, checks if the DHCP options for + subnet_id exists or not. If it already exists, + it updates the row with the columns specified. + Else creates a new row. + :type may_exist: bool + :type columns: Dictionary of DHCP_Options columns + Supported columns: see DHCP_Options table in + OVN_Northbound + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def delete_dhcp_options(self, row_uuid, if_exists=True): + """Deletes the row in DHCP_Options with the @row_uuid + + :param row_uuid: The UUID of the row to be deleted. + :type row_uuid: string + :param if_exists: Do not fail if the DHCP_Options row does not + exist + :type if_exists: bool + """ + + @abc.abstractmethod + def get_subnet_dhcp_options(self, subnet_id, with_ports=False): + """Returns the Subnet DHCP options as a dictionary + + :param subnet_id: The subnet id whose DHCP options are returned + :type subnet_id: string + :param with_ports: If True, also returns the ports DHCP options. + :type with_ports: bool + :returns: Returns a dictionary containing two keys: + subnet and ports. + """ + + @abc.abstractmethod + def get_subnets_dhcp_options(self, subnet_ids): + """Returns the Subnets DHCP options as list of dictionary + + :param subnet_ids: The subnet ids whose DHCP options are returned + :type subnet_ids: list of string + :returns: Returns the columns of the DHCP_Options as list + of dictionary. Empty list is returned if no + DHCP_Options matched found. + """ + + @abc.abstractmethod + def get_address_sets(self): + """Gets all address sets in the OVN_Northbound DB + + :returns: dictionary indexed by name, DB columns as values + """ + + @abc.abstractmethod + def get_port_groups(self): + """Gets all port groups in the OVN_Northbound DB + + :returns: dictionary indexed by name, DB columns as values + """ + + @abc.abstractmethod + def get_router_port_options(self, lsp_name): + """Get options set for lsp of type router + + :returns: router port options + """ + + @abc.abstractmethod + def add_nat_rule_in_lrouter(self, lrouter, **columns): + """Add NAT rule in logical router + + + :param lrouter: The unique name of the lrouter + :type lrouter: string + :param columns: Dictionary of nat columns + Supported columns: type, logical_ip, external_ip + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def delete_nat_rule_in_lrouter(self, lrouter, type, logical_ip, + external_ip, if_exists=True): + """Delete NAT rule in logical router + + :param lrouter: The unique name of the lrouter + :type lrouter: string + :param type: Type of nat. Supported values are 'snat', 'dnat' + and 'dnat_and_snat' + :type type: string + :param logical_ip: IP or network that needs to be natted + :type logical_ip: string + :param external_ip: External IP to be used for nat + :type external_ip: string + :param if_exists: Do not fail if the Logical_Router row does not + exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def get_lrouter_nat_rules(self, lrouter): + """Returns the nat rules of a router + + :param lrouter: The unique name of the router + :type lrouter: string + :returns: A list of nat rules of the router, with each item + as a dict with the keys - 'external_ip', 'logical_ip' + 'type' and 'uuid' of the row. + """ + + @abc.abstractmethod + def set_nat_rule_in_lrouter(self, lrouter, nat_rule_uuid, **columns): + """Sets the NAT rule fields + + :param lrouter: The unique name of the router to which this the + NAT rule belongs to. + :type lrouter: string + :param nat_rule_uuid: The uuid of the NAT rule row to be updated. + :type nat_rule_uuid: string + :type columns: dictionary + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def get_lswitch(self, lswitch_name): + """Returns the logical switch + + :param lswitch_name: The unique name of the logical switch + :type lswitch_name: string + :returns: Returns logical switch or None + """ + + @abc.abstractmethod + def get_ls_and_dns_record(self, lswitch_name): + """Returns the logical switch and 'dns' records + + :param lswitch_name: The unique name of the logical switch + :type lswitch_name: string + :returns: Returns logical switch and dns records as a tuple + """ + + @abc.abstractmethod + def get_floatingip(self, fip_id): + """Get a Floating IP by its ID + + :param fip_id: The floating IP id + :type fip_id: string + :returns: The NAT rule row or None + """ + + @abc.abstractmethod + def get_floatingip_by_ips(self, router_id, logical_ip, external_ip): + """Get a Floating IP based on it's logical and external IPs. + + DEPRECATED. In the Rocky release of OpenStack this method can be + removed and get_floatingip() should be used instead. This method + is a backward compatibility layer for the Pike -> Queens release. + + :param router_id: The ID of the router to which the FIP belongs to. + :type lrouter: string + :param logical_ip: The FIP's logical IP address + :type logical_ip: string + :param external_ip: The FIP's external IP address + :type external_ip: string + :returns: The NAT rule row or None + """ + + def check_revision_number(self, name, resource, resource_type, + if_exists=True): + """Compare the revision number from Neutron and OVN. + + Check if the revision number in OVN is lower than the one from + the Neutron resource, otherwise raise RevisionConflict and abort + the transaction. + + :param name: The unique name of the resource + :type name: string + :param resource: The neutron resource object + :type resource: dictionary + :param resource_type: The resource object type + :type resource_type: dictionary + :param if_exists: Do not fail if resource does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + :raise: RevisionConflict if the revision number in + OVN is equal or higher than the neutron object + """ + + @abc.abstractmethod + def get_lswitch_port(self, lsp_name): + """Get a Logical Switch Port by its name. + + :param lsp_name: The Logical Switch Port name + :type lsp_name: string + :returns: The Logical Switch Port row or None + """ + + @abc.abstractmethod + def get_lrouter(self, lrouter_name): + """Get a Logical Router by its name + + :param lrouter_name: The name of the logical router + :type lrouter_name: string + :returns: The Logical_Router row or None + """ + + @abc.abstractmethod + def delete_lrouter_ext_gw(self, lrouter_name): + """Delete Logical Router external gateway. + + :param lrouter_name: The name of the logical router + :type lrouter_name: string + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def get_address_set(self, addrset_id, ip_version='ip4'): + """Get a Address Set by its ID. + + :param addrset_id: The Address Set ID + :type addrset_id: string + :param ip_version: Either "ip4" or "ip6". Defaults to "ip4" + :type addr_name: string + :returns: The Address Set row or None + """ + + @abc.abstractmethod + def set_lswitch_port_to_virtual_type(self, lport_name, vip, + virtual_parent, if_exists=True): + """Set the type of a given port to "virtual". + + Set the type of a given port to "virtual" and all its related + options. + + :param lport_name: The name of the lport + :type lport_name: string + :param vip: The virtual ip + :type vip: string + :param virtual_parent: The name of the parent lport + :type virtual_parent: string + :param if_exists: Do not fail if lport does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def unset_lswitch_port_to_virtual_type(self, lport_name, + virtual_parent, if_exists=True): + """Unset the type of a given port from "virtual". + + Unset the type of a given port from "virtual" and all its related + options. + + :param lport_name: The name of the lport + :type lport_name: string + :param virtual_parent: The name of the parent lport + :type virtual_parent: string + :param if_exists: Do not fail if lport does not exist + :type if_exists: bool + :returns: :class:`Command` with no result + """ + + +@six.add_metaclass(abc.ABCMeta) +class SbAPI(api.API): + + @abc.abstractmethod + def chassis_exists(self, hostname): + """Test if chassis for given hostname exists. + + @param hostname: The hostname of the chassis + @type hostname: string + :returns: True if the chassis exists, else False. + """ + + @abc.abstractmethod + def get_chassis_hostname_and_physnets(self): + """Return a dict contains hostname and physnets mapping. + + Hostname will be dict key, and a list of physnets will be dict + value. And hostname and physnets are related to the same host. + """ + + def get_gateway_chassis_from_cms_options(self): + """Get chassis eligible for external connectivity from CMS options. + + When admin wants to enable router gateway on few chassis, + he would set the external_ids as + + ovs-vsctl set open . + external_ids:ovn-cms-options="enable-chassis-as-gw" + In this function, we parse ovn-cms-options and return these chassis + :returns: List with chassis names. + """ + + @abc.abstractmethod + def get_chassis_and_physnets(self): + """Return a dict contains chassis name and physnets mapping. + + Chassis name will be dict key, and a list of physnets will be dict + value. And chassis name and physnets are related to the same chassis. + """ + + @abc.abstractmethod + def get_all_chassis(self, chassis_type=None): + """Return a list of all chassis which match the compute_type + + :param chassis_type: The type of chassis + :type chassis_type: string + """ + + @abc.abstractmethod + def get_chassis_data_for_ml2_bind_port(self, hostname): + """Return chassis data for ML2 port binding. + + @param hostname: The hostname of the chassis + @type hostname: string + :returns: Tuple containing the chassis datapath type, + iface types and physical networks for the + OVN bridge mappings. + :raises: RuntimeError exception if an OVN chassis + does not exist. + """ diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py new file mode 100644 index 00000000000..6d6ee6f2ceb --- /dev/null +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/commands.py @@ -0,0 +1,973 @@ +# Copyright 2019 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_utils import timeutils +from ovsdbapp.backend.ovs_idl import command +from ovsdbapp.backend.ovs_idl import idlutils + +from neutron._i18n import _ +from neutron.common.ovn import constants as ovn_const +from neutron.common.ovn import exceptions as ovn_exc +from neutron.common.ovn import utils + +RESOURCE_TYPE_MAP = { + ovn_const.TYPE_NETWORKS: 'Logical_Switch', + ovn_const.TYPE_PORTS: 'Logical_Switch_Port', + ovn_const.TYPE_ROUTERS: 'Logical_Router', + ovn_const.TYPE_ROUTER_PORTS: 'Logical_Router_Port', + ovn_const.TYPE_FLOATINGIPS: 'NAT', + ovn_const.TYPE_SUBNETS: 'DHCP_Options', +} + + +def _addvalue_to_list(row, column, new_value): + row.addvalue(column, new_value) + + +def _delvalue_from_list(row, column, old_value): + row.delvalue(column, old_value) + + +def _updatevalues_in_list(row, column, new_values=None, old_values=None): + new_values = new_values or [] + old_values = old_values or [] + + for new_value in new_values: + row.addvalue(column, new_value) + for old_value in old_values: + row.delvalue(column, old_value) + + +def get_lsp_dhcp_options_uuids(lsp, lsp_name): + # Get dhcpv4_options and dhcpv6_options uuids from Logical_Switch_Port, + # which are references of port dhcp options in DHCP_Options table. + uuids = set() + for dhcp_opts in getattr(lsp, 'dhcpv4_options', []): + external_ids = getattr(dhcp_opts, 'external_ids', {}) + if external_ids.get('port_id') == lsp_name: + uuids.add(dhcp_opts.uuid) + for dhcp_opts in getattr(lsp, 'dhcpv6_options', []): + external_ids = getattr(dhcp_opts, 'external_ids', {}) + if external_ids.get('port_id') == lsp_name: + uuids.add(dhcp_opts.uuid) + return uuids + + +def _add_gateway_chassis(api, txn, lrp_name, val): + gateway_chassis = api._tables.get('Gateway_Chassis') + if gateway_chassis: + prio = len(val) + uuid_list = [] + for chassis in val: + gwc_name = '%s_%s' % (lrp_name, chassis) + try: + gwc = idlutils.row_by_value(api.idl, + 'Gateway_Chassis', + 'name', gwc_name) + except idlutils.RowNotFound: + gwc = txn.insert(gateway_chassis) + gwc.name = gwc_name + gwc.chassis_name = chassis + gwc.priority = prio + prio = prio - 1 + uuid_list.append(gwc.uuid) + return 'gateway_chassis', uuid_list + else: + chassis = {ovn_const.OVN_GATEWAY_CHASSIS_KEY: val[0]} + return 'options', chassis + + +class CheckLivenessCommand(command.BaseCommand): + def __init__(self, api): + super(CheckLivenessCommand, self).__init__(api) + + def run_idl(self, txn): + # txn.pre_commit responsible for updating nb_global.nb_cfg, but + # python-ovs will not update nb_cfg if no other changes are made + self.api.nb_global.setkey('external_ids', + ovn_const.OVN_LIVENESS_CHECK_EXT_ID_KEY, + str(timeutils.utcnow(with_timezone=True))) + self.result = self.api.nb_global.nb_cfg + + +class LSwitchSetExternalIdsCommand(command.BaseCommand): + def __init__(self, api, name, ext_ids, if_exists): + super(LSwitchSetExternalIdsCommand, self).__init__(api) + self.name = name + self.ext_ids = ext_ids + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lswitch = idlutils.row_by_value(self.api.idl, 'Logical_Switch', + 'name', self.name) + + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Switch %s does not exist") % self.name + raise RuntimeError(msg) + + lswitch.verify('external_ids') + external_ids = getattr(lswitch, 'external_ids', {}) + for key, value in self.ext_ids.items(): + external_ids[key] = value + lswitch.external_ids = external_ids + + +class AddLSwitchPortCommand(command.BaseCommand): + def __init__(self, api, lport, lswitch, may_exist, **columns): + super(AddLSwitchPortCommand, self).__init__(api) + self.lport = lport + self.lswitch = lswitch + self.may_exist = may_exist + self.columns = columns + + def run_idl(self, txn): + try: + lswitch = idlutils.row_by_value(self.api.idl, 'Logical_Switch', + 'name', self.lswitch) + except idlutils.RowNotFound: + msg = _("Logical Switch %s does not exist") % self.lswitch + raise RuntimeError(msg) + if self.may_exist: + port = idlutils.row_by_value(self.api.idl, + 'Logical_Switch_Port', 'name', + self.lport, None) + if port: + return + + port = txn.insert(self.api._tables['Logical_Switch_Port']) + port.name = self.lport + dhcpv4_options = self.columns.pop('dhcpv4_options', []) + if isinstance(dhcpv4_options, list): + port.dhcpv4_options = dhcpv4_options + else: + port.dhcpv4_options = [dhcpv4_options.result] + dhcpv6_options = self.columns.pop('dhcpv6_options', []) + if isinstance(dhcpv6_options, list): + port.dhcpv6_options = dhcpv6_options + else: + port.dhcpv6_options = [dhcpv6_options.result] + for col, val in self.columns.items(): + setattr(port, col, val) + # add the newly created port to existing lswitch + _addvalue_to_list(lswitch, 'ports', port.uuid) + self.result = port.uuid + + def post_commit(self, txn): + self.result = txn.get_insert_uuid(self.result) + + +class SetLSwitchPortCommand(command.BaseCommand): + def __init__(self, api, lport, if_exists, **columns): + super(SetLSwitchPortCommand, self).__init__(api) + self.lport = lport + self.columns = columns + self.if_exists = if_exists + + def run_idl(self, txn): + try: + port = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', + 'name', self.lport) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Switch Port %s does not exist") % self.lport + raise RuntimeError(msg) + + # Delete DHCP_Options records no longer referred by this port. + # The table rows should be consistent for the same transaction. + # After we get DHCP_Options rows uuids from port dhcpv4_options + # and dhcpv6_options references, the rows shouldn't disappear for + # this transaction before we delete it. + cur_port_dhcp_opts = get_lsp_dhcp_options_uuids( + port, self.lport) + new_port_dhcp_opts = set() + dhcpv4_options = self.columns.pop('dhcpv4_options', None) + if dhcpv4_options is None: + new_port_dhcp_opts.update([option.uuid for option in + getattr(port, 'dhcpv4_options', [])]) + elif isinstance(dhcpv4_options, list): + new_port_dhcp_opts.update(dhcpv4_options) + port.dhcpv4_options = dhcpv4_options + else: + new_port_dhcp_opts.add(dhcpv4_options.result) + port.dhcpv4_options = [dhcpv4_options.result] + dhcpv6_options = self.columns.pop('dhcpv6_options', None) + if dhcpv6_options is None: + new_port_dhcp_opts.update([option.uuid for option in + getattr(port, 'dhcpv6_options', [])]) + elif isinstance(dhcpv6_options, list): + new_port_dhcp_opts.update(dhcpv6_options) + port.dhcpv6_options = dhcpv6_options + else: + new_port_dhcp_opts.add(dhcpv6_options.result) + port.dhcpv6_options = [dhcpv6_options.result] + for uuid in cur_port_dhcp_opts - new_port_dhcp_opts: + self.api._tables['DHCP_Options'].rows[uuid].delete() + + for col, val in self.columns.items(): + setattr(port, col, val) + + +class DelLSwitchPortCommand(command.BaseCommand): + def __init__(self, api, lport, lswitch, if_exists): + super(DelLSwitchPortCommand, self).__init__(api) + self.lport = lport + self.lswitch = lswitch + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lport = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', + 'name', self.lport) + lswitch = idlutils.row_by_value(self.api.idl, 'Logical_Switch', + 'name', self.lswitch) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Port %s does not exist") % self.lport + raise RuntimeError(msg) + + # Delete DHCP_Options records no longer referred by this port. + cur_port_dhcp_opts = get_lsp_dhcp_options_uuids( + lport, self.lport) + for uuid in cur_port_dhcp_opts: + self.api._tables['DHCP_Options'].rows[uuid].delete() + + _delvalue_from_list(lswitch, 'ports', lport) + self.api._tables['Logical_Switch_Port'].rows[lport.uuid].delete() + + +class AddLRouterCommand(command.BaseCommand): + def __init__(self, api, name, may_exist, **columns): + super(AddLRouterCommand, self).__init__(api) + self.name = name + self.columns = columns + self.may_exist = may_exist + + def run_idl(self, txn): + if self.may_exist: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.name, None) + if lrouter: + return + + row = txn.insert(self.api._tables['Logical_Router']) + row.name = self.name + for col, val in self.columns.items(): + setattr(row, col, val) + + +class UpdateLRouterCommand(command.BaseCommand): + def __init__(self, api, name, if_exists, **columns): + super(UpdateLRouterCommand, self).__init__(api) + self.name = name + self.columns = columns + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.name, None) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Router %s does not exist") % self.name + raise RuntimeError(msg) + + if lrouter: + for col, val in self.columns.items(): + setattr(lrouter, col, val) + return + + +class DelLRouterCommand(command.BaseCommand): + def __init__(self, api, name, if_exists): + super(DelLRouterCommand, self).__init__(api) + self.name = name + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.name) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Router %s does not exist") % self.name + raise RuntimeError(msg) + + self.api._tables['Logical_Router'].rows[lrouter.uuid].delete() + + +class AddLRouterPortCommand(command.BaseCommand): + def __init__(self, api, name, lrouter, may_exist, **columns): + super(AddLRouterPortCommand, self).__init__(api) + self.name = name + self.lrouter = lrouter + self.may_exist = may_exist + self.columns = columns + + def run_idl(self, txn): + + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.lrouter) + except idlutils.RowNotFound: + msg = _("Logical Router %s does not exist") % self.lrouter + raise RuntimeError(msg) + try: + idlutils.row_by_value(self.api.idl, 'Logical_Router_Port', + 'name', self.name) + if self.may_exist: + return + # The LRP entry with certain name has already exist, raise an + # exception to notice caller. It's caller's responsibility to + # call UpdateLRouterPortCommand to get LRP entry processed + # correctly. + msg = _("Logical Router Port with name \"%s\" " + "already exists.") % self.name + raise RuntimeError(msg) + except idlutils.RowNotFound: + lrouter_port = txn.insert(self.api._tables['Logical_Router_Port']) + lrouter_port.name = self.name + for col, val in self.columns.items(): + if col == 'gateway_chassis': + col, val = _add_gateway_chassis(self.api, txn, self.name, + val) + setattr(lrouter_port, col, val) + _addvalue_to_list(lrouter, 'ports', lrouter_port) + + +class UpdateLRouterPortCommand(command.BaseCommand): + def __init__(self, api, name, if_exists, **columns): + super(UpdateLRouterPortCommand, self).__init__(api) + self.name = name + self.columns = columns + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lrouter_port = idlutils.row_by_value(self.api.idl, + 'Logical_Router_Port', + 'name', self.name) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Router Port %s does not exist") % self.name + raise RuntimeError(msg) + + # TODO(lucasagomes): Remove this check once we drop the support + # for OVS versions <= 2.8 + ipv6_ra_configs_supported = self.api.is_col_present( + 'Logical_Router_Port', 'ipv6_ra_configs') + for col, val in self.columns.items(): + if col == 'ipv6_ra_configs' and not ipv6_ra_configs_supported: + continue + + if col == 'gateway_chassis': + col, val = _add_gateway_chassis(self.api, txn, self.name, + val) + setattr(lrouter_port, col, val) + + +class DelLRouterPortCommand(command.BaseCommand): + def __init__(self, api, name, lrouter, if_exists): + super(DelLRouterPortCommand, self).__init__(api) + self.name = name + self.lrouter = lrouter + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lrouter_port = idlutils.row_by_value(self.api.idl, + 'Logical_Router_Port', + 'name', self.name) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Router Port %s does not exist") % self.name + raise RuntimeError(msg) + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.lrouter) + except idlutils.RowNotFound: + msg = _("Logical Router %s does not exist") % self.lrouter + raise RuntimeError(msg) + + _delvalue_from_list(lrouter, 'ports', lrouter_port) + lrouter_port.delete() + + +class SetLRouterPortInLSwitchPortCommand(command.BaseCommand): + def __init__(self, api, lswitch_port, lrouter_port, is_gw_port, + if_exists, lsp_address): + super(SetLRouterPortInLSwitchPortCommand, self).__init__(api) + self.lswitch_port = lswitch_port + self.lrouter_port = lrouter_port + self.is_gw_port = is_gw_port + self.if_exists = if_exists + self.lsp_address = lsp_address + + def run_idl(self, txn): + try: + port = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', + 'name', self.lswitch_port) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Switch Port %s does not " + "exist") % self.lswitch_port + raise RuntimeError(msg) + + options = {'router-port': self.lrouter_port} + if self.is_gw_port: + options[ovn_const.OVN_GATEWAY_NAT_ADDRESSES_KEY] = 'router' + setattr(port, 'options', options) + setattr(port, 'type', 'router') + setattr(port, 'addresses', self.lsp_address) + + +class AddStaticRouteCommand(command.BaseCommand): + def __init__(self, api, lrouter, **columns): + super(AddStaticRouteCommand, self).__init__(api) + self.lrouter = lrouter + self.columns = columns + + def run_idl(self, txn): + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.lrouter) + except idlutils.RowNotFound: + msg = _("Logical Router %s does not exist") % self.lrouter + raise RuntimeError(msg) + + row = txn.insert(self.api._tables['Logical_Router_Static_Route']) + for col, val in self.columns.items(): + setattr(row, col, val) + _addvalue_to_list(lrouter, 'static_routes', row.uuid) + + +class DelStaticRouteCommand(command.BaseCommand): + def __init__(self, api, lrouter, ip_prefix, nexthop, if_exists): + super(DelStaticRouteCommand, self).__init__(api) + self.lrouter = lrouter + self.ip_prefix = ip_prefix + self.nexthop = nexthop + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.lrouter) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Router %s does not exist") % self.lrouter + raise RuntimeError(msg) + + static_routes = getattr(lrouter, 'static_routes', []) + for route in static_routes: + ip_prefix = getattr(route, 'ip_prefix', '') + nexthop = getattr(route, 'nexthop', '') + if self.ip_prefix == ip_prefix and self.nexthop == nexthop: + _delvalue_from_list(lrouter, 'static_routes', route) + route.delete() + break + + +class AddAddrSetCommand(command.BaseCommand): + def __init__(self, api, name, may_exist, **columns): + super(AddAddrSetCommand, self).__init__(api) + self.name = name + self.columns = columns + self.may_exist = may_exist + + def run_idl(self, txn): + if self.may_exist: + addrset = idlutils.row_by_value(self.api.idl, 'Address_Set', + 'name', self.name, None) + if addrset: + return + row = txn.insert(self.api._tables['Address_Set']) + row.name = self.name + for col, val in self.columns.items(): + setattr(row, col, val) + + +class DelAddrSetCommand(command.BaseCommand): + def __init__(self, api, name, if_exists): + super(DelAddrSetCommand, self).__init__(api) + self.name = name + self.if_exists = if_exists + + def run_idl(self, txn): + try: + addrset = idlutils.row_by_value(self.api.idl, 'Address_Set', + 'name', self.name) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Address set %s does not exist. " + "Can't delete.") % self.name + raise RuntimeError(msg) + + self.api._tables['Address_Set'].rows[addrset.uuid].delete() + + +class UpdateAddrSetCommand(command.BaseCommand): + def __init__(self, api, name, addrs_add, addrs_remove, if_exists): + super(UpdateAddrSetCommand, self).__init__(api) + self.name = name + self.addrs_add = addrs_add + self.addrs_remove = addrs_remove + self.if_exists = if_exists + + def run_idl(self, txn): + try: + addrset = idlutils.row_by_value(self.api.idl, 'Address_Set', + 'name', self.name) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Address set %s does not exist. " + "Can't update addresses") % self.name + raise RuntimeError(msg) + + _updatevalues_in_list( + addrset, 'addresses', + new_values=self.addrs_add, + old_values=self.addrs_remove) + + +class UpdateAddrSetExtIdsCommand(command.BaseCommand): + def __init__(self, api, name, external_ids, if_exists): + super(UpdateAddrSetExtIdsCommand, self).__init__(api) + self.name = name + self.external_ids = external_ids + self.if_exists = if_exists + + def run_idl(self, txn): + try: + addrset = idlutils.row_by_value(self.api.idl, 'Address_Set', + 'name', self.name) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Address set %s does not exist. " + "Can't update external IDs") % self.name + raise RuntimeError(msg) + + addrset.verify('external_ids') + addrset_external_ids = getattr(addrset, 'external_ids', {}) + for ext_id_key, ext_id_value in self.external_ids.items(): + addrset_external_ids[ext_id_key] = ext_id_value + addrset.external_ids = addrset_external_ids + + +class UpdateChassisExtIdsCommand(command.BaseCommand): + def __init__(self, api, name, external_ids, if_exists): + super(UpdateChassisExtIdsCommand, self).__init__(api) + self.name = name + self.external_ids = external_ids + self.if_exists = if_exists + + def run_idl(self, txn): + try: + chassis = idlutils.row_by_value(self.api.idl, 'Chassis', + 'name', self.name) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Chassis %s does not exist. " + "Can't update external IDs") % self.name + raise RuntimeError(msg) + + chassis.verify('external_ids') + chassis_external_ids = getattr(chassis, 'external_ids', {}) + for ext_id_key, ext_id_value in self.external_ids.items(): + chassis_external_ids[ext_id_key] = ext_id_value + chassis.external_ids = chassis_external_ids + + +class UpdatePortBindingExtIdsCommand(command.BaseCommand): + def __init__(self, api, name, external_ids, if_exists): + super(UpdatePortBindingExtIdsCommand, self).__init__(api) + self.name = name + self.external_ids = external_ids + self.if_exists = if_exists + + def run_idl(self, txn): + try: + port = idlutils.row_by_value(self.api.idl, 'Port_Binding', + 'logical_port', self.name) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Port %s does not exist. " + "Can't update external IDs") % self.name + raise RuntimeError(msg) + + port.verify('external_ids') + port_external_ids = getattr(port, 'external_ids', {}) + for ext_id_key, ext_id_value in self.external_ids.items(): + port_external_ids[ext_id_key] = ext_id_value + port.external_ids = port_external_ids + + +class AddDHCPOptionsCommand(command.BaseCommand): + def __init__(self, api, subnet_id, port_id=None, may_exist=True, + **columns): + super(AddDHCPOptionsCommand, self).__init__(api) + self.columns = columns + self.may_exist = may_exist + self.subnet_id = subnet_id + self.port_id = port_id + self.new_insert = False + + def _get_dhcp_options_row(self): + for row in self.api._tables['DHCP_Options'].rows.values(): + external_ids = getattr(row, 'external_ids', {}) + port_id = external_ids.get('port_id') + if self.subnet_id == external_ids.get('subnet_id'): + if self.port_id == port_id: + return row + + def run_idl(self, txn): + row = None + if self.may_exist: + row = self._get_dhcp_options_row() + + if not row: + row = txn.insert(self.api._tables['DHCP_Options']) + self.new_insert = True + for col, val in self.columns.items(): + setattr(row, col, val) + self.result = row.uuid + + def post_commit(self, txn): + # Update the result with inserted uuid for new inserted row, or the + # uuid get in run_idl should be real uuid already. + if self.new_insert: + self.result = txn.get_insert_uuid(self.result) + + +class DelDHCPOptionsCommand(command.BaseCommand): + def __init__(self, api, row_uuid, if_exists=True): + super(DelDHCPOptionsCommand, self).__init__(api) + self.if_exists = if_exists + self.row_uuid = row_uuid + + def run_idl(self, txn): + if self.row_uuid not in self.api._tables['DHCP_Options'].rows: + if self.if_exists: + return + msg = _("DHCP Options row %s does not exist") % self.row_uuid + raise RuntimeError(msg) + + self.api._tables['DHCP_Options'].rows[self.row_uuid].delete() + + +class AddNATRuleInLRouterCommand(command.BaseCommand): + # TODO(chandrav): Add unit tests, bug #1638715. + def __init__(self, api, lrouter, **columns): + super(AddNATRuleInLRouterCommand, self).__init__(api) + self.lrouter = lrouter + self.columns = columns + + def run_idl(self, txn): + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.lrouter) + except idlutils.RowNotFound: + msg = _("Logical Router %s does not exist") % self.lrouter + raise RuntimeError(msg) + + row = txn.insert(self.api._tables['NAT']) + for col, val in self.columns.items(): + setattr(row, col, val) + # TODO(chandrav): convert this to ovs transaction mutate + lrouter.verify('nat') + nat = getattr(lrouter, 'nat', []) + nat.append(row.uuid) + setattr(lrouter, 'nat', nat) + + +class DeleteNATRuleInLRouterCommand(command.BaseCommand): + # TODO(chandrav): Add unit tests, bug #1638715. + def __init__(self, api, lrouter, type, logical_ip, external_ip, + if_exists): + super(DeleteNATRuleInLRouterCommand, self).__init__(api) + self.lrouter = lrouter + self.type = type + self.logical_ip = logical_ip + self.external_ip = external_ip + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.lrouter) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Router %s does not exist") % self.lrouter + raise RuntimeError(msg) + + lrouter.verify('nat') + # TODO(chandrav): convert this to ovs transaction mutate + nats = getattr(lrouter, 'nat', []) + for nat in nats: + type = getattr(nat, 'type', '') + external_ip = getattr(nat, 'external_ip', '') + logical_ip = getattr(nat, 'logical_ip', '') + if (self.type == type and + self.external_ip == external_ip and + self.logical_ip == logical_ip): + nats.remove(nat) + nat.delete() + break + setattr(lrouter, 'nat', nats) + + +class SetNATRuleInLRouterCommand(command.BaseCommand): + def __init__(self, api, lrouter, nat_rule_uuid, **columns): + super(SetNATRuleInLRouterCommand, self).__init__(api) + self.lrouter = lrouter + self.nat_rule_uuid = nat_rule_uuid + self.columns = columns + + def run_idl(self, txn): + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.lrouter) + except idlutils.RowNotFound: + msg = _("Logical Router %s does not exist") % self.lrouter + raise RuntimeError(msg) + + lrouter.verify('nat') + nat_rules = getattr(lrouter, 'nat', []) + for nat_rule in nat_rules: + if nat_rule.uuid == self.nat_rule_uuid: + for col, val in self.columns.items(): + setattr(nat_rule, col, val) + break + + +class CheckRevisionNumberCommand(command.BaseCommand): + + def __init__(self, api, name, resource, resource_type, if_exists): + super(CheckRevisionNumberCommand, self).__init__(api) + self.name = name + self.resource = resource + self.resource_type = resource_type + self.if_exists = if_exists + + def _get_floatingip(self): + # TODO(lucasagomes): We can't use self.api.lookup() because that + # method does not introspect map type columns. We could either: + # 1. Enhance it to look into maps or, 2. Add a new ``name`` column + # to the NAT table so that we can use lookup() just like we do + # for other resources + for nat in self.api._tables['NAT'].rows.values(): + if nat.type != 'dnat_and_snat': + continue + ext_ids = getattr(nat, 'external_ids', {}) + if ext_ids.get(ovn_const.OVN_FIP_EXT_ID_KEY) == self.name: + return nat + + raise idlutils.RowNotFound( + table='NAT', col='external_ids', match=self.name) + + def _get_subnet(self): + for dhcp in self.api._tables['DHCP_Options'].rows.values(): + ext_ids = getattr(dhcp, 'external_ids', {}) + # Ignore ports DHCP Options + if ext_ids.get('port_id'): + continue + if ext_ids.get('subnet_id') == self.name: + return dhcp + + raise idlutils.RowNotFound( + table='DHCP_Options', col='external_ids', match=self.name) + + def run_idl(self, txn): + try: + ovn_table = RESOURCE_TYPE_MAP[self.resource_type] + # TODO(lucasagomes): After OVS 2.8.2 is released all tables should + # have the external_ids column. We can remove this conditional + # here by then. + if not self.api.is_col_present(ovn_table, 'external_ids'): + return + + ovn_resource = None + if self.resource_type == ovn_const.TYPE_FLOATINGIPS: + ovn_resource = self._get_floatingip() + elif self.resource_type == ovn_const.TYPE_SUBNETS: + ovn_resource = self._get_subnet() + else: + ovn_resource = self.api.lookup(ovn_table, self.name) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = (_('Failed to check the revision number for %s: Resource ' + 'does not exist') % self.name) + raise RuntimeError(msg) + + external_ids = getattr(ovn_resource, 'external_ids', {}) + ovn_revision = int(external_ids.get( + ovn_const.OVN_REV_NUM_EXT_ID_KEY, -1)) + neutron_revision = utils.get_revision_number(self.resource, + self.resource_type) + if ovn_revision > neutron_revision: + raise ovn_exc.RevisionConflict( + resource_id=self.name, resource_type=self.resource_type) + + ovn_resource.verify('external_ids') + ovn_resource.setkey('external_ids', ovn_const.OVN_REV_NUM_EXT_ID_KEY, + str(neutron_revision)) + + def post_commit(self, txn): + self.result = ovn_const.TXN_COMMITTED + + +class DeleteLRouterExtGwCommand(command.BaseCommand): + + def __init__(self, api, lrouter, if_exists): + super(DeleteLRouterExtGwCommand, self).__init__(api) + self.lrouter = lrouter + self.if_exists = if_exists + + def run_idl(self, txn): + # TODO(lucasagomes): Remove this check after OVS 2.8.2 is tagged + # (prior to that, the external_ids column didn't exist in this + # table). + if not self.api.is_col_present('Logical_Router_Static_Route', + 'external_ids'): + return + + try: + lrouter = idlutils.row_by_value(self.api.idl, 'Logical_Router', + 'name', self.lrouter) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = _("Logical Router %s does not exist") % self.lrouter + raise RuntimeError(msg) + + lrouter.verify('static_routes') + static_routes = getattr(lrouter, 'static_routes', []) + for route in static_routes: + external_ids = getattr(route, 'external_ids', {}) + if ovn_const.OVN_ROUTER_IS_EXT_GW in external_ids: + _delvalue_from_list(lrouter, 'static_routes', route) + route.delete() + break + + lrouter.verify('nat') + nats = getattr(lrouter, 'nat', []) + for nat in nats: + if nat.type != 'snat': + continue + _delvalue_from_list(lrouter, 'nat', nat) + nat.delete() + + lrouter_ext_ids = getattr(lrouter, 'external_ids', {}) + gw_port_id = lrouter_ext_ids.get(ovn_const.OVN_GW_PORT_EXT_ID_KEY) + if not gw_port_id: + return + + try: + lrouter_port = idlutils.row_by_value( + self.api.idl, 'Logical_Router_Port', 'name', + utils.ovn_lrouter_port_name(gw_port_id)) + except idlutils.RowNotFound: + return + + _delvalue_from_list(lrouter, 'ports', lrouter_port) + + +class SetLSwitchPortToVirtualTypeCommand(command.BaseCommand): + def __init__(self, api, lport, vip, parent, if_exists): + super(SetLSwitchPortToVirtualTypeCommand, self).__init__(api) + self.lport = lport + self.vip = vip + self.parent = parent + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lsp = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', + 'name', self.lport) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = "Logical Switch Port %s does not exist" % self.lport + raise RuntimeError(msg) + + options = lsp.options + options[ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY] = self.vip + virtual_parents = options.get( + ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY, set()) + if virtual_parents: + virtual_parents = set(virtual_parents.split(',')) + + virtual_parents.add(self.parent) + options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY] = ','.join( + virtual_parents) + setattr(lsp, 'options', options) + setattr(lsp, 'type', ovn_const.LSP_TYPE_VIRTUAL) + + +class UnsetLSwitchPortToVirtualTypeCommand(command.BaseCommand): + def __init__(self, api, lport, parent, if_exists): + super(UnsetLSwitchPortToVirtualTypeCommand, self).__init__(api) + self.lport = lport + self.parent = parent + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lsp = idlutils.row_by_value(self.api.idl, 'Logical_Switch_Port', + 'name', self.lport) + except idlutils.RowNotFound: + if self.if_exists: + return + msg = "Logical Switch Port %s does not exist" % self.lport + raise RuntimeError(msg) + + options = lsp.options + virtual_parents = options.get( + ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY, set()) + if virtual_parents: + virtual_parents = set(virtual_parents.split(',')) + + try: + virtual_parents.remove(self.parent) + except KeyError: + pass + + # If virtual-parents is now empty, change the type and remove the + # virtual-parents and virtual-ip options + if not virtual_parents: + options.pop(ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY, None) + options.pop(ovn_const.LSP_OPTIONS_VIRTUAL_IP_KEY, None) + setattr(lsp, 'type', '') + else: + options[ovn_const.LSP_OPTIONS_VIRTUAL_PARENTS_KEY] = ','.join( + virtual_parents) + + setattr(lsp, 'options', options) diff --git a/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/worker.py b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/worker.py new file mode 100644 index 00000000000..b0320189706 --- /dev/null +++ b/neutron/plugins/ml2/drivers/ovn/mech_driver/ovsdb/worker.py @@ -0,0 +1,38 @@ +# Copyright 2019 Red Hat, Inc. +# +# 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 worker + +from neutron.common import config + + +class MaintenanceWorker(worker.BaseWorker): + + def start(self): + super(MaintenanceWorker, self).start() + # NOTE(twilson) The super class will trigger the post_fork_initialize + # in the driver, which starts the connection/IDL notify loop which + # keeps the process from exiting + + def stop(self): + """Stop service.""" + super(MaintenanceWorker, self).stop() + + def wait(self): + """Wait for service to complete.""" + super(MaintenanceWorker, self).wait() + + @staticmethod + def reset(): + config.reset_service() diff --git a/neutron/tests/unit/fake_resources.py b/neutron/tests/unit/fake_resources.py new file mode 100644 index 00000000000..c39f47771cf --- /dev/null +++ b/neutron/tests/unit/fake_resources.py @@ -0,0 +1,752 @@ +# +# 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 mock +from neutron_lib.api.definitions import l3 +from oslo_utils import uuidutils + +from neutron.common.ovn import constants as ovn_const +from neutron.common.ovn import utils as ovn_utils + + +class FakeOvsdbNbOvnIdl(object): + + def __init__(self, **kwargs): + self.lswitch_table = FakeOvsdbTable.create_one_ovsdb_table() + self.lsp_table = FakeOvsdbTable.create_one_ovsdb_table() + self.lrouter_table = FakeOvsdbTable.create_one_ovsdb_table() + self.lrouter_static_route_table = \ + FakeOvsdbTable.create_one_ovsdb_table() + self.lrp_table = FakeOvsdbTable.create_one_ovsdb_table() + self.addrset_table = FakeOvsdbTable.create_one_ovsdb_table() + self.acl_table = FakeOvsdbTable.create_one_ovsdb_table() + self.dhcp_options_table = FakeOvsdbTable.create_one_ovsdb_table() + self.nat_table = FakeOvsdbTable.create_one_ovsdb_table() + self.port_group_table = FakeOvsdbTable.create_one_ovsdb_table() + self._tables = {} + self._tables['Logical_Switch'] = self.lswitch_table + self._tables['Logical_Switch_Port'] = self.lsp_table + self._tables['Logical_Router'] = self.lrouter_table + self._tables['Logical_Router_Port'] = self.lrp_table + self._tables['Logical_Router_Static_Route'] = \ + self.lrouter_static_route_table + self._tables['ACL'] = self.acl_table + self._tables['Address_Set'] = self.addrset_table + self._tables['DHCP_Options'] = self.dhcp_options_table + self._tables['NAT'] = self.nat_table + self._tables['Port_Group'] = self.port_group_table + self.transaction = mock.MagicMock() + self.ls_add = mock.Mock() + self.set_lswitch_ext_ids = mock.Mock() + self.ls_del = mock.Mock() + self.create_lswitch_port = mock.Mock() + self.set_lswitch_port = mock.Mock() + self.delete_lswitch_port = mock.Mock() + self.get_acls_for_lswitches = mock.Mock() + self.create_lrouter = mock.Mock() + self.lrp_del = mock.Mock() + self.update_lrouter = mock.Mock() + self.delete_lrouter = mock.Mock() + self.add_lrouter_port = mock.Mock() + self.update_lrouter_port = mock.Mock() + self.delete_lrouter_port = mock.Mock() + self.set_lrouter_port_in_lswitch_port = mock.Mock() + self.add_acl = mock.Mock() + self.delete_acl = mock.Mock() + self.update_acls = mock.Mock() + self.idl = mock.Mock() + self.add_static_route = mock.Mock() + self.delete_static_route = mock.Mock() + self.create_address_set = mock.Mock() + self.update_address_set_ext_ids = mock.Mock() + self.delete_address_set = mock.Mock() + self.update_address_set = mock.Mock() + self.get_all_chassis_gateway_bindings = mock.Mock() + self.get_gateway_chassis_binding = mock.Mock() + self.get_unhosted_gateways = mock.Mock() + self.add_dhcp_options = mock.Mock() + self.delete_dhcp_options = mock.Mock() + self.get_subnet_dhcp_options = mock.Mock() + self.get_subnet_dhcp_options.return_value = { + 'subnet': None, 'ports': []} + self.get_subnets_dhcp_options = mock.Mock() + self.get_subnets_dhcp_options.return_value = [] + self.get_all_dhcp_options = mock.Mock() + self.get_router_port_options = mock.MagicMock() + self.get_router_port_options.return_value = {} + self.add_nat_rule_in_lrouter = mock.Mock() + self.delete_nat_rule_in_lrouter = mock.Mock() + self.get_lrouter_nat_rules = mock.Mock() + self.get_lrouter_nat_rules.return_value = [] + self.set_nat_rule_in_lrouter = mock.Mock() + self.check_for_row_by_value_and_retry = mock.Mock() + self.get_parent_port = mock.Mock() + self.get_parent_port.return_value = [] + self.dns_add = mock.Mock() + self.get_lswitch = mock.Mock() + fake_ovs_row = FakeOvsdbRow.create_one_ovsdb_row() + self.get_lswitch.return_value = fake_ovs_row + self.get_lswitch_port = mock.Mock() + self.get_lswitch_port.return_value = fake_ovs_row + self.get_ls_and_dns_record = mock.Mock() + self.get_ls_and_dns_record.return_value = (fake_ovs_row, None) + self.ls_set_dns_records = mock.Mock() + self.get_floatingip = mock.Mock() + self.get_floatingip.return_value = None + self.check_revision_number = mock.Mock() + self.lookup = mock.MagicMock() + # TODO(lucasagomes): The get_floatingip_by_ips() method is part + # of a backwards compatibility layer for the Pike -> Queens release, + # remove it in the Rocky release. + self.get_floatingip_by_ips = mock.Mock() + self.get_floatingip_by_ips.return_value = None + self.is_col_present = mock.Mock() + self.is_col_present.return_value = False + self.get_lrouter = mock.Mock() + self.get_lrouter.return_value = None + self.delete_lrouter_ext_gw = mock.Mock() + self.delete_lrouter_ext_gw.return_value = None + self.is_port_groups_supported = mock.Mock() + # TODO(lucasagomes): Flip this return value to True at some point, + # port groups should be the default method used by networking-ovn + self.is_port_groups_supported.return_value = False + self.get_address_set = mock.Mock() + self.get_address_set.return_value = None + self.pg_acl_add = mock.Mock() + self.pg_acl_del = mock.Mock() + self.pg_del = mock.Mock() + self.pg_add = mock.Mock() + self.get_port_group = mock.Mock() + self.pg_add_ports = mock.Mock() + self.pg_del_ports = mock.Mock() + self.lsp_get_up = mock.Mock() + self.nb_global = mock.Mock() + self.db_list_rows = mock.Mock() + self.lsp_list = mock.MagicMock() + self.db_find = mock.Mock() + self.db_set = mock.Mock() + self.db_clear = mock.Mock() + self.db_remove = mock.Mock() + self.set_lswitch_port_to_virtual_type = mock.Mock() + self.unset_lswitch_port_to_virtual_type = mock.Mock() + self.ls_get = mock.Mock() + + +class FakeOvsdbSbOvnIdl(object): + + def __init__(self, **kwargs): + self.chassis_exists = mock.Mock() + self.chassis_exists.return_value = True + self.get_chassis_hostname_and_physnets = mock.Mock() + self.get_chassis_hostname_and_physnets.return_value = {} + self.get_all_chassis = mock.Mock() + self.get_chassis_data_for_ml2_bind_port = mock.Mock() + self.get_chassis_data_for_ml2_bind_port.return_value = \ + ('fake', '', ['fake-physnet']) + self.get_logical_port_chassis_and_datapath = mock.Mock() + self.get_logical_port_chassis_and_datapath.return_value = \ + ('fake', 'fake-dp') + self.get_chassis_and_physnets = mock.Mock() + self.get_gateway_chassis_from_cms_options = mock.Mock() + self.is_col_present = mock.Mock() + self.is_col_present.return_value = False + + +class FakeOvsdbTransaction(object): + def __init__(self, **kwargs): + self.insert = mock.Mock() + + +class FakePlugin(object): + + def __init__(self, **kwargs): + self.get_ports = mock.Mock() + self._get_port_security_group_bindings = mock.Mock() + + +class FakeResource(dict): + + def __init__(self, manager=None, info=None, loaded=False, methods=None): + """Set attributes and methods for a resource. + + :param manager: + The resource manager + :param Dictionary info: + A dictionary with all attributes + :param bool loaded: + True if the resource is loaded in memory + :param Dictionary methods: + A dictionary with all methods + """ + info = info or {} + super(FakeResource, self).__init__(info) + methods = methods or {} + + self.__name__ = type(self).__name__ + self.manager = manager + self._info = info + self._add_details(info) + self._add_methods(methods) + self._loaded = loaded + # Add a revision number by default + setattr(self, 'revision_number', 1) + + @property + def db_obj(self): + return self + + def _add_details(self, info): + for (k, v) in info.items(): + setattr(self, k, v) + + def _add_methods(self, methods): + """Fake methods with MagicMock objects. + + For each <@key, @value> pairs in methods, add an callable MagicMock + object named @key as an attribute, and set the mock's return_value to + @value. When users access the attribute with (), @value will be + returned, which looks like a function call. + """ + for (name, ret) in methods.items(): + method = mock.MagicMock(return_value=ret) + setattr(self, name, method) + + def __repr__(self): + reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and + k != 'manager') + info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) + return "<%s %s>" % (self.__class__.__name__, info) + + def keys(self): + return self._info.keys() + + def info(self): + return self._info + + def update(self, info): + super(FakeResource, self).update(info) + self._add_details(info) + + +class FakeNetwork(object): + """Fake one or more networks.""" + + @staticmethod + def create_one_network(attrs=None): + """Create a fake network. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the network + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuidutils.generate_uuid() + network_attrs = { + 'id': 'network-id-' + fake_uuid, + 'name': 'network-name-' + fake_uuid, + 'status': 'ACTIVE', + 'tenant_id': 'project-id-' + fake_uuid, + 'admin_state_up': True, + 'shared': False, + 'subnets': [], + 'provider:network_type': 'geneve', + 'provider:physical_network': None, + 'provider:segmentation_id': 10, + 'router:external': False, + 'availability_zones': [], + 'availability_zone_hints': [], + 'is_default': False, + } + + # Overwrite default attributes. + network_attrs.update(attrs) + + return FakeResource(info=copy.deepcopy(network_attrs), + loaded=True) + + +class FakeNetworkContext(object): + def __init__(self, network, segments): + self.fake_network = network + self.fake_segments = segments + self._plugin_context = mock.MagicMock() + + @property + def current(self): + return self.fake_network + + @property + def original(self): + return None + + @property + def network_segments(self): + return self.fake_segments + + +class FakeSubnetContext(object): + def __init__(self, subnet, original_subnet=None, network=None): + self.fake_subnet = subnet + self.fake_original_subnet = original_subnet + self.fake_network = FakeNetworkContext(network, None) + + @property + def current(self): + return self.fake_subnet + + @property + def original(self): + return self.fake_original_subnet + + @property + def network(self): + return self.fake_network + + +class FakeOvsdbRow(FakeResource): + """Fake one or more OVSDB rows.""" + + @staticmethod + def create_one_ovsdb_row(attrs=None, methods=None): + """Create a fake OVSDB row. + + :param Dictionary attrs: + A dictionary with all attributes + :param Dictionary methods: + A dictionary with all methods + :return: + A FakeResource object faking the OVSDB row + """ + attrs = attrs or {} + methods = methods or {} + + # Set default attributes. + fake_uuid = uuidutils.generate_uuid() + ovsdb_row_attrs = { + 'uuid': fake_uuid, + 'name': 'name-' + fake_uuid, + 'external_ids': {}, + } + + # Set default methods. + ovsdb_row_methods = { + 'addvalue': None, + 'delete': None, + 'delvalue': None, + 'verify': None, + 'setkey': None, + } + + # Overwrite default attributes and methods. + ovsdb_row_attrs.update(attrs) + ovsdb_row_methods.update(methods) + + return FakeResource(info=copy.deepcopy(ovsdb_row_attrs), + loaded=True, + methods=copy.deepcopy(ovsdb_row_methods)) + + +class FakeOvsdbTable(FakeResource): + """Fake one or more OVSDB tables.""" + + @staticmethod + def create_one_ovsdb_table(attrs=None): + """Create a fake OVSDB table. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the OVSDB table + """ + attrs = attrs or {} + + # Set default attributes. + ovsdb_table_attrs = { + 'rows': {}, + 'columns': {}, + } + + # Overwrite default attributes. + ovsdb_table_attrs.update(attrs) + + return FakeResource(info=copy.deepcopy(ovsdb_table_attrs), + loaded=True) + + +class FakePort(object): + """Fake one or more ports.""" + + @staticmethod + def create_one_port(attrs=None): + """Create a fake port. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the port + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuidutils.generate_uuid() + port_attrs = { + 'admin_state_up': True, + 'allowed_address_pairs': [{}], + 'binding:host_id': 'binding-host-id-' + fake_uuid, + 'binding:profile': {}, + 'binding:vif_details': {}, + 'binding:vif_type': 'ovs', + 'binding:vnic_type': 'normal', + 'device_id': 'device-id-' + fake_uuid, + 'device_owner': 'compute:nova', + 'dns_assignment': [{}], + 'dns_name': 'dns-name-' + fake_uuid, + 'extra_dhcp_opts': [{}], + 'fixed_ips': [{'subnet_id': 'subnet-id-' + fake_uuid, + 'ip_address': '10.10.10.20'}], + 'id': 'port-id-' + fake_uuid, + 'mac_address': 'fa:16:3e:a9:4e:72', + 'name': 'port-name-' + fake_uuid, + 'network_id': 'network-id-' + fake_uuid, + 'port_security_enabled': True, + 'security_groups': [], + 'status': 'ACTIVE', + 'tenant_id': 'project-id-' + fake_uuid, + } + + # Overwrite default attributes. + port_attrs.update(attrs) + + return FakeResource(info=copy.deepcopy(port_attrs), + loaded=True) + + +class FakePortContext(object): + def __init__(self, port, host, segments_to_bind): + self.fake_port = port + self.fake_host = host + self.fake_segments_to_bind = segments_to_bind + self.set_binding = mock.Mock() + + @property + def current(self): + return self.fake_port + + @property + def host(self): + return self.fake_host + + @property + def segments_to_bind(self): + return self.fake_segments_to_bind + + +class FakeSecurityGroup(object): + """Fake one or more security groups.""" + + @staticmethod + def create_one_security_group(attrs=None): + """Create a fake security group. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the security group + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuidutils.generate_uuid() + security_group_attrs = { + 'id': 'security-group-id-' + fake_uuid, + 'name': 'security-group-name-' + fake_uuid, + 'description': 'security-group-description-' + fake_uuid, + 'tenant_id': 'project-id-' + fake_uuid, + 'security_group_rules': [], + } + + # Overwrite default attributes. + security_group_attrs.update(attrs) + + return FakeResource(info=copy.deepcopy(security_group_attrs), + loaded=True) + + +class FakeSecurityGroupRule(object): + """Fake one or more security group rules.""" + + @staticmethod + def create_one_security_group_rule(attrs=None): + """Create a fake security group rule. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the security group rule + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuidutils.generate_uuid() + security_group_rule_attrs = { + 'direction': 'ingress', + 'ethertype': 'IPv4', + 'id': 'security-group-rule-id-' + fake_uuid, + 'port_range_max': 22, + 'port_range_min': 22, + 'protocol': 'tcp', + 'remote_group_id': None, + 'remote_ip_prefix': '0.0.0.0/0', + 'security_group_id': 'security-group-id-' + fake_uuid, + 'tenant_id': 'project-id-' + fake_uuid, + } + + # Overwrite default attributes. + security_group_rule_attrs.update(attrs) + + return FakeResource(info=copy.deepcopy(security_group_rule_attrs), + loaded=True) + + +class FakeSegment(object): + """Fake one or more segments.""" + + @staticmethod + def create_one_segment(attrs=None): + """Create a fake segment. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the segment + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuidutils.generate_uuid() + segment_attrs = { + 'id': 'segment-id-' + fake_uuid, + 'network_type': 'geneve', + 'physical_network': None, + 'segmentation_id': 10, + } + + # Overwrite default attributes. + segment_attrs.update(attrs) + + return FakeResource(info=copy.deepcopy(segment_attrs), + loaded=True) + + +class FakeSubnet(object): + """Fake one or more subnets.""" + + @staticmethod + def create_one_subnet(attrs=None): + """Create a fake subnet. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the subnet + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuidutils.generate_uuid() + subnet_attrs = { + 'id': 'subnet-id-' + fake_uuid, + 'name': 'subnet-name-' + fake_uuid, + 'network_id': 'network-id-' + fake_uuid, + 'cidr': '10.10.10.0/24', + 'tenant_id': 'project-id-' + fake_uuid, + 'enable_dhcp': True, + 'dns_nameservers': [], + 'allocation_pools': [], + 'host_routes': [], + 'ip_version': 4, + 'gateway_ip': '10.10.10.1', + 'ipv6_address_mode': 'None', + 'ipv6_ra_mode': 'None', + 'subnetpool_id': None, + } + + # Overwrite default attributes. + subnet_attrs.update(attrs) + + return FakeResource(info=copy.deepcopy(subnet_attrs), + loaded=True) + + +class FakeFloatingIp(object): + """Fake one or more floating ips.""" + + @staticmethod + def create_one_fip(attrs=None): + """Create a fake floating ip. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the floating ip + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuidutils.generate_uuid() + fip_attrs = { + 'id': 'fip-id-' + fake_uuid, + 'tenant_id': '', + 'fixed_ip_address': '10.0.0.10', + 'floating_ip_address': '172.21.0.100', + 'router_id': 'router-id', + 'port_id': 'port_id', + 'fixed_port_id': 'port_id', + 'floating_port_id': 'fip-port-id', + 'status': 'Active', + 'floating_network_id': 'fip-net-id', + 'dns': '', + 'dns_domain': '', + 'dns_name': '', + 'project_id': '', + } + + # Overwrite default attributes. + fip_attrs.update(attrs) + + return FakeResource(info=copy.deepcopy(fip_attrs), + loaded=True) + + +class FakeOVNPort(object): + """Fake one or more ports.""" + + @staticmethod + def create_one_port(attrs=None): + """Create a fake ovn port. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the port + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuidutils.generate_uuid() + port_attrs = { + 'addresses': [], + 'dhcpv4_options': '', + 'dhcpv6_options': [], + 'enabled': True, + 'external_ids': {}, + 'name': fake_uuid, + 'options': {}, + 'parent_name': [], + 'port_security': [], + 'tag': [], + 'tag_request': [], + 'type': '', + 'up': False, + } + + # Overwrite default attributes. + port_attrs.update(attrs) + return type('Logical_Switch_Port', (object, ), port_attrs) + + @staticmethod + def from_neutron_port(port): + """Create a fake ovn port based on a neutron port.""" + external_ids = { + ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: + ovn_utils.ovn_name(port['network_id']), + ovn_const.OVN_SG_IDS_EXT_ID_KEY: + ' '.join(port['security_groups']), + ovn_const.OVN_DEVICE_OWNER_EXT_ID_KEY: + port.get('device_owner', '')} + addresses = [port['mac_address'], ] + addresses += [x['ip_address'] for x in port.get('fixed_ips', [])] + port_security = ( + addresses + [x['ip_address'] for x in + port.get('allowed_address_pairs', [])]) + return FakeOVNPort.create_one_port( + {'external_ids': external_ids, 'addresses': addresses, + 'port_security': port_security}) + + +FakeStaticRoute = collections.namedtuple( + 'Static_Routes', ['ip_prefix', 'nexthop', 'external_ids']) + + +class FakeOVNRouter(object): + + @staticmethod + def create_one_router(attrs=None): + router_attrs = { + 'enabled': False, + 'external_ids': {}, + 'load_balancer': [], + 'name': '', + 'nat': [], + 'options': {}, + 'ports': [], + 'static_routes': [], + } + + # Overwrite default attributes. + router_attrs.update(attrs) + return type('Logical_Router', (object, ), router_attrs) + + @staticmethod + def from_neutron_router(router): + + def _get_subnet_id(gw_info): + subnet_id = '' + ext_ips = gw_info.get('external_fixed_ips', []) + if ext_ips: + subnet_id = ext_ips[0]['subnet_id'] + return subnet_id + + external_ids = { + ovn_const.OVN_GW_PORT_EXT_ID_KEY: router.get('gw_port_id') or '', + ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: + router.get('name', 'no_router_name')} + + # Get the routes + routes = [] + for r in router.get('routes', []): + routes.append(FakeStaticRoute(ip_prefix=r['destination'], + nexthop=r['nexthop'], + external_ids={})) + + gw_info = router.get(l3.EXTERNAL_GW_INFO) + if gw_info: + external_ids = { + ovn_const.OVN_ROUTER_IS_EXT_GW: 'true', + ovn_const.OVN_SUBNET_EXT_ID_KEY: _get_subnet_id(gw_info)} + routes.append(FakeStaticRoute( + ip_prefix='0.0.0.0/0', nexthop='', + external_ids=external_ids)) + + return FakeOVNRouter.create_one_router( + {'external_ids': external_ids, + 'enabled': router.get('admin_state_up') or False, + 'name': ovn_utils.ovn_name(router['id']), + 'static_routes': routes}) diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/__init__.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/__init__.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/__init__.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_commands.py b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_commands.py new file mode 100644 index 00000000000..48b7a07a4b0 --- /dev/null +++ b/neutron/tests/unit/plugins/ml2/drivers/ovn/mech_driver/ovsdb/test_commands.py @@ -0,0 +1,1428 @@ +# +# 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 mock +from ovsdbapp.backend.ovs_idl import idlutils + +from neutron.common.ovn import constants as ovn_const +from neutron.common.ovn import exceptions as ovn_exc +from neutron.plugins.ml2.drivers.ovn.mech_driver.ovsdb import commands +from neutron.tests import base +from neutron.tests.unit import fake_resources as fakes + + +class TestBaseCommandHelpers(base.BaseTestCase): + def setUp(self): + super(TestBaseCommandHelpers, self).setUp() + self.column = 'ovn' + self.new_value = '1' + self.old_value = '2' + + def _get_fake_row_mutate(self): + return fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={self.column: []}) + + def test__addvalue_to_list(self): + fake_row_mutate = self._get_fake_row_mutate() + commands._addvalue_to_list( + fake_row_mutate, self.column, self.new_value) + fake_row_mutate.addvalue.assert_called_once_with( + self.column, self.new_value) + fake_row_mutate.verify.assert_not_called() + + def test__delvalue_from_list(self): + fake_row_mutate = self._get_fake_row_mutate() + commands._delvalue_from_list( + fake_row_mutate, self.column, self.old_value) + fake_row_mutate.delvalue.assert_called_once_with( + self.column, self.old_value) + fake_row_mutate.verify.assert_not_called() + + def test__updatevalues_in_list_none(self): + fake_row_mutate = self._get_fake_row_mutate() + commands._updatevalues_in_list(fake_row_mutate, self.column) + fake_row_mutate.addvalue.assert_not_called() + fake_row_mutate.delvalue.assert_not_called() + fake_row_mutate.verify.assert_not_called() + + def test__updatevalues_in_list_empty(self): + fake_row_mutate = self._get_fake_row_mutate() + commands._updatevalues_in_list(fake_row_mutate, self.column, [], []) + fake_row_mutate.addvalue.assert_not_called() + fake_row_mutate.delvalue.assert_not_called() + fake_row_mutate.verify.assert_not_called() + + def test__updatevalues_in_list(self): + fake_row_mutate = self._get_fake_row_mutate() + commands._updatevalues_in_list( + fake_row_mutate, self.column, + new_values=[self.new_value], + old_values=[self.old_value]) + fake_row_mutate.addvalue.assert_called_once_with( + self.column, self.new_value) + fake_row_mutate.delvalue.assert_called_once_with( + self.column, self.old_value) + fake_row_mutate.verify.assert_not_called() + + +class TestBaseCommand(base.BaseTestCase): + def setUp(self): + super(TestBaseCommand, self).setUp() + self.ovn_api = fakes.FakeOvsdbNbOvnIdl() + self.transaction = fakes.FakeOvsdbTransaction() + self.ovn_api.transaction = self.transaction + + +class TestCheckLivenessCommand(TestBaseCommand): + def test_check_liveness(self): + old_ng_cfg = self.ovn_api.nb_global.ng_cfg + cmd = commands.CheckLivenessCommand(self.ovn_api) + cmd.run_idl(self.transaction) + self.assertNotEqual(cmd.result, old_ng_cfg) + + +class TestLSwitchSetExternalIdsCommand(TestBaseCommand): + + def _test_lswitch_extid_update_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.LSwitchSetExternalIdsCommand( + self.ovn_api, 'fake-lswitch', + {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: 'neutron-network'}, + if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lswitch_no_exist_ignore(self): + self._test_lswitch_extid_update_no_exist(if_exists=True) + + def test_lswitch_no_exist_fail(self): + self._test_lswitch_extid_update_no_exist(if_exists=False) + + def test_lswitch_extid_update(self): + network_name = 'private' + new_network_name = 'private-new' + ext_ids = {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: network_name} + new_ext_ids = {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: new_network_name} + fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': ext_ids}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lswitch): + cmd = commands.LSwitchSetExternalIdsCommand( + self.ovn_api, fake_lswitch.name, + {ovn_const.OVN_NETWORK_NAME_EXT_ID_KEY: new_network_name}, + if_exists=True) + cmd.run_idl(self.transaction) + self.assertEqual(new_ext_ids, fake_lswitch.external_ids) + + +class TestAddLSwitchPortCommand(TestBaseCommand): + + def test_lswitch_not_found(self): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.AddLSwitchPortCommand( + self.ovn_api, 'fake-lsp', 'fake-lswitch', may_exist=True) + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + self.transaction.insert.assert_not_called() + + def test_lswitch_port_exists(self): + with mock.patch.object(idlutils, 'row_by_value', + return_value=mock.ANY): + cmd = commands.AddLSwitchPortCommand( + self.ovn_api, 'fake-lsp', 'fake-lswitch', may_exist=True) + cmd.run_idl(self.transaction) + self.transaction.insert.assert_not_called() + + def test_lswitch_port_add_exists(self): + fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row() + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lswitch): + fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row() + self.ovn_api._tables['Logical_Switch_Port'].rows[fake_lsp.uuid] = \ + fake_lsp + self.transaction.insert.return_value = fake_lsp + cmd = commands.AddLSwitchPortCommand( + self.ovn_api, fake_lsp.name, fake_lswitch.name, + may_exist=False) + cmd.run_idl(self.transaction) + # NOTE(rtheis): Mocking the transaction allows this insert + # to succeed when it normally would fail due the duplicate name. + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['Logical_Switch_Port']) + + def _test_lswitch_port_add(self, may_exist=True): + lsp_name = 'fake-lsp' + fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row() + with mock.patch.object(idlutils, 'row_by_value', + side_effect=[fake_lswitch, None]): + fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'foo': None}) + self.transaction.insert.return_value = fake_lsp + cmd = commands.AddLSwitchPortCommand( + self.ovn_api, lsp_name, fake_lswitch.name, + may_exist=may_exist, foo='bar') + cmd.run_idl(self.transaction) + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['Logical_Switch_Port']) + fake_lswitch.addvalue.assert_called_once_with( + 'ports', fake_lsp.uuid) + self.assertEqual(lsp_name, fake_lsp.name) + self.assertEqual('bar', fake_lsp.foo) + + def test_lswitch_port_add_may_exist(self): + self._test_lswitch_port_add(may_exist=True) + + def test_lswitch_port_add_ignore_exists(self): + self._test_lswitch_port_add(may_exist=False) + + def _test_lswitch_port_add_with_dhcp(self, dhcpv4_opts, dhcpv6_opts): + lsp_name = 'fake-lsp' + fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row() + fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row() + self.transaction.insert.return_value = fake_lsp + with mock.patch.object(idlutils, 'row_by_value', + side_effect=[fake_lswitch, None]): + cmd = commands.AddLSwitchPortCommand( + self.ovn_api, lsp_name, fake_lswitch.name, + may_exist=True, dhcpv4_options=dhcpv4_opts, + dhcpv6_options=dhcpv6_opts) + if not isinstance(dhcpv4_opts, list): + dhcpv4_opts.result = 'fake-uuid-1' + if not isinstance(dhcpv6_opts, list): + dhcpv6_opts.result = 'fake-uuid-2' + self.transaction.insert.reset_mock() + cmd.run_idl(self.transaction) + self.transaction.insert.assert_called_once_with( + self.ovn_api.lsp_table) + fake_lswitch.addvalue.assert_called_once_with( + 'ports', fake_lsp.uuid) + self.assertEqual(lsp_name, fake_lsp.name) + if isinstance(dhcpv4_opts, list): + self.assertEqual(dhcpv4_opts, fake_lsp.dhcpv4_options) + else: + self.assertEqual(['fake-uuid-1'], fake_lsp.dhcpv4_options) + if isinstance(dhcpv6_opts, list): + self.assertEqual(dhcpv6_opts, fake_lsp.dhcpv6_options) + else: + self.assertEqual(['fake-uuid-2'], fake_lsp.dhcpv6_options) + + def test_lswitch_port_add_with_dhcp(self): + dhcpv4_opts_cmd = commands.AddDHCPOptionsCommand( + self.ovn_api, mock.ANY, port_id=mock.ANY) + dhcpv6_opts_cmd = commands.AddDHCPOptionsCommand( + self.ovn_api, mock.ANY, port_id=mock.ANY) + for dhcpv4_opts in ([], ['fake-uuid-1'], dhcpv4_opts_cmd): + for dhcpv6_opts in ([], ['fake-uuid-2'], dhcpv6_opts_cmd): + self._test_lswitch_port_add_with_dhcp(dhcpv4_opts, dhcpv6_opts) + + +class TestSetLSwitchPortCommand(TestBaseCommand): + + def _test_lswitch_port_update_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.SetLSwitchPortCommand( + self.ovn_api, 'fake-lsp', if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lswitch_port_no_exist_ignore(self): + self._test_lswitch_port_update_no_exist(if_exists=True) + + def test_lswitch_port_no_exist_fail(self): + self._test_lswitch_port_update_no_exist(if_exists=False) + + def test_lswitch_port_update(self): + ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test'} + new_ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test-new'} + fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': ext_ids}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lsp): + cmd = commands.SetLSwitchPortCommand( + self.ovn_api, fake_lsp.name, if_exists=True, + external_ids=new_ext_ids) + cmd.run_idl(self.transaction) + self.assertEqual(new_ext_ids, fake_lsp.external_ids) + + def _test_lswitch_port_update_del_dhcp(self, clear_v4_opts, + clear_v6_opts, set_v4_opts=False, + set_v6_opts=False): + ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test'} + dhcp_options_tbl = self.ovn_api._tables['DHCP_Options'] + fake_dhcpv4_opts = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': {'port_id': 'fake-lsp'}}) + dhcp_options_tbl.rows[fake_dhcpv4_opts.uuid] = fake_dhcpv4_opts + fake_dhcpv6_opts = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': {'port_id': 'fake-lsp'}}) + dhcp_options_tbl.rows[fake_dhcpv6_opts.uuid] = fake_dhcpv6_opts + fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'name': 'fake-lsp', + 'external_ids': ext_ids, + 'dhcpv4_options': [fake_dhcpv4_opts], + 'dhcpv6_options': [fake_dhcpv6_opts]}) + + columns = {} + if clear_v4_opts: + columns['dhcpv4_options'] = [] + elif set_v4_opts: + columns['dhcpv4_options'] = [fake_dhcpv4_opts.uuid] + if clear_v6_opts: + columns['dhcpv6_options'] = [] + elif set_v6_opts: + columns['dhcpv6_options'] = [fake_dhcpv6_opts.uuid] + + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lsp): + cmd = commands.SetLSwitchPortCommand( + self.ovn_api, fake_lsp.name, if_exists=True, **columns) + cmd.run_idl(self.transaction) + + if clear_v4_opts and clear_v6_opts: + fake_dhcpv4_opts.delete.assert_called_once_with() + fake_dhcpv6_opts.delete.assert_called_once_with() + elif clear_v4_opts: + # not clear_v6_opts and set_v6_opts is any + fake_dhcpv4_opts.delete.assert_called_once_with() + fake_dhcpv6_opts.delete.assert_not_called() + elif clear_v6_opts: + # not clear_v4_opts and set_v6_opts is any + fake_dhcpv4_opts.delete.assert_not_called() + fake_dhcpv6_opts.delete.assert_called_once_with() + else: + # not clear_v4_opts and not clear_v6_opts and + # set_v4_opts is any and set_v6_opts is any + fake_dhcpv4_opts.delete.assert_not_called() + fake_dhcpv6_opts.delete.assert_not_called() + + def test_lswitch_port_update_del_port_dhcpv4_options(self): + self._test_lswitch_port_update_del_dhcp(True, False) + + def test_lswitch_port_update_del_port_dhcpv6_options(self): + self._test_lswitch_port_update_del_dhcp(False, True) + + def test_lswitch_port_update_del_all_port_dhcp_options(self): + self._test_lswitch_port_update_del_dhcp(True, True) + + def test_lswitch_port_update_del_no_port_dhcp_options(self): + self._test_lswitch_port_update_del_dhcp(False, False) + + def test_lswitch_port_update_set_port_dhcpv4_options(self): + self._test_lswitch_port_update_del_dhcp(False, True, set_v4_opts=True) + + def test_lswitch_port_update_set_port_dhcpv6_options(self): + self._test_lswitch_port_update_del_dhcp(True, False, set_v6_opts=True) + + def test_lswitch_port_update_set_all_port_dhcp_options(self): + self._test_lswitch_port_update_del_dhcp(False, False, set_v4_opts=True, + set_v6_opts=True) + + def _test_lswitch_port_update_with_dhcp(self, dhcpv4_opts, dhcpv6_opts): + ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test'} + fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'name': 'fake-lsp', + 'external_ids': ext_ids, + 'dhcpv4_options': ['fake-v4-subnet-dhcp-opt'], + 'dhcpv6_options': ['fake-v6-subnet-dhcp-opt']}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lsp): + cmd = commands.SetLSwitchPortCommand( + self.ovn_api, fake_lsp.name, if_exists=True, + external_ids=ext_ids, dhcpv4_options=dhcpv4_opts, + dhcpv6_options=dhcpv6_opts) + if not isinstance(dhcpv4_opts, list): + dhcpv4_opts.result = 'fake-uuid-1' + if not isinstance(dhcpv6_opts, list): + dhcpv6_opts.result = 'fake-uuid-2' + cmd.run_idl(self.transaction) + if isinstance(dhcpv4_opts, list): + self.assertEqual(dhcpv4_opts, fake_lsp.dhcpv4_options) + else: + self.assertEqual(['fake-uuid-1'], fake_lsp.dhcpv4_options) + if isinstance(dhcpv6_opts, list): + self.assertEqual(dhcpv6_opts, fake_lsp.dhcpv6_options) + else: + self.assertEqual(['fake-uuid-2'], fake_lsp.dhcpv6_options) + + def test_lswitch_port_update_with_dhcp(self): + v4_dhcp_cmd = commands.AddDHCPOptionsCommand(self.ovn_api, mock.ANY, + port_id=mock.ANY) + v6_dhcp_cmd = commands.AddDHCPOptionsCommand(self.ovn_api, mock.ANY, + port_id=mock.ANY) + for dhcpv4_opts in ([], ['fake-v4-subnet-dhcp-opt'], v4_dhcp_cmd): + for dhcpv6_opts in ([], ['fake-v6-subnet-dhcp-opt'], v6_dhcp_cmd): + self._test_lswitch_port_update_with_dhcp( + dhcpv4_opts, dhcpv6_opts) + + +class TestDelLSwitchPortCommand(TestBaseCommand): + + def _test_lswitch_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=['fake-lsp', idlutils.RowNotFound]): + cmd = commands.DelLSwitchPortCommand( + self.ovn_api, 'fake-lsp', 'fake-lswitch', if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lswitch_no_exist_ignore(self): + self._test_lswitch_no_exist(if_exists=True) + + def test_lswitch_no_exist_fail(self): + self._test_lswitch_no_exist(if_exists=False) + + def _test_lswitch_port_del_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.DelLSwitchPortCommand( + self.ovn_api, 'fake-lsp', 'fake-lswitch', if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lswitch_port_no_exist_ignore(self): + self._test_lswitch_port_del_no_exist(if_exists=True) + + def test_lswitch_port_no_exist_fail(self): + self._test_lswitch_port_del_no_exist(if_exists=False) + + def test_lswitch_port_del(self): + fake_lsp = mock.MagicMock() + fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'ports': [fake_lsp]}) + self.ovn_api._tables['Logical_Switch_Port'].rows[fake_lsp.uuid] = \ + fake_lsp + with mock.patch.object(idlutils, 'row_by_value', + side_effect=[fake_lsp, fake_lswitch]): + cmd = commands.DelLSwitchPortCommand( + self.ovn_api, fake_lsp.name, fake_lswitch.name, if_exists=True) + cmd.run_idl(self.transaction) + fake_lswitch.delvalue.assert_called_once_with('ports', fake_lsp) + fake_lsp.delete.assert_called_once_with() + + def _test_lswitch_port_del_delete_dhcp_opt(self, dhcpv4_opt_ext_ids, + dhcpv6_opt_ext_ids): + ext_ids = {ovn_const.OVN_PORT_NAME_EXT_ID_KEY: 'test'} + fake_dhcpv4_options = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': dhcpv4_opt_ext_ids}) + self.ovn_api._tables['DHCP_Options'].rows[fake_dhcpv4_options.uuid] = \ + fake_dhcpv4_options + fake_dhcpv6_options = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': dhcpv6_opt_ext_ids}) + self.ovn_api._tables['DHCP_Options'].rows[fake_dhcpv6_options.uuid] = \ + fake_dhcpv6_options + fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'name': 'lsp', + 'external_ids': ext_ids, + 'dhcpv4_options': [fake_dhcpv4_options], + 'dhcpv6_options': [fake_dhcpv6_options]}) + self.ovn_api._tables['Logical_Switch_Port'].rows[fake_lsp.uuid] = \ + fake_lsp + fake_lswitch = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'ports': [fake_lsp]}) + with mock.patch.object(idlutils, 'row_by_value', + side_effect=[fake_lsp, fake_lswitch]): + cmd = commands.DelLSwitchPortCommand( + self.ovn_api, fake_lsp.name, fake_lswitch.name, if_exists=True) + cmd.run_idl(self.transaction) + fake_lswitch.delvalue.assert_called_once_with('ports', fake_lsp) + fake_lsp.delete.assert_called_once_with() + if 'port_id' in dhcpv4_opt_ext_ids: + fake_dhcpv4_options.delete.assert_called_once_with() + else: + fake_dhcpv4_options.delete.assert_not_called() + if 'port_id' in dhcpv6_opt_ext_ids: + fake_dhcpv6_options.delete.assert_called_once_with() + else: + fake_dhcpv6_options.delete.assert_not_called() + + def test_lswitch_port_del_delete_dhcp_opt(self): + for v4_ext_ids in ({'subnet_id': 'fake-ls0'}, + {'subnet_id': 'fake-ls0', 'port_id': 'lsp'}): + for v6_ext_ids in ({'subnet_id': 'fake-ls1'}, + {'subnet_id': 'fake-ls1', 'port_id': 'lsp'}): + self._test_lswitch_port_del_delete_dhcp_opt( + v4_ext_ids, v6_ext_ids) + + +class TestAddLRouterCommand(TestBaseCommand): + + def test_lrouter_exists(self): + with mock.patch.object(idlutils, 'row_by_value', + return_value=mock.ANY): + cmd = commands.AddLRouterCommand( + self.ovn_api, 'fake-lrouter', may_exist=True, + a='1', b='2') + cmd.run_idl(self.transaction) + self.transaction.insert.assert_not_called() + + def test_lrouter_add_exists(self): + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() + self.ovn_api._tables['Logical_Router'].rows[fake_lrouter.uuid] = \ + fake_lrouter + self.transaction.insert.return_value = fake_lrouter + cmd = commands.AddLRouterCommand( + self.ovn_api, fake_lrouter.name, may_exist=False) + cmd.run_idl(self.transaction) + # NOTE(rtheis): Mocking the transaction allows this insert + # to succeed when it normally would fail due the duplicate name. + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['Logical_Router']) + + def _test_lrouter_add(self, may_exist=True): + with mock.patch.object(idlutils, 'row_by_value', + return_value=None): + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() + self.transaction.insert.return_value = fake_lrouter + cmd = commands.AddLRouterCommand( + self.ovn_api, 'fake-lrouter', may_exist=may_exist, + a='1', b='2') + cmd.run_idl(self.transaction) + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['Logical_Router']) + self.assertEqual('fake-lrouter', fake_lrouter.name) + self.assertEqual('1', fake_lrouter.a) + self.assertEqual('2', fake_lrouter.b) + + def test_lrouter_add_may_exist(self): + self._test_lrouter_add(may_exist=True) + + def test_lrouter_add_ignore_exists(self): + self._test_lrouter_add(may_exist=False) + + +class TestUpdateLRouterCommand(TestBaseCommand): + + def _test_lrouter_update_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.UpdateLRouterCommand( + self.ovn_api, 'fake-lrouter', if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lrouter_no_exist_ignore(self): + self._test_lrouter_update_no_exist(if_exists=True) + + def test_lrouter_no_exist_fail(self): + self._test_lrouter_update_no_exist(if_exists=False) + + def test_lrouter_update(self): + ext_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'richard'} + new_ext_ids = {ovn_const.OVN_ROUTER_NAME_EXT_ID_KEY: 'richard-new'} + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': ext_ids}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + cmd = commands.UpdateLRouterCommand( + self.ovn_api, fake_lrouter.name, if_exists=True, + external_ids=new_ext_ids) + cmd.run_idl(self.transaction) + self.assertEqual(new_ext_ids, fake_lrouter.external_ids) + + +class TestDelLRouterCommand(TestBaseCommand): + + def _test_lrouter_del_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.DelLRouterCommand( + self.ovn_api, 'fake-lrouter', if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lrouter_no_exist_ignore(self): + self._test_lrouter_del_no_exist(if_exists=True) + + def test_lrouter_no_exist_fail(self): + self._test_lrouter_del_no_exist(if_exists=False) + + def test_lrouter_del(self): + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() + self.ovn_api._tables['Logical_Router'].rows[fake_lrouter.uuid] = \ + fake_lrouter + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + cmd = commands.DelLRouterCommand( + self.ovn_api, fake_lrouter.name, if_exists=True) + cmd.run_idl(self.transaction) + fake_lrouter.delete.assert_called_once_with() + + +class TestAddLRouterPortCommand(TestBaseCommand): + + def test_lrouter_not_found(self): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.AddLRouterPortCommand( + self.ovn_api, 'fake-lrp', 'fake-lrouter', may_exist=False) + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + self.transaction.insert.assert_not_called() + + def test_lrouter_port_exists(self): + with mock.patch.object(idlutils, 'row_by_value', + return_value=mock.ANY): + cmd = commands.AddLRouterPortCommand( + self.ovn_api, 'fake-lrp', 'fake-lrouter', may_exist=False) + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + self.transaction.insert.assert_not_called() + + def test_lrouter_port_may_exist(self): + with mock.patch.object(idlutils, 'row_by_value', + return_value=mock.ANY): + cmd = commands.AddLRouterPortCommand( + self.ovn_api, 'fake-lrp', 'fake-lrouter', may_exist=True) + cmd.run_idl(self.transaction) + self.transaction.insert.assert_not_called() + + def test_lrouter_port_add(self): + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() + with mock.patch.object(idlutils, 'row_by_value', + side_effect=[fake_lrouter, + idlutils.RowNotFound]): + fake_lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'foo': None}) + self.transaction.insert.return_value = fake_lrp + cmd = commands.AddLRouterPortCommand( + self.ovn_api, 'fake-lrp', fake_lrouter.name, may_exist=False, + foo='bar') + cmd.run_idl(self.transaction) + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['Logical_Router_Port']) + self.assertEqual('fake-lrp', fake_lrp.name) + fake_lrouter.addvalue.assert_called_once_with('ports', fake_lrp) + self.assertEqual('bar', fake_lrp.foo) + + +class TestUpdateLRouterPortCommand(TestBaseCommand): + + def _test_lrouter_port_update_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.UpdateLRouterPortCommand( + self.ovn_api, 'fake-lrp', if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lrouter_port_no_exist_ignore(self): + self._test_lrouter_port_update_no_exist(if_exists=True) + + def test_lrouter_port_no_exist_fail(self): + self._test_lrouter_port_update_no_exist(if_exists=False) + + def test_lrouter_port_update(self): + old_networks = [] + new_networks = ['10.1.0.0/24'] + fake_lrp = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'networks': old_networks}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrp): + cmd = commands.UpdateLRouterPortCommand( + self.ovn_api, fake_lrp.name, if_exists=True, + networks=new_networks) + cmd.run_idl(self.transaction) + self.assertEqual(new_networks, fake_lrp.networks) + + +class TestDelLRouterPortCommand(TestBaseCommand): + + def _test_lrouter_port_del_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.DelLRouterPortCommand( + self.ovn_api, 'fake-lrp', 'fake-lrouter', if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lrouter_port_no_exist_ignore(self): + self._test_lrouter_port_del_no_exist(if_exists=True) + + def test_lrouter_port_no_exist_fail(self): + self._test_lrouter_port_del_no_exist(if_exists=False) + + def test_lrouter_no_exist(self): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=[mock.ANY, idlutils.RowNotFound]): + cmd = commands.DelLRouterPortCommand( + self.ovn_api, 'fake-lrp', 'fake-lrouter', if_exists=True) + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lrouter_port_del(self): + fake_lrp = mock.MagicMock() + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'ports': [fake_lrp]}) + self.ovn_api._tables['Logical_Router_Port'].rows[fake_lrp.uuid] = \ + fake_lrp + with mock.patch.object(idlutils, 'row_by_value', + side_effect=[fake_lrp, fake_lrouter]): + cmd = commands.DelLRouterPortCommand( + self.ovn_api, fake_lrp.name, fake_lrouter.name, if_exists=True) + cmd.run_idl(self.transaction) + fake_lrouter.delvalue.assert_called_once_with('ports', fake_lrp) + + +class TestSetLRouterPortInLSwitchPortCommand(TestBaseCommand): + + def test_lswitch_port_no_exist_fail(self): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.SetLRouterPortInLSwitchPortCommand( + self.ovn_api, 'fake-lsp', 'fake-lrp', False, False, 'router') + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lswitch_port_no_exist_do_not_fail(self): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.SetLRouterPortInLSwitchPortCommand( + self.ovn_api, 'fake-lsp', 'fake-lrp', False, True, 'router') + cmd.run_idl(self.transaction) + + def test_lswitch_port_router_update(self): + lrp_name = 'fake-lrp' + fake_lsp = fakes.FakeOvsdbRow.create_one_ovsdb_row() + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lsp): + cmd = commands.SetLRouterPortInLSwitchPortCommand( + self.ovn_api, fake_lsp.name, lrp_name, True, True, 'router') + cmd.run_idl(self.transaction) + self.assertEqual({'router-port': lrp_name, + 'nat-addresses': 'router'}, fake_lsp.options) + self.assertEqual('router', fake_lsp.type) + self.assertEqual('router', fake_lsp.addresses) + + +class TestAddStaticRouteCommand(TestBaseCommand): + + def test_lrouter_not_found(self): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.AddStaticRouteCommand(self.ovn_api, 'fake-lrouter') + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + self.transaction.insert.assert_not_called() + + def test_static_route_add(self): + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + fake_static_route = fakes.FakeOvsdbRow.create_one_ovsdb_row() + self.transaction.insert.return_value = fake_static_route + cmd = commands.AddStaticRouteCommand( + self.ovn_api, fake_lrouter.name, + nexthop='40.0.0.100', + ip_prefix='30.0.0.0/24') + cmd.run_idl(self.transaction) + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['Logical_Router_Static_Route']) + self.assertEqual('40.0.0.100', fake_static_route.nexthop) + self.assertEqual('30.0.0.0/24', fake_static_route.ip_prefix) + fake_lrouter.addvalue.assert_called_once_with( + 'static_routes', fake_static_route.uuid) + + +class TestDelStaticRouteCommand(TestBaseCommand): + + def _test_lrouter_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.DelStaticRouteCommand( + self.ovn_api, 'fake-lrouter', + '30.0.0.0/24', '40.0.0.100', + if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_lrouter_no_exist_ignore(self): + self._test_lrouter_no_exist(if_exists=True) + + def test_lrouter_no_exist_fail(self): + self._test_lrouter_no_exist(if_exists=False) + + def test_static_route_del(self): + fake_static_route = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'ip_prefix': '50.0.0.0/24', 'nexthop': '40.0.0.101'}) + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'static_routes': [fake_static_route]}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + cmd = commands.DelStaticRouteCommand( + self.ovn_api, fake_lrouter.name, + fake_static_route.ip_prefix, fake_static_route.nexthop, + if_exists=True) + cmd.run_idl(self.transaction) + fake_lrouter.delvalue.assert_called_once_with( + 'static_routes', mock.ANY) + + def test_static_route_del_not_found(self): + fake_static_route1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'ip_prefix': '50.0.0.0/24', 'nexthop': '40.0.0.101'}) + fake_static_route2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'ip_prefix': '60.0.0.0/24', 'nexthop': '70.0.0.101'}) + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'static_routes': [fake_static_route2]}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + cmd = commands.DelStaticRouteCommand( + self.ovn_api, fake_lrouter.name, + fake_static_route1.ip_prefix, fake_static_route1.nexthop, + if_exists=True) + cmd.run_idl(self.transaction) + fake_lrouter.delvalue.assert_not_called() + self.assertEqual([mock.ANY], fake_lrouter.static_routes) + + +class TestAddAddrSetCommand(TestBaseCommand): + + def test_addrset_exists(self): + with mock.patch.object(idlutils, 'row_by_value', + return_value=mock.ANY): + cmd = commands.AddAddrSetCommand( + self.ovn_api, 'fake-addrset', may_exist=True) + cmd.run_idl(self.transaction) + self.transaction.insert.assert_not_called() + + def test_addrset_add_exists(self): + fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row() + self.ovn_api._tables['Address_Set'].rows[fake_addrset.uuid] = \ + fake_addrset + self.transaction.insert.return_value = fake_addrset + cmd = commands.AddAddrSetCommand( + self.ovn_api, fake_addrset.name, may_exist=False) + cmd.run_idl(self.transaction) + # NOTE(rtheis): Mocking the transaction allows this insert + # to succeed when it normally would fail due the duplicate name. + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['Address_Set']) + + def _test_addrset_add(self, may_exist=True): + with mock.patch.object(idlutils, 'row_by_value', + return_value=None): + fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'foo': ''}) + self.transaction.insert.return_value = fake_addrset + cmd = commands.AddAddrSetCommand( + self.ovn_api, 'fake-addrset', may_exist=may_exist, + foo='bar') + cmd.run_idl(self.transaction) + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['Address_Set']) + self.assertEqual('fake-addrset', fake_addrset.name) + self.assertEqual('bar', fake_addrset.foo) + + def test_addrset_add_may_exist(self): + self._test_addrset_add(may_exist=True) + + def test_addrset_add_ignore_exists(self): + self._test_addrset_add(may_exist=False) + + +class TestDelAddrSetCommand(TestBaseCommand): + + def _test_addrset_del_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.DelAddrSetCommand( + self.ovn_api, 'fake-addrset', if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_addrset_no_exist_ignore(self): + self._test_addrset_del_no_exist(if_exists=True) + + def test_addrset_no_exist_fail(self): + self._test_addrset_del_no_exist(if_exists=False) + + def test_addrset_del(self): + fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row() + self.ovn_api._tables['Address_Set'].rows[fake_addrset.uuid] = \ + fake_addrset + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_addrset): + cmd = commands.DelAddrSetCommand( + self.ovn_api, fake_addrset.name, if_exists=True) + cmd.run_idl(self.transaction) + fake_addrset.delete.assert_called_once_with() + + +class TestUpdateAddrSetCommand(TestBaseCommand): + + def _test_addrset_update_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.UpdateAddrSetCommand( + self.ovn_api, 'fake-addrset', + addrs_add=[], addrs_remove=[], + if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_addrset_no_exist_ignore(self): + self._test_addrset_update_no_exist(if_exists=True) + + def test_addrset_no_exist_fail(self): + self._test_addrset_update_no_exist(if_exists=False) + + def _test_addrset_update(self, addrs_add=None, addrs_del=None): + save_address = '10.0.0.1' + initial_addresses = [save_address] + final_addresses = [save_address] + expected_addvalue_calls = [] + expected_delvalue_calls = [] + if addrs_add: + for addr_add in addrs_add: + final_addresses.append(addr_add) + expected_addvalue_calls.append( + mock.call('addresses', addr_add)) + if addrs_del: + for addr_del in addrs_del: + initial_addresses.append(addr_del) + expected_delvalue_calls.append( + mock.call('addresses', addr_del)) + fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'addresses': initial_addresses}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_addrset): + cmd = commands.UpdateAddrSetCommand( + self.ovn_api, fake_addrset.name, + addrs_add=addrs_add, addrs_remove=addrs_del, + if_exists=True) + cmd.run_idl(self.transaction) + fake_addrset.addvalue.assert_has_calls(expected_addvalue_calls) + fake_addrset.delvalue.assert_has_calls(expected_delvalue_calls) + + def test_addrset_update_add(self): + self._test_addrset_update(addrs_add=['10.0.0.4']) + + def test_addrset_update_del(self): + self._test_addrset_update(addrs_del=['10.0.0.2']) + + +class TestUpdateAddrSetExtIdsCommand(TestBaseCommand): + def setUp(self): + super(TestUpdateAddrSetExtIdsCommand, self).setUp() + self.ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: 'default'} + + def _test_addrset_extids_update_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.UpdateAddrSetExtIdsCommand( + self.ovn_api, 'fake-addrset', self.ext_ids, + if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_addrset_no_exist_ignore(self): + self._test_addrset_extids_update_no_exist(if_exists=True) + + def test_addrset_no_exist_fail(self): + self._test_addrset_extids_update_no_exist(if_exists=False) + + def test_addrset_extids_update(self): + new_ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: 'default-new'} + fake_addrset = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': self.ext_ids}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_addrset): + cmd = commands.UpdateAddrSetExtIdsCommand( + self.ovn_api, fake_addrset.name, + new_ext_ids, if_exists=True) + cmd.run_idl(self.transaction) + self.assertEqual(new_ext_ids, fake_addrset.external_ids) + + +class TestUpdateChassisExtIdsCommand(TestBaseCommand): + def setUp(self): + super(TestUpdateChassisExtIdsCommand, self).setUp() + self.ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: 'default'} + + def _test_chassis_extids_update_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.UpdateChassisExtIdsCommand( + self.ovn_api, 'fake-chassis', self.ext_ids, + if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_chassis_no_exist_ignore(self): + self._test_chassis_extids_update_no_exist(if_exists=True) + + def test_chassis_no_exist_fail(self): + self._test_chassis_extids_update_no_exist(if_exists=False) + + def test_chassis_extids_update(self): + new_ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: 'default-new'} + fake_chassis = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': self.ext_ids}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_chassis): + cmd = commands.UpdateChassisExtIdsCommand( + self.ovn_api, fake_chassis.name, + new_ext_ids, if_exists=True) + cmd.run_idl(self.transaction) + self.assertEqual(new_ext_ids, fake_chassis.external_ids) + + +class TestUpdatePortBindingExtIdsCommand(TestBaseCommand): + def setUp(self): + super(TestUpdatePortBindingExtIdsCommand, self).setUp() + self.ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: 'default'} + + def _test_portbinding_extids_update_no_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.UpdatePortBindingExtIdsCommand( + self.ovn_api, 'fake-portbinding', self.ext_ids, + if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_portbinding_no_exist_ignore(self): + self._test_portbinding_extids_update_no_exist(if_exists=True) + + def test_portbinding_no_exist_fail(self): + self._test_portbinding_extids_update_no_exist(if_exists=False) + + def test_portbinding_extids_update(self): + new_ext_ids = {ovn_const.OVN_SG_EXT_ID_KEY: 'default-new'} + fake_portbinding = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': self.ext_ids}) + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_portbinding): + cmd = commands.UpdatePortBindingExtIdsCommand( + self.ovn_api, fake_portbinding.name, + new_ext_ids, if_exists=True) + cmd.run_idl(self.transaction) + self.assertEqual(new_ext_ids, fake_portbinding.external_ids) + + +class TestAddDHCPOptionsCommand(TestBaseCommand): + + def test_dhcp_options_exists(self): + fake_ext_ids = {'subnet_id': 'fake-subnet-id', + 'port_id': 'fake-port-id'} + fake_dhcp_options = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': fake_ext_ids}) + self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_options.uuid] = \ + fake_dhcp_options + cmd = commands.AddDHCPOptionsCommand( + self.ovn_api, fake_ext_ids['subnet_id'], fake_ext_ids['port_id'], + may_exist=True, external_ids=fake_ext_ids) + cmd.run_idl(self.transaction) + self.transaction.insert.assert_not_called() + self.assertEqual(fake_ext_ids, fake_dhcp_options.external_ids) + + def _test_dhcp_options_add(self, may_exist=True): + fake_subnet_id = 'fake-subnet-id-' + str(may_exist) + fake_port_id = 'fake-port-id-' + str(may_exist) + fake_ext_ids1 = {'subnet_id': fake_subnet_id, 'port_id': fake_port_id} + fake_dhcp_options1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': fake_ext_ids1}) + self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_options1.uuid] = \ + fake_dhcp_options1 + fake_ext_ids2 = {'subnet_id': fake_subnet_id} + fake_dhcp_options2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': fake_ext_ids2}) + fake_dhcp_options3 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': {'subnet_id': 'nomatch'}}) + self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_options3.uuid] = \ + fake_dhcp_options3 + self.transaction.insert.return_value = fake_dhcp_options2 + cmd = commands.AddDHCPOptionsCommand( + self.ovn_api, fake_ext_ids2['subnet_id'], may_exist=may_exist, + external_ids=fake_ext_ids2) + cmd.run_idl(self.transaction) + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['DHCP_Options']) + self.assertEqual(fake_ext_ids2, fake_dhcp_options2.external_ids) + + def test_dhcp_options_add_may_exist(self): + self._test_dhcp_options_add(may_exist=True) + + def test_dhcp_options_add_ignore_exists(self): + self._test_dhcp_options_add(may_exist=False) + + def _test_dhcp_options_update_result(self, new_insert=False): + fake_ext_ids = {'subnet_id': 'fake_subnet', 'port_id': 'fake_port'} + fake_dhcp_opts = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': fake_ext_ids}) + if new_insert: + self.transaction.insert.return_value = fake_dhcp_opts + self.transaction.get_insert_uuid = mock.Mock( + return_value='fake-uuid') + else: + self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_opts.uuid] = \ + fake_dhcp_opts + self.transaction.get_insert_uuid = mock.Mock( + return_value=None) + + cmd = commands.AddDHCPOptionsCommand( + self.ovn_api, fake_ext_ids['subnet_id'], + port_id=fake_ext_ids['port_id'], may_exist=True, + external_ids=fake_ext_ids) + cmd.run_idl(self.transaction) + cmd.post_commit(self.transaction) + if new_insert: + self.assertEqual('fake-uuid', cmd.result) + else: + self.assertEqual(fake_dhcp_opts.uuid, cmd.result) + + def test_dhcp_options_update_result_with_exist_row(self): + self._test_dhcp_options_update_result(new_insert=False) + + def test_dhcp_options_update_result_with_new_row(self): + self._test_dhcp_options_update_result(new_insert=True) + + +class TestDelDHCPOptionsCommand(TestBaseCommand): + + def _test_dhcp_options_del_no_exist(self, if_exists=True): + cmd = commands.DelDHCPOptionsCommand( + self.ovn_api, 'fake-dhcp-options', if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_dhcp_options_no_exist_ignore(self): + self._test_dhcp_options_del_no_exist(if_exists=True) + + def test_dhcp_options_no_exist_fail(self): + self._test_dhcp_options_del_no_exist(if_exists=False) + + def test_dhcp_options_del(self): + fake_dhcp_options = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': {'subnet_id': 'fake-subnet-id'}}) + self.ovn_api._tables['DHCP_Options'].rows[fake_dhcp_options.uuid] = \ + fake_dhcp_options + cmd = commands.DelDHCPOptionsCommand( + self.ovn_api, fake_dhcp_options.uuid, if_exists=True) + cmd.run_idl(self.transaction) + fake_dhcp_options.delete.assert_called_once_with() + + +class TestAddNATRuleInLRouterCommand(TestBaseCommand): + + def test_add_nat_rule(self): + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() + fake_nat_rule_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.10', + 'logical_ip': '10.0.0.4', 'type': 'dnat_and_snat'}) + fake_nat_rule_2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.8', + 'logical_ip': '10.0.0.5', 'type': 'dnat_and_snat'}) + fake_lrouter.nat = [fake_nat_rule_1, fake_nat_rule_2] + self.ovn_api._tables['NAT'].rows[fake_nat_rule_1.uuid] = \ + fake_nat_rule_1 + self.ovn_api._tables['NAT'].rows[fake_nat_rule_2.uuid] = \ + fake_nat_rule_2 + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + cmd = commands.AddNATRuleInLRouterCommand( + self.ovn_api, fake_lrouter.name) + cmd.run_idl(self.transaction) + self.transaction.insert.assert_called_once_with( + self.ovn_api._tables['NAT']) + # a UUID will have been appended + self.assertEqual(3, len(fake_lrouter.nat)) + self.assertIn(fake_nat_rule_1, fake_lrouter.nat) + self.assertIn(fake_nat_rule_2, fake_lrouter.nat) + + def test_add_nat_rule_no_lrouter_exist(self): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.AddNATRuleInLRouterCommand( + self.ovn_api, "fake-lrouter") + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + +class TestDeleteNATRuleInLRouterCommand(TestBaseCommand): + + def test_delete_nat_rule(self): + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() + fake_nat_rule_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.10', + 'logical_ip': '10.0.0.4', 'type': 'dnat_and_snat'}) + fake_nat_rule_2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.8', + 'logical_ip': '10.0.0.5', 'type': 'dnat_and_snat'}) + fake_lrouter.nat = [fake_nat_rule_1, fake_nat_rule_2] + self.ovn_api._tables['NAT'].rows[fake_nat_rule_1.uuid] = \ + fake_nat_rule_1 + self.ovn_api._tables['NAT'].rows[fake_nat_rule_2.uuid] = \ + fake_nat_rule_2 + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + cmd = commands.DeleteNATRuleInLRouterCommand( + self.ovn_api, fake_lrouter.name, fake_nat_rule_1.type, + fake_nat_rule_1.logical_ip, fake_nat_rule_1.external_ip, + False) + cmd.run_idl(self.transaction) + fake_nat_rule_1.delete.assert_called_once_with() + self.assertEqual(1, len(fake_lrouter.nat)) + self.assertNotIn(fake_nat_rule_1, fake_lrouter.nat) + self.assertIn(fake_nat_rule_2, fake_lrouter.nat) + + # run again with same arguments, should not remove anything + fake_nat_rule_1.delete.reset_mock() + cmd.run_idl(self.transaction) + fake_nat_rule_1.delete.assert_not_called() + self.assertEqual(1, len(fake_lrouter.nat)) + self.assertNotIn(fake_nat_rule_1, fake_lrouter.nat) + self.assertIn(fake_nat_rule_2, fake_lrouter.nat) + + def _test_delete_nat_rule_no_lrouter_exist(self, if_exists=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.DeleteNATRuleInLRouterCommand( + self.ovn_api, "fake-lrouter", "fake-type", "fake-logical-ip", + "fake-external-ip", if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + def test_delete_nat_rule_no_lrouter_exist_ignore(self): + self._test_delete_nat_rule_no_lrouter_exist(if_exists=True) + + def test_delete_nat_rule_no_lrouter_exist_fail(self): + self._test_delete_nat_rule_no_lrouter_exist(if_exists=False) + + +class TestSetNATRuleInLRouterCommand(TestBaseCommand): + + def test_set_nat_rule(self): + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row() + fake_nat_rule_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.10', + 'logical_ip': '10.0.0.4', 'type': 'dnat_and_snat'}) + fake_nat_rule_2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.8', + 'logical_ip': '10.0.0.5', 'type': 'dnat_and_snat'}) + fake_lrouter.nat = [fake_nat_rule_1, fake_nat_rule_2] + self.ovn_api._tables['NAT'].rows[fake_nat_rule_1.uuid] = \ + fake_nat_rule_1 + self.ovn_api._tables['NAT'].rows[fake_nat_rule_2.uuid] = \ + fake_nat_rule_2 + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + cmd = commands.SetNATRuleInLRouterCommand( + self.ovn_api, fake_lrouter.name, fake_nat_rule_1.uuid, + logical_ip='10.0.0.10') + cmd.run_idl(self.transaction) + self.assertEqual('10.0.0.10', fake_nat_rule_1.logical_ip) + self.assertEqual('10.0.0.5', fake_nat_rule_2.logical_ip) + + def test_set_nat_rule_no_lrouter_exist(self): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.SetNATRuleInLRouterCommand( + self.ovn_api, "fake-lrouter", "fake-uuid", + logical_ip='fake-ip') + self.assertRaises(RuntimeError, cmd.run_idl, self.transaction) + + +class TestCheckRevisionNumberCommand(TestBaseCommand): + def setUp(self): + super(TestCheckRevisionNumberCommand, self).setUp() + self.fip = {'name': 'floating-ip', 'revision_number': 3} + self.fip_old_rev = {'name': 'floating-ip', 'revision_number': 1} + self.nat_rule = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.10', 'name': 'floating-ip', + 'logical_ip': '10.0.0.4', 'type': 'dnat_and_snat', + 'external_ids': + {ovn_const.OVN_FIP_EXT_ID_KEY: 'floating-ip', + ovn_const.OVN_REV_NUM_EXT_ID_KEY: 3}}) + bad_nat_rule = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.11', + 'logical_ip': '10.0.0.5', 'type': 'bad_type'}) + self.ovn_api._tables['NAT'].rows[self.nat_rule.uuid] = self.nat_rule + self.ovn_api._tables['NAT'].rows[bad_nat_rule.uuid] = bad_nat_rule + + self.subnet = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': {'subnet_id': 'mysubnet'}}) + bad_subnet = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': {'port_id': 'fake-lsp'}}) + self.ovn_api._tables['DHCP_Options'].rows[self.subnet.uuid] = \ + self.subnet + self.ovn_api._tables['DHCP_Options'].rows[bad_subnet.uuid] = \ + bad_subnet + + def _test_check_revision_number( + self, name='fake-name', resource='fake-resource', + resource_type=ovn_const.TYPE_NETWORKS, if_exists=True, + revision_conflict=False): + with mock.patch.object(self.ovn_api, "is_col_present", + return_value=True): + with mock.patch.object(self.ovn_api, 'lookup', + side_effect=idlutils.RowNotFound): + cmd = commands.CheckRevisionNumberCommand( + self.ovn_api, name, resource, resource_type, + if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + elif revision_conflict: + self.assertRaises(ovn_exc.RevisionConflict, cmd.run_idl, + self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, + self.transaction) + + def test_check_revision_number_no_exist_ignore(self): + self._test_check_revision_number(if_exists=True) + + def test_check_revision_number_no_exist_fail(self): + self._test_check_revision_number(if_exists=False) + + def test_check_revision_number_floating_ip(self): + self._test_check_revision_number( + name=self.fip['name'], resource=self.fip, + resource_type=ovn_const.TYPE_FLOATINGIPS, if_exists=True) + + def test_check_revision_number_floating_ip_not_found(self): + self._test_check_revision_number( + name='fip-not-found', resource=self.fip, + resource_type=ovn_const.TYPE_FLOATINGIPS, if_exists=False) + + def test_check_revision_number_floating_ip_revision_conflict(self): + self._test_check_revision_number( + name=self.fip['name'], resource=self.fip_old_rev, + resource_type=ovn_const.TYPE_FLOATINGIPS, if_exists=False, + revision_conflict=True) + + def test_check_revision_number_subnet(self): + self._test_check_revision_number( + name=self.subnet['name'], resource=self.subnet, + resource_type=ovn_const.TYPE_SUBNETS, if_exists=True) + + def test_check_revision_number_subnet_not_found(self): + self._test_check_revision_number( + name='subnet-not-found', resource=self.subnet, + resource_type=ovn_const.TYPE_SUBNETS, if_exists=False) + + +class TestDeleteLRouterExtGwCommand(TestBaseCommand): + + def test_delete_lrouter_extgw_routes(self): + fake_route_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'ip_prefix': '0.0.0.0/0', 'nexthop': '10.0.0.1', + 'external_ids': {ovn_const.OVN_ROUTER_IS_EXT_GW: True}}) + fake_route_2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'ip_prefix': '50.0.0.0/24', 'nexthop': '40.0.0.101'}) + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'static_routes': [fake_route_1, fake_route_2]}) + with mock.patch.object(self.ovn_api, "is_col_present", + return_value=True): + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + cmd = commands.DeleteLRouterExtGwCommand( + self.ovn_api, fake_lrouter.name, False) + cmd.run_idl(self.transaction) + fake_lrouter.delvalue.assert_called_once_with( + 'static_routes', fake_route_1) + + def test_delete_lrouter_extgw_nat(self): + fake_nat_1 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.10', + 'logical_ip': '10.0.0.4', 'type': 'snat'}) + fake_nat_2 = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ip': '192.168.1.8', + 'logical_ip': '10.0.0.5', 'type': 'badtype'}) + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'nat': [fake_nat_1, fake_nat_2]}) + with mock.patch.object(self.ovn_api, "is_col_present", + return_value=True): + with mock.patch.object(idlutils, 'row_by_value', + return_value=fake_lrouter): + cmd = commands.DeleteLRouterExtGwCommand( + self.ovn_api, fake_lrouter.name, False) + cmd.run_idl(self.transaction) + fake_lrouter.delvalue.assert_called_once_with( + 'nat', fake_nat_1) + + def test_delete_lrouter_extgw_ports(self): + port_id = 'fake-port-id' + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': + {ovn_const.OVN_GW_PORT_EXT_ID_KEY: port_id}}) + with mock.patch.object(self.ovn_api, "is_col_present", + return_value=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=[fake_lrouter, port_id]): + cmd = commands.DeleteLRouterExtGwCommand( + self.ovn_api, fake_lrouter.name, False) + cmd.run_idl(self.transaction) + fake_lrouter.delvalue.assert_called_once_with( + 'ports', port_id) + + def test_delete_lrouter_extgw_ports_not_found(self): + port_id = 'fake-port-id' + fake_lrouter = fakes.FakeOvsdbRow.create_one_ovsdb_row( + attrs={'external_ids': + {ovn_const.OVN_GW_PORT_EXT_ID_KEY: port_id}}) + with mock.patch.object(self.ovn_api, "is_col_present", + return_value=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=[fake_lrouter, + idlutils.RowNotFound]): + cmd = commands.DeleteLRouterExtGwCommand( + self.ovn_api, fake_lrouter.name, False) + cmd.run_idl(self.transaction) + fake_lrouter.delvalue.assert_not_called() + + def _test_delete_lrouter_no_lrouter_exist(self, if_exists=True): + with mock.patch.object(self.ovn_api, "is_col_present", + return_value=True): + with mock.patch.object(idlutils, 'row_by_value', + side_effect=idlutils.RowNotFound): + cmd = commands.DeleteLRouterExtGwCommand( + self.ovn_api, "fake-lrouter", if_exists=if_exists) + if if_exists: + cmd.run_idl(self.transaction) + else: + self.assertRaises(RuntimeError, cmd.run_idl, + self.transaction) + + def test_delete_lrouter_no_lrouter_exist_ignore(self): + self._test_delete_lrouter_no_lrouter_exist(if_exists=True) + + def test_delete_no_lrouter_exist_fail(self): + self._test_delete_lrouter_no_lrouter_exist(if_exists=False)