From 4bde2d5237022612f8d16842356f44ce29a2a706 Mon Sep 17 00:00:00 2001 From: Terry Wilson Date: Fri, 2 Jun 2017 16:19:37 -0500 Subject: [PATCH] Add OVN Northbound API for LS, LSP, and DHCP This patch implments the ovn-nbctl API for the Logical_Switch, Logical_Switch_Port, and DHCP_Options commands. Additional patches will implement logical router and load balancer functionality. As a convenience, the add/list/get commands return a special read-only version of an ovs.db.idl.Row object called a RowView. This object can be compared to a Row for equality and hashing. This saves having to return uuids and then look them up. This behavior differs from the Open_vSwitch schema implementation. This wrapper serves both to keep people from modifying returned values outside of a transaction and as an interface for any future backend to implement. In addition, an ovs virtual environment fixture based on ovs-sandbox is added to set up a sandboxed ovs/ovn install for running functional tests. Change-Id: I93689158467ff73a1b02588510d168b50ed6292a --- bindep.txt | 6 + ovsdbapp/backend/ovs_idl/__init__.py | 69 +++ ovsdbapp/constants.py | 3 + ovsdbapp/exceptions.py | 9 +- ovsdbapp/schema/ovn_northbound/__init__.py | 0 ovsdbapp/schema/ovn_northbound/api.py | 339 ++++++++++++ ovsdbapp/schema/ovn_northbound/commands.py | 486 ++++++++++++++++++ ovsdbapp/schema/ovn_northbound/impl_idl.py | 152 ++++++ ovsdbapp/schema/ovn_southbound/__init__.py | 0 ovsdbapp/tests/functional/base.py | 47 ++ .../schema/open_vswitch/test_impl_idl.py | 14 +- .../schema/ovn_northbound/__init__.py | 0 .../schema/ovn_northbound/fixtures.py | 55 ++ .../schema/ovn_northbound/test_impl_idl.py | 417 +++++++++++++++ ovsdbapp/tests/unit/backend/test_ovs_idl.py | 46 ++ ovsdbapp/venv.py | 160 ++++++ requirements.txt | 2 + tools/tox_install.sh | 9 + tox.ini | 1 + 19 files changed, 1804 insertions(+), 11 deletions(-) create mode 100644 ovsdbapp/schema/ovn_northbound/__init__.py create mode 100644 ovsdbapp/schema/ovn_northbound/api.py create mode 100644 ovsdbapp/schema/ovn_northbound/commands.py create mode 100644 ovsdbapp/schema/ovn_northbound/impl_idl.py create mode 100644 ovsdbapp/schema/ovn_southbound/__init__.py create mode 100644 ovsdbapp/tests/functional/base.py create mode 100644 ovsdbapp/tests/functional/schema/ovn_northbound/__init__.py create mode 100644 ovsdbapp/tests/functional/schema/ovn_northbound/fixtures.py create mode 100644 ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py create mode 100644 ovsdbapp/tests/unit/backend/test_ovs_idl.py create mode 100644 ovsdbapp/venv.py diff --git a/bindep.txt b/bindep.txt index 11673d17..f56c9567 100644 --- a/bindep.txt +++ b/bindep.txt @@ -4,3 +4,9 @@ openvswitch [platform:rpm test] openvswitch-switch [platform:dpkg test] curl [test] +autoconf [test] +automake [test] +libtool [test] +gcc [test] +make [test] +patch [test] diff --git a/ovsdbapp/backend/ovs_idl/__init__.py b/ovsdbapp/backend/ovs_idl/__init__.py index a1b99b1f..2c13f337 100644 --- a/ovsdbapp/backend/ovs_idl/__init__.py +++ b/ovsdbapp/backend/ovs_idl/__init__.py @@ -10,7 +10,30 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + from ovsdbapp.backend.ovs_idl import command as cmd +from ovsdbapp.backend.ovs_idl import idlutils + +_NO_DEFAULT = object() + + +class RowView(object): + def __init__(self, row): + self._row = row + + def __getattr__(self, column_name): + return getattr(self._row, column_name) + + def __eq__(self, other): + # use other's == since it is likely to be a Row object + try: + return other == self._row + except NotImplemented: + return other._row == self._row + + def __hash__(self): + return self._row.__hash__() class Backend(object): @@ -37,3 +60,49 @@ class Backend(object): def db_find(self, table, *conditions, **kwargs): return cmd.DbFindCommand(self, table, *conditions, **kwargs) + + def lookup(self, table, record, default=_NO_DEFAULT): + try: + return self._lookup(table, record) + except idlutils.RowNotFound: + if default is not _NO_DEFAULT: + return default + raise + + def _lookup(self, table, record): + t = self.tables[table] + try: + if isinstance(record, uuid.UUID): + return t.rows[record] + try: + uuid_ = uuid.UUID(record) + return t.rows[uuid_] + except ValueError: + # Not a UUID string, continue lookup by other means + pass + except KeyError: + # If record isn't found by UUID , go ahead and look up by the table + pass + + if not self.lookup_table: + raise idlutils.RowNotFound(table=table, col='record', + match=record) + # NOTE (twilson) This is an approximation of the db-ctl implementation + # that allows a partial table, assuming that if a table has a single + # index, that we should be able to do a lookup by it. + rl = self.lookup_table.get( + table, + idlutils.RowLookup(table, idlutils.get_index_column(t), None)) + # no table means uuid only, no column means lookup table has one row + if rl.table is None: + raise idlutils.RowNotFound(table=table, col='uuid', match=record) + if rl.column is None: + return next(iter(t.rows.values())) + row = idlutils.row_by_value(self, rl.table, rl.column, record) + if rl.uuid_column: + rows = getattr(row, rl.uuid_column) + if len(rows) != 1: + raise idlutils.RowNotFound(table=table, col='record', + match=record) + row = rows[0] + return row diff --git a/ovsdbapp/constants.py b/ovsdbapp/constants.py index cfa33d86..82c0df54 100644 --- a/ovsdbapp/constants.py +++ b/ovsdbapp/constants.py @@ -13,5 +13,8 @@ # under the License. DEFAULT_OVSDB_CONNECTION = 'tcp:127.0.0.1:6640' +DEFAULT_OVNNB_CONNECTION = 'tcp:127.0.0.1:6641' DEFAULT_TIMEOUT = 5 DEVICE_NAME_MAX_LEN = 14 + +ACL_PRIORITY_MAX = 32767 diff --git a/ovsdbapp/exceptions.py b/ovsdbapp/exceptions.py index 7369092b..ca5f9c0a 100644 --- a/ovsdbapp/exceptions.py +++ b/ovsdbapp/exceptions.py @@ -15,7 +15,7 @@ import six -class OvsdbAppException(Exception): +class OvsdbAppException(RuntimeError): """Base OvsdbApp Exception. To correctly use this class, inherit from it and define @@ -52,3 +52,10 @@ class OvsdbAppException(Exception): class TimeoutException(OvsdbAppException): message = "Commands %(commands)s exceeded timeout %(timeout)d seconds" + + +class OvsdbConnectionUnavailable(OvsdbAppException): + message = ("OVS database connection to %(db_schema)s failed with error: " + "'%(error)s'. Verify that the OVS and OVN services are " + "available and that the 'ovn_nb_connection' and " + "'ovn_sb_connection' configuration options are correct.") diff --git a/ovsdbapp/schema/ovn_northbound/__init__.py b/ovsdbapp/schema/ovn_northbound/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovsdbapp/schema/ovn_northbound/api.py b/ovsdbapp/schema/ovn_northbound/api.py new file mode 100644 index 00000000..863728e7 --- /dev/null +++ b/ovsdbapp/schema/ovn_northbound/api.py @@ -0,0 +1,339 @@ +# 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 + +import six + +from ovsdbapp import api + + +@six.add_metaclass(abc.ABCMeta) +class API(api.API): + """An API based off of the ovn-nbctl CLI interface + + This API basically mirrors the ovn-nbctl operations with these changes: + 1. Methods that create objects will return a read-only view of the object + 2. Methods which list objects will return a list of read-only view objects + """ + + @abc.abstractmethod + def ls_add(self, switch=None, may_exist=False, **columns): + """Create a logical switch named 'switch' + + :param switch: The name of the switch (optional) + :type switch: string or uuid.UUID + :param may_exist: If True, don't fail if the switch already exists + :type may_exist: boolean + :param columns: Additional columns to directly set on the switch + :returns: :class:`Command` with RowView result + """ + + @abc.abstractmethod + def ls_del(self, switch, if_exists=False): + """Delete logical switch 'switch' and all its ports + + :param switch: The name or uuid of the switch + :type switch: string or uuid.UUID + :type if_exists: If True, don't fail if the switch doesn't exist + :type if_exists: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def ls_list(self): + """Get all logical switches + + :returns: :class:`Command` with RowView list result + """ + + @abc.abstractmethod + def acl_add(self, switch, direction, priority, match, action, log=False): + """Add an ACL to 'switch' + + :param switch: The name or uuid of the switch + :type switch: string or uuid.UUID + :param direction: The traffic direction to match + :type direction: 'from-lport' or 'to-lport' + :param priority: The priority field of the ACL + :type priority: int + :param match: The match rule + :type match: string + :param action: The action to take upon match + :type action: 'allow', 'allow-related', 'drop', or 'reject' + :param log: If True, enable packet logging for the ACL + :type log: boolean + :returns: :class:`Command` with RowView result + """ + + @abc.abstractmethod + def acl_del(self, switch, direction=None, priority=None, match=None): + """Remove ACLs from 'switch' + + If only switch is supplied, all the ACLs from the logical switch are + deleted. If direction is also specified, then all the flows in that + direction will be deleted from the logical switch. If all the fields + are given, then only flows that match all fields will be deleted. + + :param switch: The name or uuid of the switch + :type switch: string or uuid.UUID + :param direction: The traffic direction to match + :type direction: 'from-lport' or 'to-lport' + :param priority: The priority field of the ACL + :type priority: int + :param match: The match rule + :type match: string + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def acl_list(self, switch): + """Get the ACLs for 'switch' + + :param switch: The name or uuid of the switch + :type switch: string or uuid.UUID + :returns: :class:`Command` with RowView list result + """ + + @abc.abstractmethod + def lsp_add(self, switch, port, parent=None, tag=None, may_exist=False, + **columns): + """Add logical port 'port' on 'switch' + + NOTE: for the purposes of testing the existence of the 'port', + 'port' is treated as either a name or a uuid, as in ovn-nbctl. + + :param switch: The name or uuid of the switch + :type switch: string or uuid.UUID + :param port: The name of the port + :type port: string or uuid.UUID + :param parent: The name of the parent port (requires tag) + :type parent: string + :param tag: The tag_request field of the port. 0 causes + ovn-northd to assign a unique tag + :type tag: int [0, 4095] + :param may_exist: If True, don't fail if the switch already exists + :type may_exist: boolean + :param columns: Additional columns to directly set on the switch + :returns: :class:`Command` with RowView result + """ + + @abc.abstractmethod + def lsp_del(self, port, if_exists=False): + """Delete 'port' from its attached switch + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :type if_exists: If True, don't fail if the switch doesn't exist + :type if_exists: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lsp_list(self, switch): + """Get the logical ports on switch + + :param switch: The name or uuid of the switch + :type switch: string or uuid.UUID + :returns: :class:`Command` with RowView list result + """ + + @abc.abstractmethod + def lsp_get_parent(self, port): + """Get the parent of 'port' if set + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :returns: :class:`Command` with port parent string result or + "" if not set + """ + + @abc.abstractmethod + def lsp_set_addresses(self, port, addresses): + """Set addresses for 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :param addresses: One or more addresses in the format: + 'unknown', 'router', 'dynamic', or + 'ethaddr [ipaddr]...' + :type addresses: string + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lsp_get_addresses(self, port): + """Return the list of addresses assigned to port + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :returns: A list of string representations of addresses in the + format referenced in lsp_set_addresses + """ + + @abc.abstractmethod + def lsp_set_port_security(self, port, addresses): + """Set port security addresses for 'port' + + Sets the port security addresses associated with port to addrs. + Multiple sets of addresses may be set by using multiple addrs + arguments. If no addrs argument is given, port will not have + port security enabled. + + Port security limits the addresses from which a logical port may + send packets and to which it may receive packets. + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :param addresses: The addresses in the format 'ethaddr [ipaddr...]' + See `man ovn-nb` and port_security column for details + :type addresses: string + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lsp_get_port_security(self, port): + """Get port security addresses for 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :returns: :class:`Command` with list of strings described by + lsp_set_port_security result + """ + + @abc.abstractmethod + def lsp_get_up(self, port): + """Get state of port. + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :returns: :class:`Command` with boolean result + """ + + @abc.abstractmethod + def lsp_set_enabled(self, port, is_enabled): + """Set administrative state of 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :param is_enabled: Whether the port should be enabled + :type is_enabled: boolean + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lsp_get_enabled(self, port): + """Get administrative state of 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :returns: :class:`Command` with boolean result + """ + + @abc.abstractmethod + def lsp_set_type(self, port, port_type): + """Set the type for 'port + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :param port_type: The type of the port + :type port_type: string + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lsp_get_type(self, port): + """Get the type for 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :returns: :class:`Command` with string result + """ + + @abc.abstractmethod + def lsp_set_options(self, port, **options): + """Set options related to the type of 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :param options: keys and values for the port 'options' dict + :type options: key: string, value: string + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def lsp_get_options(self, port): + """Get the type-specific options for 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :returns: :class:`Command` with dict result + """ + + @abc.abstractmethod + def lsp_set_dhcpv4_options(self, port, dhcp_options_uuid): + """Set the dhcp4 options for 'port' + + :param port: The name or uuid of the port + :type port: string or uuid.UUID + :param dhcp_options_uuid: The uuid of the dhcp_options row + :type dhcp_options_uuid: uuid.UUID + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def dhcp_options_add(self, cidr, **external_ids): + """Create a DHCP options row with CIDR + + This is equivalent to ovn-nbctl's dhcp-options-create, but renamed + to be consistent with other object creation methods + + :param cidr: An IP network in CIDR format + :type cidr: string + :param external_ids: external_id field key/value mapping + :type external_ids: key: string, value: string + :returns: :class:`Command` with RowView result + """ + + @abc.abstractmethod + def dhcp_options_del(self, uuid): + """Delete DHCP options row with 'uuid' + + :param uuid: The uuid of the DHCP Options row to delete + :type uuid: string or uuid.UUID + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def dhcp_options_list(self): + """Get all DHCP_Options + + :returns: :class:`Command with RowView list result + """ + + @abc.abstractmethod + def dhcp_options_set_options(self, uuid, **options): + """Set the DHCP options for 'uuid' + + :param uuid: The uuid of the DHCP Options row + :type uuid: string or uuid.UUID + :returns: :class:`Command` with no result + """ + + @abc.abstractmethod + def dhcp_options_get_options(self, uuid): + """Get the DHCP options for 'uuid' + + :param uuid: The uuid of the DHCP Options row + :type uuid: string or uuid.UUID + :returns: :class:`Command` with dict result + """ diff --git a/ovsdbapp/schema/ovn_northbound/commands.py b/ovsdbapp/schema/ovn_northbound/commands.py new file mode 100644 index 00000000..6abce976 --- /dev/null +++ b/ovsdbapp/schema/ovn_northbound/commands.py @@ -0,0 +1,486 @@ +# 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 re + +import netaddr + +from ovsdbapp.backend import ovs_idl +from ovsdbapp.backend.ovs_idl import command as cmd +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp import constants as const + + +class AddCommand(cmd.BaseCommand): + table_name = [] # unhashable, won't be looked up + + def post_commit(self, txn): + # If get_insert_uuid fails, self.result was not a result of a + # recent insert. Most likely we are post_commit after a lookup() + real_uuid = txn.get_insert_uuid(self.result) or self.result + row = self.api.tables[self.table_name].rows[real_uuid] + self.result = ovs_idl.RowView(row) + + +class LsAddCommand(AddCommand): + table_name = 'Logical_Switch' + + def __init__(self, api, switch=None, may_exist=False, **columns): + super(LsAddCommand, self).__init__(api) + self.switch = switch + self.columns = columns + self.may_exist = may_exist + + def run_idl(self, txn): + # There is no requirement for name to be unique, so if a name is + # specified, we always have to do a lookup since adding it won't + # fail. If may_exist is set, we just don't do anything when dup'd + if self.switch: + sw = idlutils.row_by_value(self.api.idl, self.table_name, 'name', + self.switch, None) + if sw: + if self.may_exist: + self.result = ovs_idl.RowView(sw) + return + raise RuntimeError("Switch %s exists" % self.switch) + elif self.may_exist: + raise RuntimeError("may_exist requires name") + sw = txn.insert(self.api.tables[self.table_name]) + if self.switch: + sw.name = self.switch + else: + # because ovs.db.idl brokenly requires a changed column + sw.name = "" + for col, value in self.columns.items(): + setattr(sw, col, value) + self.result = sw.uuid + + +class LsDelCommand(cmd.BaseCommand): + def __init__(self, api, switch, if_exists=False): + super(LsDelCommand, self).__init__(api) + self.switch = switch + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lswitch = self.api.lookup('Logical_Switch', self.switch) + lswitch.delete() + except idlutils.RowNotFound: + if self.if_exists: + return + msg = "Logical Switch %s does not exist" % self.switch + raise RuntimeError(msg) + + +class LsListCommand(cmd.BaseCommand): + def run_idl(self, txn): + table = self.api.tables['Logical_Switch'] + self.result = [ovs_idl.RowView(r) for r in table.rows.values()] + + +class AclAddCommand(AddCommand): + table_name = 'ACL' + + def __init__(self, api, switch, direction, priority, match, action, + log=False, may_exist=False, **external_ids): + if direction not in ('from-lport', 'to-lport'): + raise TypeError("direction must be either from-lport or to-lport") + if not 0 <= priority <= const.ACL_PRIORITY_MAX: + raise ValueError("priority must be beween 0 and %s, inclusive" % ( + const.ACL_PRIORITY_MAX)) + if action not in ('allow', 'allow-related', 'drop', 'reject'): + raise TypeError("action must be allow/allow-related/drop/reject") + super(AclAddCommand, self).__init__(api) + self.switch = switch + self.direction = direction + self.priority = priority + self.match = match + self.action = action + self.log = log + self.may_exist = may_exist + self.external_ids = external_ids + + def acl_match(self, row): + return (self.direction == row.direction and + self.priority == row.priority and + self.match == row.match) + + def run_idl(self, txn): + ls = self.api.lookup('Logical_Switch', self.switch) + acls = [acl for acl in ls.acls if self.acl_match(acl)] + if acls: + if self.may_exist: + self.result = ovs_idl.RowView(acls[0]) + return + raise RuntimeError("ACL (%s, %s, %s) already exists" % ( + self.direction, self.priority, self.match)) + acl = txn.insert(self.api.tables[self.table_name]) + acl.direction = self.direction + acl.priority = self.priority + acl.match = self.match + acl.action = self.action + acl.log = self.log + ls.addvalue('acls', acl) + for col, value in self.external_ids.items(): + acl.setkey('external_ids', col, value) + self.result = acl.uuid + + +class AclDelCommand(cmd.BaseCommand): + def __init__(self, api, switch, direction=None, + priority=None, match=None): + if (priority is None) != (match is None): + raise TypeError("Must specify priority and match together") + if priority is not None and not direction: + raise TypeError("Cannot specify priority/match without direction") + super(AclDelCommand, self).__init__(api) + self.switch = switch + self.conditions = [] + if direction: + self.conditions.append(('direction', '=', direction)) + # priority can be 0 + if match: # and therefor prioroity due to the above check + self.conditions += [('priority', '=', priority), + ('match', '=', match)] + + def run_idl(self, txn): + ls = self.api.lookup('Logical_Switch', self.switch) + for acl in [a for a in ls.acls + if idlutils.row_match(a, self.conditions)]: + ls.delvalue('acls', acl) + acl.delete() + + +class AclListCommand(cmd.BaseCommand): + def __init__(self, api, switch): + super(AclListCommand, self).__init__(api) + self.switch = switch + + def run_idl(self, txn): + ls = self.api.lookup('Logical_Switch', self.switch) + self.result = [ovs_idl.RowView(acl) for acl in ls.acls] + + +class LspAddCommand(AddCommand): + table_name = 'Logical_Switch_Port' + + def __init__(self, api, switch, port, parent=None, tag=None, + may_exist=False, **columns): + if tag and not 0 <= tag <= 4095: + raise TypeError("tag must be 0 to 4095, inclusive") + if (parent is None) != (tag is None): + raise TypeError("parent and tag must be passed together") + super(LspAddCommand, self).__init__(api) + self.switch = switch + self.port = port + self.parent = parent + self.tag = tag + self.may_exist = may_exist + self.columns = columns + + def run_idl(self, txn): + ls = self.api.lookup('Logical_Switch', self.switch) + try: + lsp = self.api.lookup(self.table_name, self.port) + if self.may_exist: + msg = None + if lsp not in ls.ports: + msg = "%s exists, but is not in %s" % ( + self.port, self.switch) + if self.parent: + if not lsp.parent_name: + msg = "%s exists, but has no parent" % self.port + # parent_name, being optional, is stored as list + if self.parent not in lsp.parent_name: + msg = "%s exists with different parent" % self.port + if self.tag not in lsp.tag_request: + msg = "%s exists with different tag request" % ( + self.port,) + elif lsp.parent_name: + msg = "%s exists, but with a parent" % self.port + + if msg: + raise RuntimeError(msg) + self.result = ovs_idl.RowView(lsp) + return + except idlutils.RowNotFound: + # This is what we want + pass + lsp = txn.insert(self.api.tables[self.table_name]) + lsp.name = self.port + if self.tag is not None: + lsp.parent_name = self.parent + lsp.tag_request = self.tag + ls.addvalue('ports', lsp) + for col, value in self.columns.items(): + setattr(lsp, col, value) + self.result = lsp.uuid + + +class LspDelCommand(cmd.BaseCommand): + def __init__(self, api, port, switch=None, if_exists=False): + super(LspDelCommand, self).__init__(api) + self.port = port + self.switch = switch + self.if_exists = if_exists + + def run_idl(self, txn): + try: + lsp = self.api.lookup('Logical_Switch_Port', self.port) + except idlutils.RowNotFound: + if self.if_exists: + return + raise RuntimeError("%s does not exist" % self.port) + + # We need to delete the port from its switch + if self.switch: + sw = self.api.lookup('Logical_Switch', self.switch) + else: + sw = next(iter( + s for s in self.api.tables['Logical_Switch'].rows.values() + if lsp in s.ports), None) + if not (sw and lsp in sw.ports): + raise RuntimeError("%s does not exist in %s" % ( + self.port, self.switch)) + sw.delvalue('ports', lsp) + lsp.delete() + + +class LspListCommand(cmd.BaseCommand): + def __init__(self, api, switch): + super(LspListCommand, self).__init__(api) + self.switch = switch + + def run_idl(self, txn): + sw = self.api.lookup('Logical_Switch', self.switch) + self.result = [ovs_idl.RowView(r) for r in sw.ports] + + +class LspGetParentCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LspGetParentCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + self.result = next(iter(lsp.parent_name), "") + + +class LspGetTagCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LspGetTagCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + self.result = next(iter(lsp.tag), -1) + + +class LspSetAddressesCommand(cmd.BaseCommand): + addr_re = re.compile( + r'^(router|unknown|dynamic|([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2} .+)$') + + def __init__(self, api, port, addresses): + for addr in addresses: + if not self.addr_re.match(addr): + raise TypeError( + "address must be router/unknown/dynamic/ethaddr ipaddr...") + super(LspSetAddressesCommand, self).__init__(api) + self.port = port + self.addresses = addresses + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + lsp.addresses = self.addresses + + +class LspGetAddressesCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LspGetAddressesCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + self.result = lsp.addresses + + +class LspSetPortSecurityCommand(cmd.BaseCommand): + def __init__(self, api, port, addresses): + # NOTE(twilson) ovn-nbctl.c does not do any checking of addresses + # so neither do we + super(LspSetPortSecurityCommand, self).__init__(api) + self.port = port + self.addresses = addresses + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + lsp.port_security = self.addresses + + +class LspGetPortSecurityCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LspGetPortSecurityCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + self.result = lsp.port_security + + +class LspGetUpCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LspGetUpCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + # 'up' is optional, but if not up, it's not up :p + self.result = next(iter(lsp.up), False) + + +class LspSetEnabledCommand(cmd.BaseCommand): + def __init__(self, api, port, is_enabled): + super(LspSetEnabledCommand, self).__init__(api) + self.port = port + self.is_enabled = is_enabled + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + lsp.enabled = self.is_enabled + + +class LspGetEnabledCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LspGetEnabledCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + # enabled is optional, but if not disabled then enabled + self.result = next(iter(lsp.enabled), True) + + +class LspSetTypeCommand(cmd.BaseCommand): + def __init__(self, api, port, port_type): + super(LspSetTypeCommand, self).__init__(api) + self.port = port + self.port_type = port_type + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + lsp.type = self.port_type + + +class LspGetTypeCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LspGetTypeCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + self.result = lsp.type + + +class LspSetOptionsCommand(cmd.BaseCommand): + def __init__(self, api, port, **options): + super(LspSetOptionsCommand, self).__init__(api) + self.port = port + self.options = options + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + lsp.options = self.options + + +class LspGetOptionsCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LspGetOptionsCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + self.result = lsp.options + + +class LspSetDhcpV4OptionsCommand(cmd.BaseCommand): + def __init__(self, api, port, dhcpopt_uuid): + super(LspSetDhcpV4OptionsCommand, self).__init__(api) + self.port = port + self.dhcpopt_uuid = dhcpopt_uuid + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + lsp.dhcpv4_options = self.dhcpopt_uuid + + +class LspGetDhcpV4OptionsCommand(cmd.BaseCommand): + def __init__(self, api, port): + super(LspGetDhcpV4OptionsCommand, self).__init__(api) + self.port = port + + def run_idl(self, txn): + lsp = self.api.lookup('Logical_Switch_Port', self.port) + self.result = next((ovs_idl.RowView(d) + for d in lsp.dhcpv4_options), []) + + +class DhcpOptionsAddCommand(AddCommand): + table_name = 'DHCP_Options' + + def __init__(self, api, cidr, **external_ids): + cidr = netaddr.IPNetwork(cidr) + super(DhcpOptionsAddCommand, self).__init__(api) + self.cidr = str(cidr) + self.external_ids = external_ids + + def run_idl(self, txn): + dhcpopt = txn.insert(self.api.tables[self.table_name]) + dhcpopt.cidr = self.cidr + dhcpopt.external_ids = self.external_ids + self.result = dhcpopt.uuid + + +class DhcpOptionsDelCommand(cmd.BaseCommand): + def __init__(self, api, dhcpopt_uuid): + super(DhcpOptionsDelCommand, self).__init__(api) + self.dhcpopt_uuid = dhcpopt_uuid + + def run_idl(self, txn): + dhcpopt = self.api.lookup('DHCP_Options', self.dhcpopt_uuid) + dhcpopt.delete() + + +class DhcpOptionsListCommand(cmd.BaseCommand): + def run_idl(self, txn): + self.result = [ovs_idl.RowView(r) for + r in self.api.tables['DHCP_Options'].rows.values()] + + +class DhcpOptionsSetOptionsCommand(cmd.BaseCommand): + def __init__(self, api, dhcpopt_uuid, **options): + super(DhcpOptionsSetOptionsCommand, self).__init__(api) + self.dhcpopt_uuid = dhcpopt_uuid + self.options = options + + def run_idl(self, txn): + dhcpopt = self.api.lookup('DHCP_Options', self.dhcpopt_uuid) + dhcpopt.options = self.options + + +class DhcpOptionsGetOptionsCommand(cmd.BaseCommand): + def __init__(self, api, dhcpopt_uuid): + super(DhcpOptionsGetOptionsCommand, self).__init__(api) + self.dhcpopt_uuid = dhcpopt_uuid + + def run_idl(self, txn): + dhcpopt = self.api.lookup('DHCP_Options', self.dhcpopt_uuid) + self.result = dhcpopt.options diff --git a/ovsdbapp/schema/ovn_northbound/impl_idl.py b/ovsdbapp/schema/ovn_northbound/impl_idl.py new file mode 100644 index 00000000..48d6b547 --- /dev/null +++ b/ovsdbapp/schema/ovn_northbound/impl_idl.py @@ -0,0 +1,152 @@ +# 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 logging + +from ovsdbapp.backend import ovs_idl +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp.backend.ovs_idl import transaction +from ovsdbapp import exceptions +from ovsdbapp.schema.ovn_northbound import api +from ovsdbapp.schema.ovn_northbound import commands as cmd + +LOG = logging.getLogger(__name__) + + +class OvnNbApiIdlImpl(ovs_idl.Backend, api.API): + schema = 'OVN_Northbound' + ovsdb_connection = None + lookup_table = { + 'Logical_Switch': idlutils.RowLookup('Logical_Switch', 'name', None), + } + + def __init__(self, connection): + super(OvnNbApiIdlImpl, self).__init__() + try: + if OvnNbApiIdlImpl.ovsdb_connection is None: + OvnNbApiIdlImpl.ovsdb_connection = connection + OvnNbApiIdlImpl.ovsdb_connection.start() + except Exception as e: + connection_exception = exceptions.OvsdbConnectionUnavailable( + db_schema=self.schema, error=e) + LOG.exception(connection_exception) + raise connection_exception + + @property + def idl(self): + return OvnNbApiIdlImpl.ovsdb_connection.idl + + @property + def tables(self): + return self.idl.tables + + # NOTE(twilson) _tables is for legacy code, but it has always been used + # outside the Idl API implementions + _tables = tables + + def create_transaction(self, check_error=False, log_errors=True, **kwargs): + return transaction.Transaction( + self, OvnNbApiIdlImpl.ovsdb_connection, + OvnNbApiIdlImpl.ovsdb_connection.timeout, + check_error, log_errors) + + def ls_add(self, switch=None, may_exist=False, **columns): + return cmd.LsAddCommand(self, switch, may_exist, **columns) + + def ls_del(self, switch, if_exists=False): + return cmd.LsDelCommand(self, switch, if_exists) + + def ls_list(self): + return cmd.LsListCommand(self) + + def acl_add(self, switch, direction, priority, match, action, log=False, + may_exist=False, **external_ids): + return cmd.AclAddCommand(self, switch, direction, priority, + match, action, log, may_exist, **external_ids) + + def acl_del(self, switch, direction=None, priority=None, match=None): + return cmd.AclDelCommand(self, switch, direction, priority, match) + + def acl_list(self, switch): + return cmd.AclListCommand(self, switch) + + def lsp_add(self, switch, port, parent=None, tag=None, may_exist=False, + **columns): + return cmd.LspAddCommand(self, switch, port, parent, tag, may_exist, + **columns) + + def lsp_del(self, port, switch=None, if_exists=False): + return cmd.LspDelCommand(self, port, switch, if_exists) + + def lsp_list(self, switch): + return cmd.LspListCommand(self, switch) + + def lsp_get_parent(self, port): + return cmd.LspGetParentCommand(self, port) + + def lsp_get_tag(self, port): + # NOTE (twilson) tag can be unassigned for a while after setting + return cmd.LspGetTagCommand(self, port) + + def lsp_set_addresses(self, port, addresses): + return cmd.LspSetAddressesCommand(self, port, addresses) + + def lsp_get_addresses(self, port): + return cmd.LspGetAddressesCommand(self, port) + + def lsp_set_port_security(self, port, addresses): + return cmd.LspSetPortSecurityCommand(self, port, addresses) + + def lsp_get_port_security(self, port): + return cmd.LspGetPortSecurityCommand(self, port) + + def lsp_get_up(self, port): + return cmd.LspGetUpCommand(self, port) + + def lsp_set_enabled(self, port, is_enabled): + return cmd.LspSetEnabledCommand(self, port, is_enabled) + + def lsp_get_enabled(self, port): + return cmd.LspGetEnabledCommand(self, port) + + def lsp_set_type(self, port, port_type): + return cmd.LspSetTypeCommand(self, port, port_type) + + def lsp_get_type(self, port): + return cmd.LspGetTypeCommand(self, port) + + def lsp_set_options(self, port, **options): + return cmd.LspSetOptionsCommand(self, port, **options) + + def lsp_get_options(self, port): + return cmd.LspGetOptionsCommand(self, port) + + def lsp_set_dhcpv4_options(self, port, dhcpopt_uuids): + return cmd.LspSetDhcpV4OptionsCommand(self, port, dhcpopt_uuids) + + def lsp_get_dhcpv4_options(self, port): + return cmd.LspGetDhcpV4OptionsCommand(self, port) + + def dhcp_options_add(self, cidr, **external_ids): + return cmd.DhcpOptionsAddCommand(self, cidr, **external_ids) + + def dhcp_options_del(self, dhcpopt_uuid): + return cmd.DhcpOptionsDelCommand(self, dhcpopt_uuid) + + def dhcp_options_list(self): + return cmd.DhcpOptionsListCommand(self) + + def dhcp_options_set_options(self, dhcpopt_uuid, **options): + return cmd.DhcpOptionsSetOptionsCommand(self, dhcpopt_uuid, **options) + + def dhcp_options_get_options(self, dhcpopt_uuid): + return cmd.DhcpOptionsGetOptionsCommand(self, dhcpopt_uuid) diff --git a/ovsdbapp/schema/ovn_southbound/__init__.py b/ovsdbapp/schema/ovn_southbound/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovsdbapp/tests/functional/base.py b/ovsdbapp/tests/functional/base.py new file mode 100644 index 00000000..6cb06ede --- /dev/null +++ b/ovsdbapp/tests/functional/base.py @@ -0,0 +1,47 @@ +# 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 atexit +import os +import tempfile + +from ovsdbapp.backend.ovs_idl import connection +from ovsdbapp import constants +from ovsdbapp.tests import base +from ovsdbapp import venv + + +class FunctionalTestCase(base.TestCase): + connection = None + ovsvenv = venv.OvsVenvFixture(tempfile.mkdtemp(), + ovsdir=os.getenv('OVS_SRCDIR'), remove=True) + atexit.register(ovsvenv.cleanUp) + ovsvenv.setUp() + + @classmethod + def set_connection(cls): + if cls.connection is not None: + return + if cls.schema == "Open_vSwitch": + conn = cls.ovsvenv.ovs_connection + elif cls.schema == "OVN_Northbound": + conn = cls.ovsvenv.ovnnb_connection + elif cls.schema == "OVN_Southbound": + conn = cls.ovsvenv.ovnsb_connection + else: + raise TypeError("Unknown schema '%s'" % cls.schema) + idl = connection.OvsdbIdl.from_server(conn, cls.schema) + cls.connection = connection.Connection(idl, constants.DEFAULT_TIMEOUT) + + def setUp(self): + super(FunctionalTestCase, self).setUp() + self.set_connection() diff --git a/ovsdbapp/tests/functional/schema/open_vswitch/test_impl_idl.py b/ovsdbapp/tests/functional/schema/open_vswitch/test_impl_idl.py index 8b3f557c..0c6f61b3 100644 --- a/ovsdbapp/tests/functional/schema/open_vswitch/test_impl_idl.py +++ b/ovsdbapp/tests/functional/schema/open_vswitch/test_impl_idl.py @@ -13,23 +13,17 @@ # License for the specific language governing permissions and limitations # under the License. -from ovsdbapp.backend.ovs_idl import connection -from ovsdbapp import constants from ovsdbapp.schema.open_vswitch import impl_idl -from ovsdbapp.tests import base +from ovsdbapp.tests.functional import base from ovsdbapp.tests import utils -ovsdb_connection = connection.Connection( - idl=connection.OvsdbIdl.from_server( - constants.DEFAULT_OVSDB_CONNECTION, 'Open_vSwitch'), - timeout=constants.DEFAULT_TIMEOUT) - -class TestOvsdbIdl(base.TestCase): +class TestOvsdbIdl(base.FunctionalTestCase): + schema = "Open_vSwitch" def setUp(self): super(TestOvsdbIdl, self).setUp() - self.api = impl_idl.OvsdbIdl(ovsdb_connection) + self.api = impl_idl.OvsdbIdl(self.connection) self.brname = utils.get_rand_device_name() # Destroying the bridge cleans up most things created by tests cleanup_cmd = self.api.del_br(self.brname) diff --git a/ovsdbapp/tests/functional/schema/ovn_northbound/__init__.py b/ovsdbapp/tests/functional/schema/ovn_northbound/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ovsdbapp/tests/functional/schema/ovn_northbound/fixtures.py b/ovsdbapp/tests/functional/schema/ovn_northbound/fixtures.py new file mode 100644 index 00000000..65987bb0 --- /dev/null +++ b/ovsdbapp/tests/functional/schema/ovn_northbound/fixtures.py @@ -0,0 +1,55 @@ +# 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 __future__ import absolute_import + +import fixtures + +from ovsdbapp.schema.ovn_northbound import impl_idl + + +class ImplIdlFixture(fixtures.Fixture): + api, create, delete = (None, None, None) + delete_args = {'if_exists': True} + + def __init__(self, *args, **kwargs): + super(ImplIdlFixture, self).__init__() + self.args = args + self.kwargs = kwargs + + def _setUp(self): + api = self.api(None) + create_fn = getattr(api, self.create) + delete_fn = getattr(api, self.delete) + self.obj = create_fn(*self.args, **self.kwargs).execute( + check_error=True) + self.addCleanup(delete_fn(self.obj.uuid, + **self.delete_args).execute, check_error=True) + + +class LogicalSwitchFixture(ImplIdlFixture): + api = impl_idl.OvnNbApiIdlImpl + create = 'ls_add' + delete = 'ls_del' + + +class DhcpOptionsFixture(ImplIdlFixture): + api = impl_idl.OvnNbApiIdlImpl + create = 'dhcp_options_add' + delete = 'dhcp_options_del' + delete_args = {} + + +class LogicalRouterFixture(ImplIdlFixture): + api = impl_idl.OvnNbApiIdlImpl + create = 'lr_add' + delete = 'lr_del' diff --git a/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py b/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py new file mode 100644 index 00000000..138c7dd0 --- /dev/null +++ b/ovsdbapp/tests/functional/schema/ovn_northbound/test_impl_idl.py @@ -0,0 +1,417 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import netaddr + +from ovsdbapp.schema.ovn_northbound import impl_idl +from ovsdbapp.tests.functional import base +from ovsdbapp.tests.functional.schema.ovn_northbound import fixtures +from ovsdbapp.tests import utils + + +class OvnNorthboundTest(base.FunctionalTestCase): + schema = 'OVN_Northbound' + + def setUp(self): + super(OvnNorthboundTest, self).setUp() + self.api = impl_idl.OvnNbApiIdlImpl(self.connection) + + +class TestLogicalSwitchOps(OvnNorthboundTest): + def setUp(self): + super(TestLogicalSwitchOps, self).setUp() + self.table = self.api.tables['Logical_Switch'] + + def _ls_add(self, *args, **kwargs): + fix = self.useFixture(fixtures.LogicalSwitchFixture(*args, **kwargs)) + self.assertIn(fix.obj.uuid, self.table.rows) + return fix.obj + + def test_ls_add_no_name(self): + self._ls_add() + + def test_ls_add_name(self): + name = utils.get_rand_device_name() + sw = self._ls_add(name) + self.assertEqual(name, sw.name) + + def test_ls_add_exists(self): + name = utils.get_rand_device_name() + self._ls_add(name) + cmd = self.api.ls_add(name) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_ls_add_may_exist(self): + name = utils.get_rand_device_name() + sw = self._ls_add(name) + sw2 = self.api.ls_add(name, may_exist=True).execute(check_error=True) + self.assertEqual(sw, sw2) + + def test_ls_add_columns(self): + external_ids = {'mykey': 'myvalue', 'yourkey': 'yourvalue'} + ls = self._ls_add(external_ids=external_ids) + self.assertEqual(external_ids, ls.external_ids) + + def test_ls_del(self): + sw = self._ls_add() + self.api.ls_del(sw.uuid).execute(check_error=True) + self.assertNotIn(sw.uuid, self.table.rows) + + def test_ls_del_by_name(self): + name = utils.get_rand_device_name() + self._ls_add(name) + self.api.ls_del(name).execute(check_error=True) + + def test_ls_del_no_exist(self): + name = utils.get_rand_device_name() + cmd = self.api.ls_del(name) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_ls_del_if_exists(self): + name = utils.get_rand_device_name() + self.api.ls_del(name, if_exists=True).execute(check_error=True) + + def test_ls_list(self): + with self.api.transaction(check_error=True): + switches = {self._ls_add() for _ in range(3)} + switch_set = set(self.api.ls_list().execute(check_error=True)) + self.assertTrue(switches.issubset(switch_set)) + + +class TestAclOps(OvnNorthboundTest): + def setUp(self): + super(TestAclOps, self).setUp() + self.switch = self.useFixture(fixtures.LogicalSwitchFixture()).obj + + def _acl_add(self, *args, **kwargs): + cmd = self.api.acl_add(self.switch.uuid, *args, **kwargs) + aclrow = cmd.execute(check_error=True) + self.assertIn(aclrow._row, self.switch.acls) + self.assertEqual(cmd.direction, aclrow.direction) + self.assertEqual(cmd.priority, aclrow.priority) + self.assertEqual(cmd.match, aclrow.match) + self.assertEqual(cmd.action, aclrow.action) + return aclrow + + def test_acl_add(self): + self._acl_add('from-lport', 0, 'output == "fake_port" && ip', + 'drop') + + def test_acl_add_exists(self): + args = ('from-lport', 0, 'output == "fake_port" && ip', 'drop') + self._acl_add(*args) + self.assertRaises(RuntimeError, self._acl_add, *args) + + def test_acl_add_may_exist(self): + args = ('from-lport', 0, 'output == "fake_port" && ip', 'drop') + row = self._acl_add(*args) + row2 = self._acl_add(*args, may_exist=True) + self.assertEqual(row, row2) + + def test_acl_add_extids(self): + external_ids = {'mykey': 'myvalue', 'yourkey': 'yourvalue'} + acl = self._acl_add('from-lport', 0, 'output == "fake_port" && ip', + 'drop', **external_ids) + self.assertEqual(external_ids, acl.external_ids) + + def test_acl_del_all(self): + r1 = self._acl_add('from-lport', 0, 'output == "fake_port"', 'drop') + self.api.acl_del(self.switch.uuid).execute(check_error=True) + self.assertNotIn(r1.uuid, self.api.tables['ACL'].rows) + self.assertEqual([], self.switch.acls) + + def test_acl_del_direction(self): + r1 = self._acl_add('from-lport', 0, 'output == "fake_port"', 'drop') + r2 = self._acl_add('to-lport', 0, 'output == "fake_port"', 'allow') + self.api.acl_del(self.switch.uuid, 'from-lport').execute( + check_error=True) + self.assertNotIn(r1, self.switch.acls) + self.assertIn(r2, self.switch.acls) + + def test_acl_del_direction_priority_match(self): + r1 = self._acl_add('from-lport', 0, 'output == "fake_port"', 'drop') + r2 = self._acl_add('from-lport', 1, 'output == "fake_port"', 'allow') + cmd = self.api.acl_del(self.switch.uuid, + 'from-lport', 0, 'output == "fake_port"') + cmd.execute(check_error=True) + self.assertNotIn(r1, self.switch.acls) + self.assertIn(r2, self.switch.acls) + + def test_acl_del_priority_without_match(self): + self.assertRaises(TypeError, self.api.acl_del, self.switch.uuid, + 'from-lport', 0) + + def test_acl_del_priority_without_direction(self): + self.assertRaises(TypeError, self.api.acl_del, self.switch.uuid, + priority=0) + + def test_acl_list(self): + r1 = self._acl_add('from-lport', 0, 'output == "fake_port"', 'drop') + r2 = self._acl_add('from-lport', 1, 'output == "fake_port2"', 'allow') + acls = self.api.acl_list(self.switch.uuid).execute(check_error=True) + self.assertIn(r1, acls) + self.assertIn(r2, acls) + + +class TestLspOps(OvnNorthboundTest): + def setUp(self): + super(TestLspOps, self).setUp() + name = utils.get_rand_device_name() + self.switch = self.useFixture( + fixtures.LogicalSwitchFixture(name)).obj + + def _lsp_add(self, switch, name, *args, **kwargs): + name = utils.get_rand_device_name() if name is None else name + lsp = self.api.lsp_add(switch, name, *args, **kwargs).execute( + check_error=True) + self.assertIn(lsp, self.switch.ports) + return lsp + + def test_lsp_add(self): + self._lsp_add(self.switch.uuid, None) + + def test_lsp_add_exists(self): + lsp = self._lsp_add(self.switch.uuid, None) + self.assertRaises(RuntimeError, self._lsp_add, self.switch.uuid, + lsp.name) + + def test_lsp_add_may_exist(self): + lsp1 = self._lsp_add(self.switch.uuid, None) + lsp2 = self._lsp_add(self.switch.uuid, lsp1.name, may_exist=True) + self.assertEqual(lsp1, lsp2) + + def test_lsp_add_may_exist_wrong_switch(self): + sw = self.useFixture(fixtures.LogicalSwitchFixture()).obj + lsp = self._lsp_add(self.switch.uuid, None) + self.assertRaises(RuntimeError, self._lsp_add, sw, lsp.name, + may_exist=True) + + def test_lsp_add_parent(self): + lsp1 = self._lsp_add(self.switch.uuid, None) + lsp2 = self._lsp_add(self.switch.uuid, None, parent=lsp1.name, tag=0) + # parent_name, being optional, is stored as a list + self.assertIn(lsp1.name, lsp2.parent_name) + + def test_lsp_add_parent_no_tag(self): + self.assertRaises(TypeError, self._lsp_add, self.switch.uuid, + None, parent="fake_parent") + + def test_lsp_add_parent_may_exist(self): + lsp1 = self._lsp_add(self.switch.uuid, None) + lsp2 = self._lsp_add(self.switch.uuid, None, parent=lsp1.name, tag=0) + lsp3 = self._lsp_add(self.switch.uuid, lsp2.name, parent=lsp1.name, + tag=0, may_exist=True) + self.assertEqual(lsp2, lsp3) + + def test_lsp_add_parent_may_exist_no_parent(self): + lsp1 = self._lsp_add(self.switch.uuid, None) + self.assertRaises(RuntimeError, self._lsp_add, self.switch.uuid, + lsp1.name, parent="fake_parent", tag=0, + may_exist=True) + + def test_lsp_add_parent_may_exist_different_parent(self): + lsp1 = self._lsp_add(self.switch.uuid, None) + lsp2 = self._lsp_add(self.switch.uuid, None, parent=lsp1.name, tag=0) + self.assertRaises(RuntimeError, self._lsp_add, self.switch.uuid, + lsp2.name, parent="fake_parent", tag=0, + may_exist=True) + + def test_lsp_add_parent_may_exist_different_tag(self): + lsp1 = self._lsp_add(self.switch.uuid, None) + lsp2 = self._lsp_add(self.switch.uuid, None, parent=lsp1.name, tag=0) + self.assertRaises(RuntimeError, self._lsp_add, self.switch.uuid, + lsp2.name, parent=lsp1.name, tag=1, may_exist=True) + + def test_lsp_add_may_exist_existing_parent(self): + lsp1 = self._lsp_add(self.switch.uuid, None) + lsp2 = self._lsp_add(self.switch.uuid, None, parent=lsp1.name, tag=0) + self.assertRaises(RuntimeError, self._lsp_add, self.switch.uuid, + lsp2.name, may_exist=True) + + def test_lsp_add_columns(self): + options = {'myside': 'yourside'} + external_ids = {'myside': 'yourside'} + lsp = self._lsp_add(self.switch.uuid, None, options=options, + external_ids=external_ids) + self.assertEqual(options, lsp.options) + self.assertEqual(external_ids, lsp.external_ids) + + def test_lsp_del_uuid(self): + lsp = self._lsp_add(self.switch.uuid, None) + self.api.lsp_del(lsp.uuid).execute(check_error=True) + self.assertNotIn(lsp, self.switch.ports) + + def test_lsp_del_name(self): + lsp = self._lsp_add(self.switch.uuid, None) + self.api.lsp_del(lsp.name).execute(check_error=True) + self.assertNotIn(lsp, self.switch.ports) + + def test_lsp_del_switch(self): + lsp = self._lsp_add(self.switch.uuid, None) + self.api.lsp_del(lsp.uuid, self.switch.uuid).execute(check_error=True) + self.assertNotIn(lsp, self.switch.ports) + + def test_lsp_del_switch_name(self): + lsp = self._lsp_add(self.switch.uuid, None) + self.api.lsp_del(lsp.uuid, + self.switch.name).execute(check_error=True) + self.assertNotIn(lsp, self.switch.ports) + + def test_lsp_del_wrong_switch(self): + lsp = self._lsp_add(self.switch.uuid, None) + sw_id = self.useFixture(fixtures.LogicalSwitchFixture()).obj + cmd = self.api.lsp_del(lsp.uuid, sw_id) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lsp_del_switch_no_exist(self): + lsp = self._lsp_add(self.switch.uuid, None) + cmd = self.api.lsp_del(lsp.uuid, utils.get_rand_device_name()) + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lsp_del_no_exist(self): + cmd = self.api.lsp_del("fake_port") + self.assertRaises(RuntimeError, cmd.execute, check_error=True) + + def test_lsp_del_if_exist(self): + self.api.lsp_del("fake_port", if_exists=True).execute(check_error=True) + + def test_lsp_list(self): + ports = {self._lsp_add(self.switch.uuid, None) for _ in range(3)} + port_set = set(self.api.lsp_list(self.switch.uuid).execute( + check_error=True)) + self.assertTrue(ports.issubset(port_set)) + + def test_lsp_get_parent(self): + ls1 = self._lsp_add(self.switch.uuid, None) + ls2 = self._lsp_add(self.switch.uuid, None, parent=ls1.name, tag=0) + self.assertEqual( + ls1.name, self.api.lsp_get_parent(ls2.name).execute( + check_error=True)) + + def test_lsp_get_tag(self): + ls1 = self._lsp_add(self.switch.uuid, None) + ls2 = self._lsp_add(self.switch.uuid, None, parent=ls1.name, tag=0) + self.assertIsInstance(self.api.lsp_get_tag(ls2.uuid).execute( + check_error=True), int) + + def test_lsp_set_addresses(self): + lsp = self._lsp_add(self.switch.uuid, None) + for addr in ('dynamic', 'unknown', 'router', + 'de:ad:be:ef:4d:ad 192.0.2.1'): + self.api.lsp_set_addresses(lsp.name, [addr]).execute( + check_error=True) + self.assertEqual([addr], lsp.addresses) + + def test_lsp_set_addresses_invalid(self): + self.assertRaises( + TypeError, + self.api.lsp_set_addresses, 'fake', '01:02:03:04:05:06') + + def test_lsp_get_addresses(self): + addresses = [ + '01:02:03:04:05:06 192.0.2.1', + 'de:ad:be:ef:4d:ad 192.0.2.2'] + lsp = self._lsp_add(self.switch.uuid, None) + self.api.lsp_set_addresses( + lsp.name, addresses).execute(check_error=True) + self.assertEqual(set(addresses), set(self.api.lsp_get_addresses( + lsp.name).execute(check_error=True))) + + def test_lsp_get_set_port_security(self): + port_security = [ + '01:02:03:04:05:06 192.0.2.1', + 'de:ad:be:ef:4d:ad 192.0.2.2'] + lsp = self._lsp_add(self.switch.uuid, None) + self.api.lsp_set_port_security(lsp.name, port_security).execute( + check_error=True) + ps = self.api.lsp_get_port_security(lsp.name).execute( + check_error=True) + self.assertEqual(port_security, ps) + + def test_lsp_get_up(self): + lsp = self._lsp_add(self.switch.uuid, None) + self.assertFalse(self.api.lsp_get_up(lsp.name).execute( + check_error=True)) + + def test_lsp_get_set_enabled(self): + lsp = self._lsp_add(self.switch.uuid, None) + # default is True + self.assertTrue(self.api.lsp_get_enabled(lsp.name).execute( + check_error=True)) + self.api.lsp_set_enabled(lsp.name, False).execute(check_error=True) + self.assertFalse(self.api.lsp_get_enabled(lsp.name).execute( + check_error=True)) + self.api.lsp_set_enabled(lsp.name, True).execute(check_error=True) + self.assertTrue(self.api.lsp_get_enabled(lsp.name).execute( + check_error=True)) + + def test_lsp_get_set_type(self): + type_ = 'router' + lsp = self._lsp_add(self.switch.uuid, None) + self.api.lsp_set_type(lsp.uuid, type_).execute(check_error=True) + self.assertEqual(type_, self.api.lsp_get_type(lsp.uuid).execute( + check_error=True)) + + def test_lsp_get_set_options(self): + options = {'one': 'two', 'three': 'four'} + lsp = self._lsp_add(self.switch.uuid, None) + self.api.lsp_set_options(lsp.uuid, **options).execute( + check_error=True) + self.assertEqual(options, self.api.lsp_get_options(lsp.uuid).execute( + check_error=True)) + + def test_lsp_set_get_dhcpv4_options(self): + lsp = self._lsp_add(self.switch.uuid, None) + dhcpopt = self.useFixture( + fixtures.DhcpOptionsFixture('192.0.2.1/24')).obj + self.api.lsp_set_dhcpv4_options( + lsp.name, dhcpopt.uuid).execute(check_error=True) + options = self.api.lsp_get_dhcpv4_options( + lsp.uuid).execute(check_error=True) + self.assertEqual(dhcpopt, options) + + +class TestDhcpOptionsOps(OvnNorthboundTest): + def _dhcpopt_add(self, cidr, *args, **kwargs): + dhcpopt = self.useFixture(fixtures.DhcpOptionsFixture( + cidr, *args, **kwargs)).obj + self.assertEqual(cidr, dhcpopt.cidr) + return dhcpopt + + def test_dhcp_options_add(self): + self._dhcpopt_add('192.0.2.1/24') + + def test_dhcp_options_add_v6(self): + self._dhcpopt_add('2001:db8::1/32') + + def test_dhcp_options_invalid_cidr(self): + self.assertRaises(netaddr.AddrFormatError, self.api.dhcp_options_add, + '256.0.0.1/24') + + def test_dhcp_options_add_ext_ids(self): + ext_ids = {'subnet-id': '1', 'other-id': '2'} + dhcpopt = self._dhcpopt_add('192.0.2.1/24', **ext_ids) + self.assertEqual(ext_ids, dhcpopt.external_ids) + + def test_dhcp_options_list(self): + dhcpopts = {self._dhcpopt_add('192.0.2.1/24') for d in range(3)} + dhcpopts_set = set( + self.api.dhcp_options_list().execute(check_error=True)) + self.assertTrue(dhcpopts.issubset(dhcpopts_set)) + + def test_dhcp_options_get_set_options(self): + dhcpopt = self._dhcpopt_add('192.0.2.1/24') + options = {'a': 'one', 'b': 'two'} + self.api.dhcp_options_set_options( + dhcpopt.uuid, **options).execute(check_error=True) + cmd = self.api.dhcp_options_get_options(dhcpopt.uuid) + self.assertEqual(options, cmd.execute(check_error=True)) diff --git a/ovsdbapp/tests/unit/backend/test_ovs_idl.py b/ovsdbapp/tests/unit/backend/test_ovs_idl.py new file mode 100644 index 00000000..7c58e3ea --- /dev/null +++ b/ovsdbapp/tests/unit/backend/test_ovs_idl.py @@ -0,0 +1,46 @@ +# 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 ovsdbapp.backend import ovs_idl +from ovsdbapp.backend.ovs_idl import idlutils +from ovsdbapp.tests import base + + +class FakeRow(object): + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + + +class FakeTable(object): + rows = {'fake-id-1': FakeRow(uuid='fake-id-1', name='Fake1')} + indexes = [] + + +class TestBackendOvsIdl(base.TestCase): + def setUp(self): + super(TestBackendOvsIdl, self).setUp() + self.backend = ovs_idl.Backend() + self.backend.tables = {'Faketable': FakeTable()} + self.backend.lookup_table = { + 'Faketable': idlutils.RowLookup('Faketable', 'name', None)} + + def test_lookup_found(self): + row = self.backend.lookup('Faketable', 'Fake1') + self.assertEqual('Fake1', row.name) + + def test_lookup_not_found(self): + self.assertRaises(idlutils.RowNotFound, self.backend.lookup, + 'Faketable', 'notthere') + + def test_lookup_not_found_default(self): + row = self.backend.lookup('Faketable', 'notthere', "NOT_FOUND") + self.assertEqual(row, "NOT_FOUND") diff --git a/ovsdbapp/venv.py b/ovsdbapp/venv.py new file mode 100644 index 00000000..d94ce97f --- /dev/null +++ b/ovsdbapp/venv.py @@ -0,0 +1,160 @@ +# 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 glob +import os +import shutil +import signal +import subprocess +import time + +import fixtures + + +class OvsVenvFixture(fixtures.Fixture): + def __init__(self, venv, ovsdir, dummy=None, remove=False): + if not os.path.isdir(ovsdir): + raise Exception("%s is not a directory" % ovsdir) + self.venv = venv + self.ovsdir = ovsdir + self._dummy = dummy + self.remove = remove + self.env = {'OVS_RUNDIR': self.venv, 'OVS_LOGDIR': self.venv, + 'OVS_DBDIR': self.venv, 'OVS_SYSCONFDIR': self.venv, + 'PATH': "{0}/ovsdb:{0}/vswitchd:{0}/utilities:{0}/vtep:" + "{0}/ovn/controller:{0}/ovn/controller-vtep:" + "{0}/ovn/northd:{0}/ovn/utilities:{1}:".format( + self.ovsdir, os.getenv('PATH'))} + + @property + def ovs_schema(self): + return os.path.join(self.ovsdir, 'vswitchd', 'vswitch.ovsschema') + + @property + def ovnsb_schema(self): + return os.path.join(self.ovsdir, 'ovn', 'ovn-sb.ovsschema') + + @property + def ovnnb_schema(self): + return os.path.join(self.ovsdir, 'ovn', 'ovn-nb.ovsschema') + + @property + def vtep_schema(self): + return os.path.join(self.ovsdir, 'vtep', 'vtep.ovsschema') + + @property + def dummy_arg(self): + if self._dummy == 'override': + return "--enable-dummy=override" + elif self._dummy == 'system': + return "--enable-dummy=system" + else: + return "--enable-dummy=" + + @property + def ovs_connection(self): + return 'unix:' + os.path.join(self.venv, 'db.sock') + + @property + def ovnnb_connection(self): + return 'unix:' + os.path.join(self.venv, 'ovnnb_db.sock') + + @property + def ovnsb_connection(self): + return 'unix:' + os.path.join(self.venv, 'ovnsb_db.sock') + + def _setUp(self): + self.addCleanup(self.deactivate) + if not os.path.isdir(self.venv): + os.mkdir(self.venv) + self.create_db('conf.db', self.ovs_schema) + self.create_db('ovnsb.db', self.ovnsb_schema) + self.create_db('ovnnb.db', self.ovnnb_schema) + self.create_db('vtep.db', self.vtep_schema) + self.call(['ovsdb-server', + '--remote=p' + self.ovs_connection, + '--detach', '--no-chdir', '--pidfile', '-vconsole:off', + '--log-file', 'vtep.db', 'conf.db']) + self.call(['ovsdb-server', '--detach', '--no-chdir', '-vconsole:off', + '--pidfile=%s' % os.path.join(self.venv, 'ovnnb_db.pid'), + '--log-file=%s' % os.path.join(self.venv, 'ovnnb_db.log'), + '--remote=db:OVN_Northbound,NB_Global,connections', + '--private-key=db:OVN_Northbound,SSL,private_key', + '--certificate=db:OVN_Northbound,SSL,certificate', + '--ca-cert=db:OVN_Northbound,SSL,ca_cert', + '--ssl-protocols=db:OVN_Northbound,SSL,ssl_protocols', + '--ssl-ciphers=db:OVN_Northbound,SSL,ssl_ciphers', + '--remote=p' + self.ovnnb_connection, 'ovnnb.db']) + self.call(['ovsdb-server', '--detach', '--no-chdir', '-vconsole:off', + '--pidfile=%s' % os.path.join(self.venv, 'ovnsb_db.pid'), + '--log-file=%s' % os.path.join(self.venv, 'ovnsb_db.log'), + '--remote=db:OVN_Southbound,SB_Global,connections', + '--private-key=db:OVN_Southbound,SSL,private_key', + '--certificate=db:OVN_Southbound,SSL,certificate', + '--ca-cert=db:OVN_Southbound,SSL,ca_cert', + '--ssl-protocols=db:OVN_Southbound,SSL,ssl_protocols', + '--ssl-ciphers=db:OVN_Southbound,SSL,ssl_ciphers', + '--remote=p' + self.ovnsb_connection, 'ovnsb.db']) + time.sleep(1) # wait_until_true(os.path.isfile(db_sock) + self.call(['ovs-vsctl', '--no-wait', '--', 'init']) + self.call(['ovs-vswitchd', '--detach', '--no-chdir', '--pidfile', + '-vconsole:off', '-vvconn', '-vnetdev_dummy', '--log-file', + self.dummy_arg, self.ovs_connection]) + self.call(['ovn-nbctl', 'init']) + self.call(['ovn-sbctl', 'init']) + self.call([ + 'ovs-vsctl', 'set', 'open', '.', + 'external_ids:system-id=56b18105-5706-46ef-80c4-ff20979ab068', + 'external_ids:hostname=sandbox', + 'external_ids:ovn-encap-type=geneve', + 'external_ids:ovn-encap-ip=127.0.0.1']) + # TODO(twilson) SSL stuff + if False: + pass + else: + self.call(['ovs-vsctl', 'set', 'open', '.', + 'external_ids:ovn-remote=' + self.ovnsb_connection]) + self.call(['ovn-northd', '--detach', '--no-chdir', '--pidfile', + '-vconsole:off', '--log-file', + '--ovnsb-db=' + self.ovnsb_connection, + '--ovnnb-db=' + self.ovnnb_connection]) + self.call(['ovn-controller', '--detach', '--no-chdir', '--pidfile', + '-vconsole:off', '--log-file']) + self.call(['ovn-controller-vtep', '--detach', '--no-chdir', + '--pidfile', '-vconsole:off', '--log-file', + '--ovnsb-db=' + self.ovnsb_connection]) + + def deactivate(self): + self.kill_processes() + if self.remove: + shutil.rmtree(self.venv, ignore_errors=True) + + def create_db(self, name, schema): + filename = os.path.join(self.venv, name) + if not os.path.isfile(filename): + return self.call(['ovsdb-tool', '-v', 'create', name, schema]) + + def call(self, *args, **kwargs): + cwd = kwargs.pop('cwd', self.venv) + return subprocess.check_call(*args, env=self.env, cwd=cwd, **kwargs) + + def get_pids(self): + files = glob.glob(os.path.join(self.venv, "*.pid")) + result = [] + for fname in files: + with open(fname, 'r') as f: + result.append(int(f.read().strip())) + return result + + def kill_processes(self): + for pid in self.get_pids(): + os.kill(pid, signal.SIGTERM) diff --git a/requirements.txt b/requirements.txt index 6269b434..ac92f454 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,5 +3,7 @@ # process, which may cause wedges in the gate later. fixtures>=3.0.0 # Apache-2.0/BSD +netaddr>=0.7.13,!=0.7.16 # BSD ovs>=2.7.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0 +six>=1.9.0 # MIT diff --git a/tools/tox_install.sh b/tools/tox_install.sh index 07c77072..a6b45e1e 100755 --- a/tools/tox_install.sh +++ b/tools/tox_install.sh @@ -48,5 +48,14 @@ fi # install will be constrained and we need to unconstrain it. edit-constraints $localfile -- $LIB_NAME "-e file://$PWD#egg=$LIB_NAME" +# We require at least OVS 2.7. Testing infrastructure doesn't support it yet, +# so build it. Eventually, we should run some checks to see what is actually +# installed and see if we can use it instead. +if [ "$OVS_SRCDIR" -a ! -d "$OVS_SRCDIR" ]; then + echo "Building OVS in $OVS_SRCDIR" + mkdir -p $OVS_SRCDIR + git clone git://github.com/openvswitch/ovs.git $OVS_SRCDIR + (cd $OVS_SRCDIR && ./boot.sh && PYTHON=/usr/bin/python ./configure && make -j$(($(nproc) + 1))) +fi $install_cmd -U $* exit $? diff --git a/tox.ini b/tox.ini index d4576be5..4efac20a 100644 --- a/tox.ini +++ b/tox.ini @@ -37,6 +37,7 @@ commands = oslo_debug_helper {posargs} [testenv:functional] setenv = {[testenv]setenv} OS_TEST_PATH=./ovsdbapp/tests/functional + OVS_SRCDIR={envdir}/src/ovs [flake8] # E123, E125 skipped as they are invalid PEP-8.