Reorganize OVSDB API

This reorganization makes adding a second implementation of the
API a lot cleaner.

Partially-Implements: blueprint vsctl-to-ovsdb
Change-Id: Ice869f33b6023d3693ad732276267621912c691b
This commit is contained in:
Terry Wilson 2015-01-22 01:55:17 -06:00
commit 5ee145cc87
3 changed files with 598 additions and 0 deletions

0
__init__.py Normal file
View File

313
api.py Normal file
View File

@ -0,0 +1,313 @@
# Copyright (c) 2014 Openstack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import abc
from oslo_config import cfg
from oslo_utils import importutils
import six
interface_map = {
'vsctl': 'neutron.agent.ovsdb.impl_vsctl.OvsdbVsctl',
}
OPTS = [
cfg.StrOpt('ovsdb_interface',
choices=interface_map.keys(),
default='vsctl',
help=_('The interface for interacting with the OVSDB')),
]
cfg.CONF.register_opts(OPTS, 'OVS')
@six.add_metaclass(abc.ABCMeta)
class Command(object):
"""An OSVDB command that can be executed in a transaction
:attr result: The result of executing the command in a transaction
"""
@abc.abstractmethod
def execute(self, **transaction_options):
"""Immediately execute an OVSDB command
This implicitly creates a transaction with the passed options and then
executes it, returning the value of the executed transaction
:param transaction_options: Options to pass to the transaction
"""
@six.add_metaclass(abc.ABCMeta)
class Transaction(object):
@abc.abstractmethod
def commit(self):
"""Commit the transaction to OVSDB"""
@abc.abstractmethod
def add(self, command):
"""Append an OVSDB operation to the transaction"""
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, tb):
if exc_type is None:
self.result = self.commit()
@six.add_metaclass(abc.ABCMeta)
class API(object):
def __init__(self, context):
self.context = context
@staticmethod
def get(context, iface_name=None):
"""Return the configured OVSDB API implementation"""
iface = importutils.import_class(
interface_map[iface_name or cfg.CONF.OVS.ovsdb_interface])
return iface(context)
@abc.abstractmethod
def transaction(self, check_error=False, log_errors=True, **kwargs):
"""Create a transaction
:param check_error: Allow the transaction to raise an exception?
:type check_error: bool
:param log_errors: Log an error if the transaction fails?
:type log_errors: bool
:returns: A new transaction
:rtype: :class:`Transaction`
"""
@abc.abstractmethod
def add_br(self, name, may_exist=True):
"""Create an command to add an OVS bridge
:param name: The name of the bridge
:type name: string
:param may_exist: Do not fail if bridge already exists
:type may_exist: bool
:returns: :class:`Command` with no result
"""
@abc.abstractmethod
def del_br(self, name, if_exists=True):
"""Create a command to delete an OVS bridge
:param name: The name of the bridge
:type name: string
:param if_exists: Do not fail if the bridge does not exist
:type if_exists: bool
:returns: :class:`Command` with no result
"""
@abc.abstractmethod
def br_exists(self, name):
"""Create a command to check if an OVS bridge exists
:param name: The name of the bridge
:type name: string
:returns: :class:`Command` with bool result
"""
@abc.abstractmethod
def port_to_br(self, name):
"""Create a command to return the name of the bridge with the port
:param name: The name of the OVS port
:type name: string
:returns: :class:`Command` with bridge name result
"""
@abc.abstractmethod
def iface_to_br(self, name):
"""Create a command to return the name of the bridge with the interface
:param name: The name of the OVS interface
:type name: string
:returns: :class:`Command` with bridge name result
"""
@abc.abstractmethod
def list_br(self):
"""Create a command to return the current list of OVS bridge names
:returns: :class:`Command` with list of bridge names result
"""
@abc.abstractmethod
def br_get_external_id(self, name, field):
"""Create a command to return a field from the Bridge's external_ids
:param name: The name of the OVS Bridge
:type name: string
:param field: The external_ids field to return
:type field: string
:returns: :class:`Command` with field value result
"""
@abc.abstractmethod
def db_set(self, table, record, *col_values):
"""Create a command to set fields in a record
:param table: The OVS table containing the record to be modified
:type table: string
:param record: The record id (name/uuid) to be modified
:type table: string
:param col_values: The columns and their associated values
:type col_values: Tuples of (column, value). Values may be atomic
values or unnested sequences/mappings
:returns: :class:`Command` with no result
"""
# TODO(twilson) Consider handling kwargs for arguments where order
# doesn't matter. Though that would break the assert_called_once_with
# unit tests
@abc.abstractmethod
def db_clear(self, table, record, column):
"""Create a command to clear a field's value in a record
:param table: The OVS table containing the record to be modified
:type table: string
:param record: The record id (name/uuid) to be modified
:type record: string
:param column: The column whose value should be cleared
:type column: string
:returns: :class:`Command` with no result
"""
@abc.abstractmethod
def db_get(self, table, record, column):
"""Create a command to return a field's value in a record
:param table: The OVS table containing the record to be queried
:type table: string
:param record: The record id (name/uuid) to be queried
:type record: string
:param column: The column whose value should be returned
:type column: string
:returns: :class:`Command` with the field's value result
"""
@abc.abstractmethod
def db_list(self, table, records=None, columns=None, if_exists=False):
"""Create a command to return a list of OVSDB records
:param table: The OVS table to query
:type table: string
:param records: The records to return values from
:type records: list of record ids (names/uuids)
:param columns: Limit results to only columns, None means all columns
:type columns: list of column names or None
:param if_exists: Do not fail if the bridge does not exist
:type if_exists: bool
:returns: :class:`Command` with [{'column', value}, ...] result
"""
@abc.abstractmethod
def db_find(self, table, *conditions, **kwargs):
"""Create a command to return find OVSDB records matching conditions
:param table: The OVS table to query
:type table: string
:param conditions:The conditions to satisfy the query
:type conditions: 3-tuples containing (column, operation, match)
Examples:
atomic: ('tag', '=', 7)
map: ('external_ids' '=', {'iface-id': 'xxx'})
field exists?
('external_ids', '!=', {'iface-id', ''})
set contains?:
('protocols', '{>=}', 'OpenFlow13')
See the ovs-vsctl man page for more operations
:param columns: Limit results to only columns, None means all columns
:type columns: list of column names or None
:returns: :class:`Command` with [{'column', value}, ...] result
"""
@abc.abstractmethod
def set_controller(self, bridge, controllers):
"""Create a command to set an OVS bridge's OpenFlow controllers
:param bridge: The name of the bridge
:type bridge: string
:param controllers: The controller strings
:type controllers: list of strings, see ovs-vsctl manpage for format
:returns: :class:`Command` with no result
"""
@abc.abstractmethod
def del_controller(self, bridge):
"""Create a command to clear an OVS bridge's OpenFlow controllers
:param bridge: The name of the bridge
:type bridge: string
:returns: :class:`Command` with no result
"""
@abc.abstractmethod
def get_controller(self, bridge):
"""Create a command to return an OVS bridge's OpenFlow controllers
:param bridge: The name of the bridge
:type bridge: string
:returns: :class:`Command` with list of controller strings result
"""
@abc.abstractmethod
def set_fail_mode(self, bridge, mode):
"""Create a command to set an OVS bridge's failure mode
:param bridge: The name of the bridge
:type bridge: string
:param mode: The failure mode
:type mode: "secure" or "standalone"
:returns: :class:`Command` with no result
"""
@abc.abstractmethod
def add_port(self, bridge, port, may_exist=True):
"""Create a command to add a port to an OVS bridge
:param bridge: The name of the bridge
:type bridge: string
:param port: The name of the port
:type port: string
:param may_exist: Do not fail if bridge already exists
:type may_exist: bool
:returns: :class:`Command` with no result
"""
@abc.abstractmethod
def del_port(self, port, bridge=None, if_exists=True):
"""Create a command to delete a port an OVS port
:param port: The name of the port
:type port: string
:param bridge: Only delete port if it is attached to this bridge
:type bridge: string
:param if_exists: Do not fail if the bridge does not exist
:type if_exists: bool
:returns: :class:`Command` with no result
"""
@abc.abstractmethod
def list_ports(self, bridge):
"""Create a command to list the names of porsts on a bridge
:param bridge: The name of the bridge
:type bridge: string
:returns: :class:`Command` with list of port names result
"""

285
impl_vsctl.py Normal file
View File

@ -0,0 +1,285 @@
# Copyright (c) 2014 Openstack Foundation
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import collections
import itertools
import uuid
from oslo_serialization import jsonutils
from oslo_utils import excutils
from neutron.agent.linux import utils
from neutron.agent.ovsdb import api as ovsdb
from neutron.i18n import _LE
from neutron.openstack.common import log as logging
LOG = logging.getLogger(__name__)
class Transaction(ovsdb.Transaction):
def __init__(self, context, check_error=False, log_errors=True, opts=None):
self.context = context
self.check_error = check_error
self.log_errors = log_errors
self.opts = ["--timeout=%d" % self.context.vsctl_timeout,
'--oneline', '--format=json']
if opts:
self.opts += opts
self.commands = []
def add(self, command):
self.commands.append(command)
return command
def commit(self):
args = []
for cmd in self.commands:
cmd.result = None
args += cmd.vsctl_args()
res = self.run_vsctl(args)
if res is None:
return
res = res.replace(r'\\', '\\').splitlines()
for i, record in enumerate(res):
self.commands[i].result = record
return [cmd.result for cmd in self.commands]
def run_vsctl(self, args):
full_args = ["ovs-vsctl"] + self.opts + args
try:
# We log our own errors, so never have utils.execute do it
return utils.execute(full_args,
root_helper=self.context.root_helper,
log_fail_as_error=False).rstrip()
except Exception:
with excutils.save_and_reraise_exception() as ctxt:
if self.log_errors:
LOG.exception(_LE("Unable to execute %(cmd)s."),
{'cmd': full_args})
if not self.check_error:
ctxt.reraise = False
class BaseCommand(ovsdb.Command):
def __init__(self, context, cmd, opts=None, args=None):
self.context = context
self.cmd = cmd
self.opts = [] if opts is None else opts
self.args = [] if args is None else args
def execute(self, check_error=False, log_errors=True):
with Transaction(self.context, check_error=check_error,
log_errors=log_errors) as txn:
txn.add(self)
return self.result
def vsctl_args(self):
return itertools.chain(('--',), self.opts, (self.cmd,), self.args)
class MultiLineCommand(BaseCommand):
"""Command for ovs-vsctl commands that return multiple lines"""
@property
def result(self):
return self._result
@result.setter
def result(self, raw_result):
self._result = raw_result.split(r'\n') if raw_result else []
class DbCommand(BaseCommand):
def __init__(self, context, cmd, opts=None, args=None, columns=None):
if opts is None:
opts = []
if columns:
opts += ['--columns=%s' % ",".join(columns)]
super(DbCommand, self).__init__(context, cmd, opts, args)
@property
def result(self):
return self._result
@result.setter
def result(self, raw_result):
# If check_error=False, run_vsctl can return None
if not raw_result:
self._result = None
return
try:
json = jsonutils.loads(raw_result)
except (ValueError, TypeError):
# This shouldn't happen, but if it does and we check_errors
# log and raise.
with excutils.save_and_reraise_exception():
LOG.exception(_LE("Could not parse: %s"), raw_result)
headings = json['headings']
data = json['data']
results = []
for record in data:
obj = {}
for pos, heading in enumerate(headings):
obj[heading] = _val_to_py(record[pos])
results.append(obj)
self._result = results
class DbGetCommand(DbCommand):
@DbCommand.result.setter
def result(self, val):
# super()'s never worked for setters http://bugs.python.org/issue14965
DbCommand.result.fset(self, val)
# DbCommand will return [{'column': value}] and we just want value.
if self._result:
self._result = self._result[0].values()[0]
class BrExistsCommand(DbCommand):
@DbCommand.result.setter
def result(self, val):
self._result = val is not None
def execute(self):
return super(BrExistsCommand, self).execute(check_error=False,
log_errors=False)
class OvsdbVsctl(ovsdb.API):
def transaction(self, check_error=False, log_errors=True, **kwargs):
return Transaction(self.context, check_error, log_errors, **kwargs)
def add_br(self, name, may_exist=True):
opts = ['--may-exist'] if may_exist else None
return BaseCommand(self.context, 'add-br', opts, [name])
def del_br(self, name, if_exists=True):
opts = ['--if-exists'] if if_exists else None
return BaseCommand(self.context, 'del-br', opts, [name])
def br_exists(self, name):
return BrExistsCommand(self.context, 'list', args=['Bridge', name])
def port_to_br(self, name):
return BaseCommand(self.context, 'port-to-br', args=[name])
def iface_to_br(self, name):
return BaseCommand(self.context, 'iface-to-br', args=[name])
def list_br(self):
return MultiLineCommand(self.context, 'list-br')
def br_get_external_id(self, name, field):
return BaseCommand(self.context, 'br-get-external-id',
args=[name, field])
def db_set(self, table, record, *col_values):
args = [table, record]
args += _set_colval_args(*col_values)
return BaseCommand(self.context, 'set', args=args)
def db_clear(self, table, record, column):
return BaseCommand(self.context, 'clear', args=[table, record,
column])
def db_get(self, table, record, column):
# Use the 'list' command as it can return json and 'get' cannot so that
# we can get real return types instead of treating everything as string
# NOTE: openvswitch can return a single atomic value for fields that
# are sets, but only have one value. This makes directly iterating over
# the result of a db_get() call unsafe.
return DbGetCommand(self.context, 'list', args=[table, record],
columns=[column])
def db_list(self, table, records=None, columns=None, if_exists=False):
opts = ['--if-exists'] if if_exists else None
args = [table]
if records:
args += records
return DbCommand(self.context, 'list', opts=opts, args=args,
columns=columns)
def db_find(self, table, *conditions, **kwargs):
columns = kwargs.pop('columns', None)
args = itertools.chain([table],
*[_set_colval_args(c) for c in conditions])
return DbCommand(self.context, 'find', args=args, columns=columns)
def set_controller(self, bridge, controllers):
return BaseCommand(self.context, 'set-controller',
args=[bridge] + list(controllers))
def del_controller(self, bridge):
return BaseCommand(self.context, 'del-controller', args=[bridge])
def get_controller(self, bridge):
return MultiLineCommand(self.context, 'get-controller', args=[bridge])
def set_fail_mode(self, bridge, mode):
return BaseCommand(self.context, 'set-fail-mode', args=[bridge, mode])
def add_port(self, bridge, port, may_exist=True):
opts = ['--may-exist'] if may_exist else None
return BaseCommand(self.context, 'add-port', opts, [bridge, port])
def del_port(self, port, bridge=None, if_exists=True):
opts = ['--if-exists'] if if_exists else None
args = filter(None, [bridge, port])
return BaseCommand(self.context, 'del-port', opts, args)
def list_ports(self, bridge):
return MultiLineCommand(self.context, 'list-ports', args=[bridge])
def _set_colval_args(*col_values):
args = []
# TODO(twilson) This is ugly, but set/find args are very similar except for
# op. Will try to find a better way to default this op to '='
for entry in col_values:
if len(entry) == 2:
col, op, val = entry[0], '=', entry[1]
else:
col, op, val = entry
if isinstance(val, collections.Mapping):
args += ["%s:%s%s%s" % (
col, k, op, _py_to_val(v)) for k, v in val.items()]
elif (isinstance(val, collections.Sequence)
and not isinstance(val, basestring)):
args.append("%s%s%s" % (col, op, ",".join(map(_py_to_val, val))))
else:
args.append("%s%s%s" % (col, op, _py_to_val(val)))
return args
def _val_to_py(val):
"""Convert a json ovsdb return value to native python object"""
if isinstance(val, collections.Sequence) and len(val) == 2:
if val[0] == "uuid":
return uuid.UUID(val[1])
elif val[0] == "set":
return [_val_to_py(x) for x in val[1]]
elif val[0] == "map":
return {_val_to_py(x): _val_to_py(y) for x, y in val[1]}
return val
def _py_to_val(pyval):
"""Convert python value to ovs-vsctl value argument"""
if isinstance(pyval, bool):
return 'true' if pyval is True else 'false'
elif pyval == '':
return '""'
else:
return pyval