Charmed-Kubernetes/nrpe/hooks/charmhelpers/contrib/network/ovs/ovsdb.py

247 lines
8.6 KiB
Python

# Copyright 2019 Canonical Ltd
#
# 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 json
import uuid
from . import utils
class SimpleOVSDB(object):
"""Simple interface to OVSDB through the use of command line tools.
OVS and OVN is managed through a set of databases. These databases have
similar command line tools to manage them. We make use of the similarity
to provide a generic class that can be used to manage them.
The OpenvSwitch project does provide a Python API, but on the surface it
appears to be a bit too involved for our simple use case.
Examples:
sbdb = SimpleOVSDB('ovn-sbctl')
for chs in sbdb.chassis:
print(chs)
ovsdb = SimpleOVSDB('ovs-vsctl')
for br in ovsdb.bridge:
if br['name'] == 'br-test':
ovsdb.bridge.set(br['uuid'], 'external_ids:charm', 'managed')
WARNING: If a list type field only have one item `ovs-vsctl` will present
it as a single item. Since we do not know the schema we have no way of
knowing what fields should be de-serialized as lists so the caller has
to be careful of checking the type of values returned from this library.
"""
# For validation we keep a complete map of currently known good tool and
# table combinations. This requires maintenance down the line whenever
# upstream adds things that downstream wants, and the cost of maintaining
# that will most likely be lower then the cost of finding the needle in
# the haystack whenever downstream code misspells something.
_tool_table_map = {
'ovs-vsctl': (
'autoattach',
'bridge',
'ct_timeout_policy',
'ct_zone',
'controller',
'datapath',
'flow_sample_collector_set',
'flow_table',
'ipfix',
'interface',
'manager',
'mirror',
'netflow',
'open_vswitch',
'port',
'qos',
'queue',
'ssl',
'sflow',
),
'ovn-nbctl': (
'acl',
'address_set',
'connection',
'dhcp_options',
'dns',
'forwarding_group',
'gateway_chassis',
'ha_chassis',
'ha_chassis_group',
'load_balancer',
'load_balancer_health_check',
'logical_router',
'logical_router_policy',
'logical_router_port',
'logical_router_static_route',
'logical_switch',
'logical_switch_port',
'meter',
'meter_band',
'nat',
'nb_global',
'port_group',
'qos',
'ssl',
),
'ovn-sbctl': (
'address_set',
'chassis',
'connection',
'controller_event',
'dhcp_options',
'dhcpv6_options',
'dns',
'datapath_binding',
'encap',
'gateway_chassis',
'ha_chassis',
'ha_chassis_group',
'igmp_group',
'ip_multicast',
'logical_flow',
'mac_binding',
'meter',
'meter_band',
'multicast_group',
'port_binding',
'port_group',
'rbac_permission',
'rbac_role',
'sb_global',
'ssl',
'service_monitor',
),
}
def __init__(self, tool):
"""SimpleOVSDB constructor.
:param tool: Which tool with database commands to operate on.
Usually one of `ovs-vsctl`, `ovn-nbctl`, `ovn-sbctl`
:type tool: str
"""
if tool not in self._tool_table_map:
raise RuntimeError(
'tool must be one of "{}"'.format(self._tool_table_map.keys()))
self._tool = tool
def __getattr__(self, table):
if table not in self._tool_table_map[self._tool]:
raise AttributeError(
'table "{}" not known for use with "{}"'
.format(table, self._tool))
return self.Table(self._tool, table)
class Table(object):
"""Methods to interact with contents of OVSDB tables.
NOTE: At the time of this writing ``find`` is the only command
line argument to OVSDB manipulating tools that actually supports
JSON output.
"""
def __init__(self, tool, table):
"""SimpleOVSDBTable constructor.
:param table: Which table to operate on
:type table: str
"""
self._tool = tool
self._table = table
def _deserialize_ovsdb(self, data):
"""Deserialize OVSDB RFC7047 section 5.1 data.
:param data: Multidimensional list where first row contains RFC7047
type information
:type data: List[str,any]
:returns: Deserialized data.
:rtype: any
"""
# When using json formatted output to OVS commands Internal OVSDB
# notation may occur that require further deserializing.
# Reference: https://tools.ietf.org/html/rfc7047#section-5.1
ovs_type_cb_map = {
'uuid': uuid.UUID,
# NOTE: OVSDB sets have overloaded type
# see special handling below
'set': list,
'map': dict,
}
assert len(data) > 1, ('Invalid data provided, expecting list '
'with at least two elements.')
if data[0] == 'set':
# special handling for set
#
# it is either a list of strings or a list of typed lists.
# taste first element to see which it is
for el in data[1]:
# NOTE: We lock this handling down to the `uuid` type as
# that is the only one we have a practical example of.
# We could potentially just handle this generally based on
# the types listed in `ovs_type_cb_map` but let's open for
# that as soon as we have a concrete example to validate on
if isinstance(
el, list) and len(el) and el[0] == 'uuid':
decoded_set = []
for el in data[1]:
decoded_set.append(self._deserialize_ovsdb(el))
return(decoded_set)
# fall back to normal processing below
break
# Use map to deserialize data with fallback to `str`
f = ovs_type_cb_map.get(data[0], str)
return f(data[1])
def _find_tbl(self, condition=None):
"""Run and parse output of OVSDB `find` command.
:param condition: An optional RFC 7047 5.1 match condition
:type condition: Optional[str]
:returns: Dictionary with data
:rtype: Dict[str, any]
"""
cmd = [self._tool, '-f', 'json', 'find', self._table]
if condition:
cmd.append(condition)
output = utils._run(*cmd)
data = json.loads(output)
for row in data['data']:
values = []
for col in row:
if isinstance(col, list) and len(col) > 1:
values.append(self._deserialize_ovsdb(col))
else:
values.append(col)
yield dict(zip(data['headings'], values))
def __iter__(self):
return self._find_tbl()
def clear(self, rec, col):
utils._run(self._tool, 'clear', self._table, rec, col)
def find(self, condition):
return self._find_tbl(condition=condition)
def remove(self, rec, col, value):
utils._run(self._tool, 'remove', self._table, rec, col, value)
def set(self, rec, col, value):
utils._run(self._tool, 'set', self._table, rec,
'{}={}'.format(col, value))