Skip to content

Instantly share code, notes, and snippets.

@victorock
Last active April 30, 2019 18:45
Show Gist options
  • Save victorock/2cb16e46068639e4eceb6578cb65a955 to your computer and use it in GitHub Desktop.
Save victorock/2cb16e46068639e4eceb6578cb65a955 to your computer and use it in GitHub Desktop.
#
# (c) 2019 Red Hat Inc.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
# (c) 2018, Victor da Costa <[email protected]>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import json
import uuid
from itertools import izip
from ansible.module_utils.network.common.network import NetworkModule
from ansible.module_utils.common.process import get_bin_path
from ansible.module_utils._text import to_bytes, to_text
ovs_argument_spec = {
'db': {
'required': False,
'default': 'unix:/var/run/openvswitch/db.sock',
'type': 'str',
'aliases': ['server']
},
'timeout': {
'required': False,
'default': 5,
'type': 'int'
},
'private_key': {
'required': False,
'default': None,
'type': 'str'
},
'certificate': {
'required': False,
'default': None,
'type': 'str'
},
'ca_cert': {
'required': False,
'default': None,
'type': 'str'
},
'bootstrap_ca_cert': {
'required': False,
'default': None,
'type': 'str'
},
'peer_ca_cert': {
'required': False,
'default': None,
'type': 'str'
}
}
# Annotation to sanitize inline arguments to ovs input
def no_ovs_commands(func):
def wrapper(*args, **kwargs):
forbidden = ['--']
for arg in args:
if isinstance(arg, basestring):
for f in forbidden:
if f in arg:
raise ValueError(
(
"Illegal arguments: {f}"
).format(
f=f
)
)
return func(*args , **kwargs)
return wrapper
def flatten(args):
if isinstance(args, (list, tuple, set)):
return sum(map(flatten, args), [])
else:
return [args]
def split_commas(data):
return list(data.split(","))
def to_kdv(d='', **kv):
'''
transform key/value dict to delimited list: [ {k}{d}{v}, ...]
'''
if not isinstance(kv, dict):
return None
dl = []
for k,v in kv.iteritems():
# check if dict is too deep
if not isinstance(v, str):
raise TypeError("expected str")
dl.append("{0}{1}{2}".format(k,d,v))
return list(dl)
def to_json(data):
"""
return: dict
"""
if isinstance(data, (str, buffer)):
return dict(json.loads(str(data), cls=json.JSONDecoder))
elif isinstance(data, dict):
return dict(data)
else:
raise TypeError("expected dict, str or buffer")
def to_type(*args):
"""
transform cell containing tuple ('type', 'value') to variable of `type` containing `value`
"""
_type = {
'integer': 'int',
'map': 'dict',
'string': 'str',
'set': 'set',
'str': 'str',
'dict': 'dict',
'uuid': 'uuid.UUID',
'decimal': 'floating',
'boolean': 'bool'
}
if isinstance(args, (list, tuple)) and len(args) == 2:
# if arg is complex, with tuples of tuples, expand recursively...
if isinstance(args[1], (list, tuple)):
return to_type(args[1])
return eval(_type[args[0]])(args[1])
return args
class OvsCommand(object):
def __init__(self, *args, **kwargs):
self.commands = kwargs.pop("commands", [])
self.format = kwargs.pop("format", "json")
self.data = kwargs.pop("data", "json")
self.server = kwargs.pop("server", "unix:/var/run/openvswitch/db.sock")
self.table = kwargs.pop("table", "Open_vSwitch")
self.timeout = kwargs.pop("timeout", 5)
self.private_key = kwargs.pop("private_key", False)
self.certificate = kwargs.pop("certificate", False)
self.ca_cert = kwargs.pop("ca_cert", False)
self.bootstrap_ca_cert = kwargs.pop("bootstrap_ca_cert", False)
self.peer_ca_cert = kwargs.pop("peer_ca_cert", False)
self.lines = []
def add_line(self, *args):
line = " ".join(flatten([args]))
self.lines.append(line)
return line
class OvsDbClient(OvsCommand):
def __init__(self, *args, **kwargs):
super(OvsDbClient, self).__init__(*args, **kwargs)
self.database = kwargs.pop("database", "Open_vSwitch")
@property
def command(self):
''' Build ovs-vsctl command
'''
# ovs-vsctl
cli = [
get_bin_path("ovsdb-client",True)
]
# --private-key={private_key}
if self.private_key:
cli.append(
"--private-key={0}".format(self.private_key)
)
# --certificate={certificate}
if self.certificate:
cli.append(
"--certificate={0}".format(self.certificate)
)
# --ca-cert={ca_cert}
if self.ca_cert:
cli.append(
"--ca-cert={0}".format(self.ca_cert)
)
# --bootstrap-ca-cert={bootstrap_ca_cert}
if self.bootstrap_ca_cert:
cli.append(
"--bootstrap-ca-cert={0}".format(self.bootstrap_ca_cert)
)
# --format={format}
if self.format:
cli.append(
"--format={0}".format(self.format)
)
# --data={data}
if self.data:
cli.append(
"--data={0}".format(self.data)
)
cli = ' '.join(flatten(cli))
args = ' --- '.join(flatten(self.lines))
return str(cli + args)
@no_ovs_commands
def schema(self):
return str(
self.add_line(
"get-schema",
to_text(self.server),
to_text(self.database)
)
)
@no_ovs_commands
def schema_version(self):
return str(
self.add_line(
"get-schema-version",
to_text(self.server),
to_text(self.database)
)
)
@no_ovs_commands
def transact(self, **transaction):
return str(
self.add_line(
"list-tables",
to_text(self.server),
to_json(transaction)
)
)
@no_ovs_commands
def monitor(self, table, *columns):
return str(
self.add_line(
"monitor",
to_text(self.server),
to_text(self.database),
to_text(table),
join_commas(*columns)
)
)
@no_ovs_commands
def dbs(self):
return str(
self.add_line(
"list-dbs",
to_text(self.server)
)
)
@no_ovs_commands
def columns(self, table):
return str(
self.add_line(
"list-columns",
to_text(self.server),
to_text(self.database),
to_text(table)
)
)
@no_ovs_commands
def tables(self):
return str(
self.add_line(
"list-tables",
to_text(self.server),
to_text(self.database)
)
)
@no_ovs_commands
def dump(self, database):
return str(
self.add_line(
"dump",
to_text(self.server),
to_text(self.database)
)
)
class OvsVsctl(OvsCommand):
def __init__(self, *args, **kwargs):
super(OvsVsctl, self).__init__(*args, **kwargs)
@property
def command(self):
''' Build ovs-vsctl command
'''
# ovs-vsctl --db="{server}" --timeout={timeout}"
cli = [
get_bin_path("ovs-vsctl",True),
"--db=\"{0}\"".format(self.server),
"--timeout=\"{0}\"".format(self.timeout)
]
# --private-key={private_key}
if self.private_key:
cli.append(
"--private-key={0}".format(self.private_key)
)
# --certificate={certificate}
if self.certificate:
cli.append(
"--certificate={0}".format(self.certificate)
)
# --ca-cert={ca_cert}
if self.ca_cert:
cli.append(
"--ca-cert={0}".format(self.ca_cert)
)
# --bootstrap-ca-cert={bootstrap_ca_cert}
if self.bootstrap_ca_cert:
cli.append(
"--bootstrap-ca-cert={0}".format(self.bootstrap_ca_cert)
)
# --per-ca-cert={peer_ca_cert}
if self.peer_ca_cert:
cli.append(
"--peer-ca-cert={0}".format(self.peer_ca_cert)
)
# --format={format}
if self.format:
cli.append(
"--format={0}".format(self.format)
)
# --data={data}
if self.data:
cli.append(
"--data={0}".format(self.data)
)
cli = ' '.join(flatten(cli))
args = ' --- '.join(flatten(self.lines))
return str(cli + args)
@no_ovs_commands
def list(self, record=str()):
'''
--if-exists list table [record]...
Lists the data in each specified record. If no records are
specified, lists all the records in table.
return: str
'''
return str(
self.add_line(
"--if-exists",
"list",
to_text(self.table),
to_text(record)
)
)
@no_ovs_commands
def find(self, **kwargs):
'''
find table [column[:key]=value]...
Lists the data in each record in table whose column equals value
or, if key is specified, whose column contains a key with the
specified value. The following operators may be used where = is
written in the syntax summary:
= != < > <= >=
Selects records in which column[:key] equals, does not
equal, is less than, is greater than, is less than or
equal to, or is greater than or equal to value, respec‐
tively.
Consider column[:key] and value as sets of elements.
Identical sets are considered equal. Otherwise, if the
sets have different numbers of elements, then the set
with more elements is considered to be larger. Other‐
wise, consider a element from each set pairwise, in in‐
creasing order within each set. The first pair that dif‐
fers determines the result. (For a column that contains
key-value pairs, first all the keys are compared, and
values are considered only if the two sets contain iden‐
tical keys.)
{=} {!=}
Test for set equality or inequality, respectively.
{<=} Selects records in which column[:key] is a subset of
value. For example, flood-vlans{<=}1,2 selects records
in which the flood-vlans column is the empty set or con‐
tains 1 or 2 or both.
{<} Selects records in which column[:key] is a proper subset
of value. For example, flood-vlans{<}1,2 selects records
in which the flood-vlans column is the empty set or con‐
tains 1 or 2 but not both.
{>=} {>}
Same as {<=} and {<}, respectively, except that the rela‐
tionship is reversed. For example, flood-vlans{>=}1,2
selects records in which the flood-vlans column contains
both 1 and 2.
For arithmetic operators (= != < > <= >=), when key is specified
but a particular record's column does not contain key, the
record is always omitted from the results. Thus, the condition
other-config:mtu!=1500 matches records that have a mtu key whose
value is not 1500, but not those that lack an mtu key.
For the set operators, when key is specified but a particular
record's column does not contain key, the comparison is done
against an empty set. Thus, the condition other-con‐
fig:mtu{!=}1500 matches records that have a mtu key whose value
is not 1500 and those that lack an mtu key.
Don't forget to escape < or > from interpretation by the shell.
return: str
'''
return str(
self.add_line(
"--if-exists",
"find",
to_text(self.table),
" ".join(to_kdv(d='=', **kwargs))
)
)
@no_ovs_commands
def get(self, id, record, columns):
'''
--id=@{id} get table record [column[:key]]
Prints the value of each specified column in the given record in
table. For map columns, a key may optionally be specified, in
which case the value associated with key in the column is
printed, instead of the entire map.
return: str
'''
return str(
self.add_line(
"--id=@{0}".format(id),
"get",
to_text(self.table),
to_text(record),
" ".join(flatten(columns))
)
)
@no_ovs_commands
def set(self, record, kwargs):
''' --if-exists set table record column[:key]=value...
Sets the value of each specified column in the given record in
table to value. For map columns, a key may optionally be speci‐
fied, in which case the value associated with key in that column
is changed (or added, if none exists), instead of the entire
map.
return: str
'''
return str(
self.add_line(
"--if-exists",
"set",
to_text(self.table),
to_text(record),
" ".join(to_kdv(d='=', kwargs))
)
)
@no_ovs_commands
def add(self, record, column, value):
'''
--if-exists add table record column [key=]value
Adds the specified value or key-value pair to column in record
in table. If column is a map, then key is required, otherwise
it is prohibited. If key already exists in a map column, then
the current value is not replaced (use the set command to re‐
place an existing value).
return: str
'''
return str(
self.add_line(
"--if-exists",
"add",
to_text(self.table),
to_text(record),
to_text(column),
to_text(value)
)
)
@no_ovs_commands
def remove(self, record, column, value):
'''
--if-exists remove table record column value...
--if-exists remove table record column key...
--if-exists remove table record column key=value...
Removes the specified values or key-value pairs from column in
record in table. The first form applies to columns that are not
maps: each specified value is removed from the column. The sec‐
ond and third forms apply to map columns: if only a key is spec‐
ified, then any key-value pair with the given key is removed,
regardless of its value; if a value is given then a pair is re‐
moved only if both key and value match.
return: str
'''
return str(
self.add_line(
"--if-exists",
"remove",
to_text(self.table),
to_text(record),
to_text(column),
to_text(value)
)
)
@no_ovs_commands
def clear(self, record, *args):
'''
--if-exists clear table record column
Sets each column in record in table to the empty set or empty
map, as appropriate. This command applies only to columns that
are allowed to be empty.
return: str
'''
return str(
self.add_line(
"--if-exists",
"clear",
to_text(self.table),
to_text(record),
" ".join(list(args))
)
)
@no_ovs_commands
def create(self, id, *args):
'''
--id=@{id} create table column[:key]=value...
Creates a new record in table and sets the initial values of
each column. Columns not explicitly set will receive their de‐
fault values. Outputs the UUID of the new row.
The UUID for the new row is stored in @create and may be re‐
ferred to by that name elsewhere in the same ovs-vsctl invoca‐
tion in contexts where a UUID is expected. Such references may
precede or follow the create command.
return: str
'''
return str(
self.add_line(
"--id=@{0}".format(str(id)),
"create",
to_text(self.table),
to_text(" ".join(list(args)))
)
)
@no_ovs_commands
def wait_until(self, record, **kwargs):
'''
wait-until table record [column[:key]=value]...
Waits until table contains a record named record whose column
equals value or, if key is specified, whose column contains a
key with the specified value. Any of the operators !=, <, >,
<=, or >= may be substituted for = to test for inequality, less
than, greater than, less than or equal to, or greater than or
equal to, respectively. (Don't forget to escape < or > from in‐
terpretation by the shell.)
If no column[:key]=value arguments are given, this command waits
only until record exists. If more than one such argument is
given, the command waits until all of them are satisfied.
Caution (ovs-vsctl as example)
Usually wait-until should be placed at the beginning of a
set of ovs-vsctl commands. For example, wait-until
bridge br0 -- get bridge br0 datapath_id waits until a
bridge named br0 is created, then prints its datapath_id
column, whereas get bridge br0 datapath_id -- wait-until
bridge br0 will abort if no bridge named br0 exists when
ovs-vsctl initially connects to the database.
Consider specifying --timeout=0 along with --wait-until, to pre‐
vent ovs-vsctl from terminating after waiting only at most 5
seconds.
return: str
'''
return str(
self.add_line(
"wait_until",
to_text(self.table),
to_text(record),
" ".join(to_kdv(d='=',**kwargs))
)
)
class OvsRow(object):
def __init__(self, columns, row):
for column, cell in izip(list(columns), list(row)):
setattr(self, column, to_type(cell))
def __getitem__(self, column):
return getattr(self, column)
class OvsTable(object):
def __init__(self, data):
'''
data = {
'headings': [ 'a', 'b', 'c', ...],
'data': [
[(type, data), (type, data), (type, data), ...],
[(type, data), (type, data), (type, data), ...],
....
]
}
'''
self.__json = to_json(data)
self.__columns = list(self.__json.pop("headings"))
self.__rows = [ OvsRow(self.__columns, row) for row in list(self.__json.pop("data")) ]
def __getitem__(self, name):
return getattr(self, name)
def __getattr__(self, column):
return [str(getattr(rows, column)) for rows in self.rows]
@property
def columns(self):
return self.__columns
@property
def rows(self):
return self.__rows
class OvsCli(NetworkModule):
def __init__(self, *args, **kwargs):
super(OvsCli, self).__init__(*args, **kwargs)
def get_device_info(self):
return self.connection.get_device_info()
def get_config(self):
return self.connection.get_config()
def get_capabilities(self):
return self.connection.get_capabilities()
def get_diff(self, candidate=None, running=None, diff_match='line', diff_ignore_lines=None, path=None, diff_replace='line'):
return self.connection.get_diff(candidate=candidate, running=running, diff_match=diff_match, diff_ignore_lines=diff_ignore_lines, path=path, diff_replace=diff_replace)
def edit_config(self, candidate=None, commit=True, replace=None, diff=False, comment=None, cli=None):
return self.connection.edit_config(candidate=candidate, commit=commit, replace=replace, diff=diff, comment=comment, cli=cli)
def get(self, )
def load_config(self, commands, return_error=False, opts=None, replace=None):
return self.connection.load_config(commands=commands, return_error=return_error, opts=opts, replace=replace)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment