Categories
OVS/OVN

ovsdbapp, your library for Open vSwitch and OVN

The first thing you will notice reviewing the code of this library is a common pattern: Terry Wilson, one of the fathers of the creature. It has many other contributors, but he is the main maintainer. Thanks!

This library provides a Python native implementation to access to the Open vSwitch database, using the OVSDB management protocol. With just one single function call you can create a bridge, delete a port or list all QoS rules. If you are a developer, you should take a look at the code to see how things are made. You will understand a bit better how this library connects to the database, receives the updates and stores locally a copy of the database.

Let’s take a quick look.

This library has two main parts. The first one is the backend. The backend implements a class called Connection, that drives the connection to an OVSDB server using an OVS IDL instance. This class will start or stop the connectivity and will process any command transmited or received from the OVSDB server. Now we reach the core of all this process: the IDL class. Although this is not implemented in this library but in ovs, we need to know how it works because this is the rosetta stone. The IDL instance (Interface Definition Language) mantains a in-memory replica of the database. Here is where we go when we execute a database read command, instead of creating a transaction, building a query and executing this query on the OVSDB server.

The IDL instance, when the connection is established, registers in the server the interest in some columns or tables of the database. Since this moment, the IDL instace will wait for the server updates everytime a change in the database happens, keeping the in-memory replica always updated. This mechanism frees the client from implementing some kind of polling method to update the replica, that could saturate both the client and the server.

This session implements to methods: active, using TCP, and passive, using PTCP. This session creates two streams to send and receive the information to and from the server.

Once the IDL object is instantiated, is given to a Connection class as a constructor parameter. This new Connection object is in charge of, using the IDL object, keeping an active connection to the Open vSwitch database. When the start method is called, a new thread is created. This thread runs an easy loop waiting for new messages from the dabatase server. When a message arrives, is stored in a queue.

Database schemas and commands.

The second part of this library are the database schemas and their commands. But wait, did you say database schemas? Are there more than one? Yes because of OVN. Open vSwitch has one single database, defined in this schema. But OVN has two of them, the northbound and the southbound. The wonders and fantasies of OVN cannot be described in this humble post; that’s why I recommend you, avid reader, to discover them, properly dressed and a full canteen.

For each database, a set of commands are implemented. For example, if in OVS we want to add a new bridge, the AddBridgeCommand has the logic to first insert a new bridge in the table “Bridge”, then create a port with the same name and finally, for the interface associated to this port, set the type to “internal”. And all this complex command can be called from a OvsdbIdl instance just passing the bridge name.

Show me the database!

In memory, divided by tables, stored in a dictionary. Is that easy:

ovs.tables = {
    'Controller': <TableSchema>,
    'Brigde': <TableSchema>,
    'Queue': <TableSchema>,
    'IPFIX': <TableSchema>, ...}

The OVS database schema depends on the version installed and it’s defined here. When consulting the OVS database schema reference manual, be sure that refers to the version you are using.

And now, one step forward: I want to play with it.

Here is where ovsdbapp OvsdbIdl class comes into play. This class exposes a rich API and a set of implemented commands. You can add a bridge, a port or list the interfaces of a bridge with just one single command call. Of course you can freely create your own transaction and retrieve or modify the information from the OVS database. Actually, the read commands can be executed without any transaction, just accessing to this ovs.tables big dictionary.

But hold your horses, fearless stranger. This is a transactional database that “offers atomic, consistent, isolated, durable transactions”. If you want to modify the database, this should be done within a transaction. Inside the transaction context, each database interaction is stored and when the transaction context finishes, those stored commands are sent using the TransactionQueue of the Connection object. 

The Example (well just an example with delusions of grandeur)

Let’s add a new bridge, “fistro”. This is the code I used:

#!/usr/bin/env python

from ovs.db import idl

from ovsdbapp.backend.ovs_idl import idlutils
from ovsdbapp.backend.ovs_idl import connection
from ovsdbapp.schema.open_vswitch import impl_idl as ovs_impl_idl


OVS_CONNECTION = 'tcp:127.0.0.1:6640'
SCHEMA_NAME = 'Open_vSwitch'
OVSDB_CONNECTION_TIMEOUT = 30


class NotificationBackend(object):

    @staticmethod
    def notify_event(event, table):
        print('New event "%s", table "%s"' % (event, table))


class OvsIdl(idl.Idl):

    def __init__(self, connection_string, schema_name, notification_backend):
        helper = idlutils.get_schema_helper(connection_string, schema_name)
        helper.register_all()
        self._notification_backend = notification_backend
        super(OvsIdl, self).__init__(connection_string, helper)

    def notify(self, event, row, updates=None):
        if row._table.name == 'Bridge':
            self._notification_backend.notify_event(event, row._table.name)


def get_ovs(viewer):
    _idl = OvsIdl(OVS_CONNECTION, SCHEMA_NAME, viewer)
    _conn = connection.Connection(_idl, timeout=OVSDB_CONNECTION_TIMEOUT)
    return ovs_impl_idl.OvsdbIdl(_conn, start=True)


ovs = get_ovs(NotificationBackend())
ovs.add_br('fistro_bridge')
ovs.add_port('fistro_bridge', 'fistro_port')

When the bridge is added (the transaction finishes), the OVS database will send a RPC message that will be “captured” by the JSONRPC session created by Connection using the IDL object. This is the JSON message received:

msg = notification, method="update2", params=["a4bb6b06-bdde-11ea-a487-525400d09343",
{"Bridge":{"5b13d639-d5f9-4151-8c13-26663742deca":{"insert":{"name":"fistro","ports":["uuid","61b9502e-6c2c-47e5-82ea-91604db3b2c1"]}}},
"Interface":{"9d8b0cff-b476-42f1-8926-ba147aaba587":{"insert":{"name":"fistro","type":"internal"}}},
"Open_vSwitch":{"868ac88e-983d-4c8b-8b43-f9685e77c460":{"modify":{"bridges":["uuid","5b13d639-d5f9-4151-8c13-26663742deca"],"next_cfg":2512}}},
"Port":{"61b9502e-6c2c-47e5-82ea-91604db3b2c1":{"insert":{"interfaces":["uuid","9d8b0cff-b476-42f1-8926-ba147aaba587"],"name":"fistro"}}}}]

This message is received inside the IDL waiting loop (called from the Connection waiting loop thread) and parsed some lines later. Each message received can contain several events. This one, for example, has four: an insertion in “Bridge”, “Interface” and “Port” and a modification in “Open_vSwitch” tables.

For example, the first one, the insertion in “Bridge” table. The first parameter, 5b13d639-d5f9-4151-8c13-26663742deca, is the UUID assigned to this new table register. Because the bridge creation transaction implies the creation of an internal port representing the bridge, the port UUID created in also included in the field “ports” if the “Bridge” register.

This is what we see when we list this new Bridge:

root@dev18:~$ ovs-vsctl list Bridge fistro
_uuid               : 5b13d639-d5f9-4151-8c13-26663742deca
auto_attach         : []
controller          : []
datapath_id         : "00003ad6135b5141"
datapath_type       : ""
datapath_version    : "<unknown>"
external_ids        : {}
fail_mode           : []
flood_vlans         : []
flow_tables         : {}
ipfix               : []
mcast_snooping_enable: false
mirrors             : []
name                : "fistro"
netflow             : []
other_config        : {}
ports               : [61b9502e-6c2c-47e5-82ea-91604db3b2c1]
protocols           : []
rstp_enable         : false
rstp_status         : {}
sflow               : []
status              : {}
stp_enable          : false

Once those events are parsed, will be stored in the in-memory database (Python dictionary) and will generate an event the could be captured and processed (see function “notify”).

Leave a Reply

Your email address will not be published. Required fields are marked *