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:
commit
5ee145cc87
0
__init__.py
Normal file
0
__init__.py
Normal file
313
api.py
Normal file
313
api.py
Normal 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
285
impl_vsctl.py
Normal 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
|
Loading…
Reference in New Issue
Block a user