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
This commit is contained in:
Terry Wilson 2017-06-02 16:19:37 -05:00
parent 0823a67e6e
commit 4bde2d5237
19 changed files with 1804 additions and 11 deletions

View File

@ -4,3 +4,9 @@
openvswitch [platform:rpm test] openvswitch [platform:rpm test]
openvswitch-switch [platform:dpkg test] openvswitch-switch [platform:dpkg test]
curl [test] curl [test]
autoconf [test]
automake [test]
libtool [test]
gcc [test]
make [test]
patch [test]

View File

@ -10,7 +10,30 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import uuid
from ovsdbapp.backend.ovs_idl import command as cmd 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): class Backend(object):
@ -37,3 +60,49 @@ class Backend(object):
def db_find(self, table, *conditions, **kwargs): def db_find(self, table, *conditions, **kwargs):
return cmd.DbFindCommand(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

View File

@ -13,5 +13,8 @@
# under the License. # under the License.
DEFAULT_OVSDB_CONNECTION = 'tcp:127.0.0.1:6640' DEFAULT_OVSDB_CONNECTION = 'tcp:127.0.0.1:6640'
DEFAULT_OVNNB_CONNECTION = 'tcp:127.0.0.1:6641'
DEFAULT_TIMEOUT = 5 DEFAULT_TIMEOUT = 5
DEVICE_NAME_MAX_LEN = 14 DEVICE_NAME_MAX_LEN = 14
ACL_PRIORITY_MAX = 32767

View File

@ -15,7 +15,7 @@
import six import six
class OvsdbAppException(Exception): class OvsdbAppException(RuntimeError):
"""Base OvsdbApp Exception. """Base OvsdbApp Exception.
To correctly use this class, inherit from it and define To correctly use this class, inherit from it and define
@ -52,3 +52,10 @@ class OvsdbAppException(Exception):
class TimeoutException(OvsdbAppException): class TimeoutException(OvsdbAppException):
message = "Commands %(commands)s exceeded timeout %(timeout)d seconds" 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.")

View File

@ -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
"""

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -13,23 +13,17 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from ovsdbapp.backend.ovs_idl import connection
from ovsdbapp import constants
from ovsdbapp.schema.open_vswitch import impl_idl from ovsdbapp.schema.open_vswitch import impl_idl
from ovsdbapp.tests import base from ovsdbapp.tests.functional import base
from ovsdbapp.tests import utils 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.FunctionalTestCase):
class TestOvsdbIdl(base.TestCase): schema = "Open_vSwitch"
def setUp(self): def setUp(self):
super(TestOvsdbIdl, self).setUp() 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() self.brname = utils.get_rand_device_name()
# Destroying the bridge cleans up most things created by tests # Destroying the bridge cleans up most things created by tests
cleanup_cmd = self.api.del_br(self.brname) cleanup_cmd = self.api.del_br(self.brname)

View File

@ -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'

View File

@ -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))

View File

@ -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")

160
ovsdbapp/venv.py Normal file
View File

@ -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)

View File

@ -3,5 +3,7 @@
# process, which may cause wedges in the gate later. # process, which may cause wedges in the gate later.
fixtures>=3.0.0 # Apache-2.0/BSD fixtures>=3.0.0 # Apache-2.0/BSD
netaddr>=0.7.13,!=0.7.16 # BSD
ovs>=2.7.0 # Apache-2.0 ovs>=2.7.0 # Apache-2.0
pbr!=2.1.0,>=2.0.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0
six>=1.9.0 # MIT

View File

@ -48,5 +48,14 @@ fi
# install will be constrained and we need to unconstrain it. # install will be constrained and we need to unconstrain it.
edit-constraints $localfile -- $LIB_NAME "-e file://$PWD#egg=$LIB_NAME" 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 $* $install_cmd -U $*
exit $? exit $?

View File

@ -37,6 +37,7 @@ commands = oslo_debug_helper {posargs}
[testenv:functional] [testenv:functional]
setenv = {[testenv]setenv} setenv = {[testenv]setenv}
OS_TEST_PATH=./ovsdbapp/tests/functional OS_TEST_PATH=./ovsdbapp/tests/functional
OVS_SRCDIR={envdir}/src/ovs
[flake8] [flake8]
# E123, E125 skipped as they are invalid PEP-8. # E123, E125 skipped as they are invalid PEP-8.