Hi Numan,

The first 11 patches in this series fail CI because of failures to `make rpm-fedora`. I think some of the specfile changes from patch 12 should be added to earlier patches in order to allow CI to succeed.

I have more below.

On 8/11/25 6:09 AM, [email protected] wrote:
From: Numan Siddique <[email protected]>

This commit is the first of the series to add a new service
to OVN called ovn-br-controller (OVN bridge controller).
This commit adds
  - a schema file
  - a basic skeleton of ovn-br-controller service (which does nothing)
  - ovn-brctl utility similar to ovn-nbctl/ovn-sbctl.

Rational for adding this service:
There's a need to configure the provider bridges with specific OpenFlow
rules after packets leave the OVN pipeline and enters these provider
bridges  via the patch ports.  Since OVN has a very good abstraction
of the OpenFlow rules using OVN logical flows, we can leverage the same
here so that CMS doesn't have to deal with the specifics of OpenFlow
protocol.  Also if the CMS is written in golang or other languages,
CMS has to mostly rely on ovs-vsctl/ovs-ofctl to program the flows.
Adding this support in OVN avoids the CMS to exec these utils to
add/delete and dump the existing OpenFlow rules.

Any user can also use this new service to program and manage any OVS
bridge without using OVN and hence this service is named as
"ovn-br-controller."

Signed-off-by: Numan Siddique <[email protected]>
---
  Makefile.am                           |   5 +-
  automake.mk                           |  36 ++
  br-controller/automake.mk             |   8 +
  br-controller/ovn-br-controller.8.xml |   5 +
  br-controller/ovn-br-controller.c     | 175 ++++++++
  lib/automake.mk                       |  17 +-
  lib/ovn-br-idl.ann                    |   9 +
  lib/ovn-util.c                        |  13 +
  lib/ovn-util.h                        |   1 +
  ovn-br.ovsschema                      |  94 +++++
  ovn-br.xml                            | 452 ++++++++++++++++++++
  tutorial/ovn-sandbox                  |  24 +-
  utilities/automake.mk                 |   8 +
  utilities/ovn-brctl.c                 | 585 ++++++++++++++++++++++++++
  14 files changed, 1429 insertions(+), 3 deletions(-)
  create mode 100644 br-controller/automake.mk
  create mode 100644 br-controller/ovn-br-controller.8.xml
  create mode 100644 br-controller/ovn-br-controller.c
  create mode 100644 lib/ovn-br-idl.ann
  create mode 100644 ovn-br.ovsschema
  create mode 100644 ovn-br.xml
  create mode 100644 utilities/ovn-brctl.c

diff --git a/Makefile.am b/Makefile.am
index ea98fb5fb5..1936e9697a 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -115,7 +115,9 @@ EXTRA_DIST = \
        ovn-ic-nb.ovsschema \
        ovn-ic-nb.xml \
        ovn-ic-sb.ovsschema \
-       ovn-ic-sb.xml
+       ovn-ic-sb.xml \
+       ovn-br.ovsschema \
+       ovn-br.xml
  bin_PROGRAMS =
  sbin_PROGRAMS =
  bin_SCRIPTS =
@@ -507,4 +509,5 @@ include controller/automake.mk
  include controller-vtep/automake.mk
  include northd/automake.mk
  include ic/automake.mk
+include br-controller/automake.mk
  include build-aux/automake.mk
diff --git a/automake.mk b/automake.mk
index a7947a3f54..53860ad89b 100644
--- a/automake.mk
+++ b/automake.mk
@@ -120,6 +120,35 @@ ovn-ic-sb.5: \
                $(srcdir)/ovn-ic-sb.xml > [email protected] && \
        mv [email protected] $@
+# OVN bridge controller E-R diagram
+#
+# If "python" or "dot" is not available, then we do not add graphical diagram
+# to the documentation.
+if HAVE_DOT
+ovn-br.gv: ${OVSDIR}/ovsdb/ovsdb-dot.in $(srcdir)/ovn-br.ovsschema
+       $(AM_V_GEN)$(OVSDB_DOT) --no-arrows $(srcdir)/ovn-br.ovsschema > $@
+ovn-br.pic: ovn-br.gv ${OVSDIR}/ovsdb/dot2pic
+       $(AM_V_GEN)(dot -T plain < ovn-br.gv | $(PYTHON3) ${OVSDIR}/ovsdb/dot2pic -f 3) 
> [email protected] && \
+       mv [email protected] $@
+OVN_BR_PIC = ovn-br.pic
+OVN_BR_DOT_DIAGRAM_ARG = --er-diagram=$(OVN_BR_PIC)
+CLEANFILES += ovn-br.gv ovn-br.pic
+endif
+
+# OVN bridge controller schema documentation
+EXTRA_DIST += ovn-br.xml
+CLEANFILES += ovn-br.5
+man_MANS += ovn-br.5
+
+ovn-br.5: \
+       ${OVSDIR}/ovsdb/ovsdb-doc $(srcdir)/ovn-br.xml 
$(srcdir)/ovn-br.ovsschema $(OVN_BR_PIC)
+       $(AM_V_GEN)$(OVSDB_DOC) \
+               $(OVN_BR_DOT_DIAGRAM_ARG) \
+               --version=$(VERSION) \
+               $(srcdir)/ovn-br.ovsschema \
+               $(srcdir)/ovn-br.xml > [email protected] && \
+       mv [email protected] $@
+
  # Version checking for ovn-nb.ovsschema.
  ALL_LOCAL += ovn-nb.ovsschema.stamp
  ovn-nb.ovsschema.stamp: ovn-nb.ovsschema
@@ -143,9 +172,16 @@ ovn-ic-sb.ovsschema.stamp: ovn-ic-sb.ovsschema
        $(srcdir)/build-aux/cksum-schema-check $? $@
  CLEANFILES += ovn-ic-sb.ovsschema.stamp
+# Version checking for ovn-br.ovsschema.
+ALL_LOCAL += ovn-br.ovsschema.stamp
+ovn-br.ovsschema.stamp: ovn-br.ovsschema
+       $(srcdir)/build-aux/cksum-schema-check $? $@
+CLEANFILES += ovn-br.ovsschema.stamp
+
  pkgdata_DATA += ovn-nb.ovsschema
  pkgdata_DATA += ovn-sb.ovsschema
  pkgdata_DATA += ovn-ic-nb.ovsschema
  pkgdata_DATA += ovn-ic-sb.ovsschema
+pkgdata_DATA += ovn-br.ovsschema
CLEANFILES += ovn-sb.ovsschema.stamp
diff --git a/br-controller/automake.mk b/br-controller/automake.mk
new file mode 100644
index 0000000000..012e9a5ed8
--- /dev/null
+++ b/br-controller/automake.mk
@@ -0,0 +1,8 @@
+bin_PROGRAMS += br-controller/ovn-br-controller
+br_controller_ovn_br_controller_SOURCES = \
+       br-controller/ovn-br-controller.c
+
+br_controller_ovn_br_controller_LDADD = lib/libovn.la 
$(OVS_LIBDIR)/libopenvswitch.la
+man_MANS += br-controller/ovn-br-controller.8
+EXTRA_DIST += br-controller/ovn-br-controller.8.xml
+CLEANFILES += br-controller/ovn-br-controller.8
diff --git a/br-controller/ovn-br-controller.8.xml 
b/br-controller/ovn-br-controller.8.xml
new file mode 100644
index 0000000000..fa6eb2e2e2
--- /dev/null
+++ b/br-controller/ovn-br-controller.8.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manpage program="ovn-br-controller" section="8" title="ovn-br-controller">
+    <h1>Name</h1>
+    <p>ovn-br-controller -- Open Virtual Network local OVS bridge 
controller</p>
+</manpage>
diff --git a/br-controller/ovn-br-controller.c 
b/br-controller/ovn-br-controller.c
new file mode 100644
index 0000000000..0fef0e5fee
--- /dev/null
+++ b/br-controller/ovn-br-controller.c
@@ -0,0 +1,175 @@
+/* Copyright (c) 2025 Crusoe Energy Systems LLC
+ *
+ * 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.
+ */
+
+#include <config.h>
+
+#include <errno.h>
+#include <getopt.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* OVS includes. */
+#include "openvswitch/vlog.h"
+#include "lib/command-line.h"
+#include "lib/daemon.h"
+#include "lib/dirs.h"
+#include "lib/fatal-signal.h"
+#include "lib/stream.h"
+#include "lib/stream-ssl.h"
+#include "lib/unixctl.h"
+
+/* OVN includes. */
+#include "lib/ovn-br-idl.h"
+#include "lib/ovn-util.h"
+
+VLOG_DEFINE_THIS_MODULE(main);
+
+static void parse_options(int argc, char *argv[]);
+OVS_NO_RETURN static void usage(void);
+
+
+/* SSL/TLS options. */
+static const char *ssl_private_key_file;
+static const char *ssl_certificate_file;
+static const char *ssl_ca_cert_file;
+
+/* --unixctl-path: Path to use for unixctl server socket. */
+static char *unixctl_path;
+
+int
+main(int argc OVS_UNUSED, char *argv[] OVS_UNUSED)
+{
+    ovs_cmdl_proctitle_init(argc, argv);
+    ovn_set_program_name(argv[0]);
+    service_start(&argc, &argv);
+    parse_options(argc, argv);
+    fatal_ignore_sigpipe();
+
+    return 0;
+}
+
+/* static functions. */
+static void
+parse_options(int argc, char *argv[])
+{
+    enum {
+        OPT_PEER_CA_CERT = UCHAR_MAX + 1,
+        OPT_BOOTSTRAP_CA_CERT,
+        VLOG_OPTION_ENUMS,
+        OVN_DAEMON_OPTION_ENUMS,
+        SSL_OPTION_ENUMS,
+    };
+
+    static struct option long_options[] = {
+        {"help", no_argument, NULL, 'h'},
+        {"version", no_argument, NULL, 'V'},
+        {"unixctl", required_argument, NULL, 'u'},
+        VLOG_LONG_OPTIONS,
+        OVN_DAEMON_LONG_OPTIONS,
+        STREAM_SSL_LONG_OPTIONS,
+        {"peer-ca-cert", required_argument, NULL, OPT_PEER_CA_CERT},
+        {"bootstrap-ca-cert", required_argument, NULL, OPT_BOOTSTRAP_CA_CERT},
+        {NULL, 0, NULL, 0}
+    };
+    char *short_options = ovs_cmdl_long_options_to_short_options(long_options);
+
+    for (;;) {
+        int c;
+
+        c = getopt_long(argc, argv, short_options, long_options, NULL);
+        if (c == -1) {
+            break;
+        }
+
+        switch (c) {
+        case 'h':
+            usage();
+
+        case 'V':
+            ovs_print_version(OFP15_VERSION, OFP15_VERSION);
+            printf("OVN BR DB Schema %s\n", ovnbrrec_get_db_version());
+            exit(EXIT_SUCCESS);
+
+        case 'u':
+            unixctl_path = optarg;
+            break;
+
+        VLOG_OPTION_HANDLERS
+        OVN_DAEMON_OPTION_HANDLERS
+
+        case 'p':
+            ssl_private_key_file = optarg;
+            break;
+
+        case 'c':
+            ssl_certificate_file = optarg;
+            break;
+
+        case 'C':
+            ssl_ca_cert_file = optarg;
+            break;
+
+        case OPT_SSL_PROTOCOLS:
+            stream_ssl_set_protocols(optarg);
+            break;
+
+        case OPT_SSL_CIPHERS:
+            stream_ssl_set_ciphers(optarg);
+            break;
+
+        case OPT_SSL_CIPHERSUITES:
+            stream_ssl_set_ciphersuites(optarg);
+            break;
+
+        case OPT_PEER_CA_CERT:
+            stream_ssl_set_peer_ca_cert_file(optarg);
+            break;
+
+        case OPT_BOOTSTRAP_CA_CERT:
+            stream_ssl_set_ca_cert_file(optarg, true);
+            break;
+
+        case '?':
+            exit(EXIT_FAILURE);
+
+        default:
+            ovs_abort(0, "Invalid option.");
+        }
+    }
+    free(short_options);
+
+    argc -= optind;
+    argv += optind;
+}
+
+static void
+usage(void)
+{
+    printf("%s: OVN bridge controller\n"
+           "usage %s [OPTIONS] [OVS-DATABASE]\n"
+           "where OVS-DATABASE is a socket on which the OVS OVSDB server "
+           "is listening.\n",
+           program_name, program_name);
+    stream_usage("OVS-DATABASE", true, false, true);
+    daemon_usage();
+    vlog_usage();
+    printf("\nOther options:\n"
+           "  -u, --unixctl=SOCKET    set control socket name\n"
+           "  -n                      custom chassis name\n"
+           "  -h, --help              display this help message\n"
+           "  -V, --version           display version information\n");
+    exit(EXIT_SUCCESS);
+}
diff --git a/lib/automake.mk b/lib/automake.mk
index a59c722d62..46c24e99d1 100644
--- a/lib/automake.mk
+++ b/lib/automake.mk
@@ -59,7 +59,9 @@ nodist_lib_libovn_la_SOURCES = \
        lib/ovn-ic-nb-idl.c \
        lib/ovn-ic-nb-idl.h \
        lib/ovn-ic-sb-idl.c \
-       lib/ovn-ic-sb-idl.h
+       lib/ovn-ic-sb-idl.h \
+       lib/ovn-br-idl.c \
+       lib/ovn-br-idl.h
CLEANFILES += $(nodist_lib_libovn_la_SOURCES) @@ -129,3 +131,16 @@ OVN_IC_SB_IDL_FILES = \
  lib/ovn-ic-sb-idl.ovsidl: $(OVN_IC_SB_IDL_FILES)
        $(AM_V_GEN)$(OVSDB_IDLC) annotate $(OVN_IC_SB_IDL_FILES) > [email protected] && \
        mv [email protected] $@
+
+# ovn-br IDL
+OVSIDL_BUILT += \
+       lib/ovn-br-idl.c \
+       lib/ovn-br-idl.h \
+       lib/ovn-br-idl.ovsidl
+EXTRA_DIST += lib/ovn-br-idl.ann
+OVN_PR_IDL_FILES = \
+       $(srcdir)/ovn-br.ovsschema \
+       $(srcdir)/lib/ovn-br-idl.ann
+lib/ovn-br-idl.ovsidl: $(OVN_PR_IDL_FILES)
+       $(AM_V_GEN)$(OVSDB_IDLC) annotate $(OVN_PR_IDL_FILES) > [email protected] && \
+       mv [email protected] $@
diff --git a/lib/ovn-br-idl.ann b/lib/ovn-br-idl.ann
new file mode 100644
index 0000000000..80993338a6
--- /dev/null
+++ b/lib/ovn-br-idl.ann
@@ -0,0 +1,9 @@
+# -*- python -*-
+
+# This code, when invoked by "ovsdb-idlc annotate" (by the build
+# process), annotates ovn-br.ovsschema with additional data that give
+# the ovsdb-idl engine information about the types involved, so that
+# it can generate more programmer-friendly data structures.
+
+s["idlPrefix"] = "ovnbrrec_"
+s["idlHeader"] = "\"lib/ovn-br-idl.h\""
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index ab5c4fc477..1cfeed6044 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -560,6 +560,19 @@ default_ic_sb_db(void)
      return def;
  }
+const char *
+default_br_db(void)
+{
+    static char *def;
+    if (!def) {
+        def = getenv("OVN_BR_DB");
+        if (!def) {
+            def = xasprintf("unix:%s/ovnbr_db.sock", ovn_rundir());
+        }
+    }
+    return def;
+}
+
  char *
  get_abs_unix_ctl_path(const char *path)
  {
diff --git a/lib/ovn-util.h b/lib/ovn-util.h
index bde40666a1..c66d7c31ff 100644
--- a/lib/ovn-util.h
+++ b/lib/ovn-util.h
@@ -131,6 +131,7 @@ const char *default_nb_db(void);
  const char *default_sb_db(void);
  const char *default_ic_nb_db(void);
  const char *default_ic_sb_db(void);
+const char *default_br_db(void);
  char *get_abs_unix_ctl_path(const char *path);
struct ovsdb_idl_table_class;
diff --git a/ovn-br.ovsschema b/ovn-br.ovsschema
new file mode 100644
index 0000000000..5635ee5bbd
--- /dev/null
+++ b/ovn-br.ovsschema
@@ -0,0 +1,94 @@
+{
+    "name": "OVN_Bridge_Controller",
+    "version": "0.0.1",
+    "cksum": "124113656 4389",
+    "tables": {
+        "BR_Global": {
+            "columns": {
+                "br_cfg": {"type": {"key": "integer"}},
+                "external_ids": {
+                    "type": {"key": "string", "value": "string",
+                             "min": 0, "max": "unlimited"}},
+                "connections": {
+                        "type": {"key": {"type": "uuid",
+                                        "refTable": "Connection"},
+                                        "min": 0,
+                                        "max": "unlimited"}},
+                "ssl": {
+                    "type": {"key": {"type": "uuid",
+                                    "refTable": "SSL"},
+                                    "min": 0, "max": 1}},
+                "options": {
+                    "type": {"key": "string", "value": "string",
+                            "min": 0, "max": "unlimited"}}},
+            "maxRows": 1,
+            "isRoot": true},
+        "Connection": {
+            "columns": {
+                "target": {"type": "string"},
+                "max_backoff": {"type": {"key": {"type": "integer",
+                                         "minInteger": 1000},
+                                         "min": 0,
+                                         "max": 1}},
+                "inactivity_probe": {"type": {"key": "integer",
+                                              "min": 0,
+                                              "max": 1}},
+                "other_config": {"type": {"key": "string",
+                                          "value": "string",
+                                          "min": 0,
+                                          "max": "unlimited"}},
+                "external_ids": {"type": {"key": "string",
+                                 "value": "string",
+                                 "min": 0,
+                                 "max": "unlimited"}},
+                "is_connected": {"type": "boolean", "ephemeral": true},
+                "status": {"type": {"key": "string",
+                                    "value": "string",
+                                    "min": 0,
+                                    "max": "unlimited"},
+                                    "ephemeral": true}},
+            "indexes": [["target"]]},
+        "SSL": {
+            "columns": {
+                "private_key": {"type": "string"},
+                "certificate": {"type": "string"},
+                "ca_cert": {"type": "string"},
+                "bootstrap_ca_cert": {"type": "boolean"},
+                "ssl_protocols": {"type": "string"},
+                "ssl_ciphers": {"type": "string"},
+                "ssl_ciphersuites": {"type": "string"},
+                "external_ids": {"type": {"key": "string",
+                                          "value": "string",
+                                          "min": 0,
+                                          "max": "unlimited"}}},
+            "maxRows": 1},
+        "Bridge": {
+            "columns": {
+                "name": {"type": "string"},
+                "options": {
+                    "type": {"key": "string", "value": "string",
+                             "min": 0, "max": "unlimited"}},
+                "external_ids": {
+                    "type": {"key": "string", "value": "string",
+                             "min": 0, "max": "unlimited"}}},
+            "indexes": [["name"]],
+            "isRoot": true},
+        "Logical_Flow": {
+            "columns": {
+                "bridge": {"type": {"key": {"type": "uuid",
+                                      "refTable": "Bridge"},
+                           "min": 0, "max": 1}},

Out of curiosity, what happens if you run ovn-brcontroller on the same hypervisor where ovn-controller is running, then you create a Bridge in the BR DB for "br-int" and you add logical flows to the BR DB for "br-int"?

Should there be safeguards in the code to prevent this?

+                "table_id": {"type": {"key": {"type": "integer",
+                                              "minInteger": 0,
+                                              "maxInteger": 32}}},

Why is the table_id limited to 32? We don't have ingress and egress pipelines, so the table_id should have a larger range for these logical flows.

+                "priority": {"type": {"key": {"type": "integer",
+                                              "minInteger": 0,
+                                              "maxInteger": 65535}}},
+                "match": {"type": "string"},
+                "actions": {"type": "string"},
+                "external_ids": {
+                    "type": {"key": "string", "value": "string",
+                             "min": 0, "max": "unlimited"}}},
+            "isRoot": true}
+    }
+}
diff --git a/ovn-br.xml b/ovn-br.xml
new file mode 100644
index 0000000000..be6232094b
--- /dev/null
+++ b/ovn-br.xml
@@ -0,0 +1,452 @@
+<?xml version="1.0" encoding="utf-8"?>
+<database name="ovn-bridge-controller" title="OVN Bridge Controller Database">
+  <p>
+    This database is the interface between OVN Bridge Controller and the
+    cloud management system (CMS) to program and control the OVS bridges
+    using OVN logical flows. The CMS produces almost all of
+    the contents of the database.  The <code>ovn-bridge-controller</code> 
program
+    monitors the database contents and programs the OVS bridges with the
+    OpenFlow rules.
+  </p>
+
+  <h2>External IDs</h2>
+
+  <p>
+    Each of the tables in this database contains a special column, named
+    <code>external_ids</code>.  This column has the same form and purpose each
+    place it appears.
+  </p>
+
+  <dl>
+    <dt><code>external_ids</code>: map of string-string pairs</dt>
+    <dd>
+      Key-value pairs for use by the CMS.  The CMS might use certain pairs, for
+      example, to identify entities in its own configuration that correspond to
+      those in this database.
+    </dd>
+  </dl>
+
+  <table name="BR_Global" title="Bridge Controller configuration">
+    <column name="br_cfg">
+      Sequence number for client to increment.  When a client modifies any
+      part of the bridge Controller database configuration and wishes to wait
+      for <code>ovn-br-controller</code> to finish applying the changes, it may
+      increment this sequence number.
+    </column>
+
+    <group title="Common Columns">
+        <column name="external_ids">
+        See <em>External IDs</em> at the beginning of this document.
+      </column>
+    </group>
+
+    <group title="Common options">
+      <column name="options">
+        This column provides general key/value settings. The supported
+        options are described individually below.
+      </column>
+    </group>
+
+    <group title="Connection Options">
+      <column name="connections">
+        Database clients to which the Open vSwitch database server should
+        connect or on which it should listen, along with options for how these
+        connections should be configured.  See the <ref table="Connection"/>
+        table for more information.
+      </column>
+      <column name="ssl">
+        Global SSL/TLS configuration.
+      </column>
+    </group>
+  </table>
+
+  <table name="Connection" title="OVSDB client connections.">
+    <p>
+      Configuration for a database connection to an Open vSwitch database
+      (OVSDB) client.
+    </p>
+
+    <p>
+      This table primarily configures the Open vSwitch database server
+      (<code>ovsdb-server</code>).
+    </p>
+
+    <p>
+      The Open vSwitch database server can initiate and maintain active
+      connections to remote clients.  It can also listen for database
+      connections.
+    </p>
+
+    <group title="Core Features">
+      <column name="target">
+        <p>Connection methods for clients.</p>
+        <p>
+          The following connection methods are currently supported:
+        </p>
+        <dl>
+          
<dt><code>ssl:<var>host</var></code>[<code>:<var>port</var></code>]</dt>
+          <dd>
+            <p>
+              The specified SSL/TLS <var>port</var> on the host at the given
+              <var>host</var>, which can either be a DNS name (if built with
+              unbound library) or an IP address. A valid SSL/TLS configuration
+              must be provided when this form is used, this configuration can
+              be specified via command-line options or the <ref table="SSL"/>
+              table.
+            </p>
+            <p>
+              If <var>port</var> is not specified, it defaults to 6640.
+            </p>
+            <p>
+              SSL/TLS support is an optional feature that is not always
+              built as part of OVN or Open vSwitch.
+            </p>
+          </dd>
+
+          
<dt><code>tcp:<var>host</var></code>[<code>:<var>port</var></code>]</dt>
+          <dd>
+            <p>
+              The specified TCP <var>port</var> on the host at the given
+              <var>host</var>, which can either be a DNS name (if built with
+              unbound library) or an IP address.  If <var>host</var> is an IPv6
+              address, wrap it in square brackets, e.g. 
<code>tcp:[::1]:6640</code>.
+            </p>
+            <p>
+              If <var>port</var> is not specified, it defaults to 6640.
+            </p>
+          </dd>
+          
<dt><code>pssl:</code>[<var>port</var>][<code>:<var>host</var></code>]</dt>
+          <dd>
+            <p>
+              Listens for SSL/TLS connections on the specified TCP
+              <var>port</var>.
+              Specify 0 for <var>port</var> to have the kernel automatically
+              choose an available port.  If <var>host</var>, which can either
+              be a DNS name (if built with unbound library) or an IP address,
+              is specified, then connections are restricted to the resolved or
+              specified local IPaddress (either IPv4 or IPv6 address).  If
+              <var>host</var> is an IPv6 address, wrap in square brackets,
+              e.g. <code>pssl:6640:[::1]</code>.  If <var>host</var> is not
+              specified then it listens only on IPv4 (but not IPv6) addresses.
+              A valid SSL/TLS configuration must be provided when this form is
+              used, this can be specified either via command-line options or
+              the <ref table="SSL"/> table.
+            </p>
+            <p>
+              If <var>port</var> is not specified, it defaults to 6640.
+            </p>
+            <p>
+              SSL/TLS support is an optional feature that is not always built
+              as part of OVN or Open vSwitch.
+            </p>
+          </dd>
+          
<dt><code>ptcp:</code>[<var>port</var>][<code>:<var>host</var></code>]</dt>
+          <dd>
+            <p>
+              Listens for connections on the specified TCP <var>port</var>.
+              Specify 0 for <var>port</var> to have the kernel automatically
+              choose an available port.  If <var>host</var>, which can either
+              be a DNS name (if built with unbound library) or an IP address,
+              is specified, then connections are restricted to the resolved or
+              specified local IP address (either IPv4 or IPv6 address).  If
+              <var>host</var> is an IPv6 address, wrap it in square brackets,
+              e.g. <code>ptcp:6640:[::1]</code>.  If <var>host</var> is not
+              specified then it listens only on IPv4 addresses.
+            </p>
+            <p>
+              If <var>port</var> is not specified, it defaults to 6640.
+            </p>
+          </dd>
+        </dl>
+        <p>When multiple clients are configured, the <ref column="target"/>
+        values must be unique.  Duplicate <ref column="target"/> values yield
+        unspecified results.</p>
+      </column>
+    </group>
+
+    <group title="Client Failure Detection and Handling">
+      <column name="max_backoff">
+        Maximum number of milliseconds to wait between connection attempts.
+        Default is implementation-specific.
+      </column>
+
+      <column name="inactivity_probe">
+        Maximum number of milliseconds of idle time on connection to the client
+        before sending an inactivity probe message.  If Open vSwitch does not
+        communicate with the client for the specified number of seconds, it
+        will send a probe.  If a response is not received for the same
+        additional amount of time, Open vSwitch assumes the connection has been
+        broken and attempts to reconnect.  Default is implementation-specific.
+        A value of 0 disables inactivity probes.
+      </column>
+    </group>
+
+    <group title="Status">
+      <p>
+        Key-value pair of <ref column="is_connected"/> is always updated.
+        Other key-value pairs in the status columns may be updated depends
+        on the <ref column="target"/> type.
+      </p>
+
+      <p>
+        When <ref column="target"/> specifies a connection method that
+        listens for inbound connections (e.g. <code>ptcp:</code> or
+        <code>punix:</code>), both <ref column="n_connections"/> and
+        <ref column="is_connected"/> may also be updated while the
+        remaining key-value pairs are omitted.
+      </p>
+
+      <p>
+        On the other hand, when <ref column="target"/> specifies an
+        outbound connection, all key-value pairs may be updated, except
+        the above-mentioned two key-value pairs associated with inbound
+        connection targets. They are omitted.
+      </p>
+
+      <column name="is_connected">
+        <code>true</code> if currently connected to this client,
+        <code>false</code> otherwise.
+      </column>
+
+      <column name="status" key="last_error">
+        A human-readable description of the last error on the connection
+        to the manager; i.e. <code>strerror(errno)</code>.  This key
+        will exist only if an error has occurred.
+      </column>
+
+      <column name="status" key="state"
+              type='{"type": "string", "enum": ["set", ["VOID", "BACKOFF", "CONNECTING", 
"ACTIVE", "IDLE"]]}'>
+        <p>
+          The state of the connection to the manager:
+        </p>
+        <dl>
+          <dt><code>VOID</code></dt>
+          <dd>Connection is disabled.</dd>
+
+          <dt><code>BACKOFF</code></dt>
+          <dd>Attempting to reconnect at an increasing period.</dd>
+
+          <dt><code>CONNECTING</code></dt>
+          <dd>Attempting to connect.</dd>
+
+          <dt><code>ACTIVE</code></dt>
+          <dd>Connected, remote host responsive.</dd>
+
+          <dt><code>IDLE</code></dt>
+          <dd>Connection is idle.  Waiting for response to keep-alive.</dd>
+        </dl>
+        <p>
+          These values may change in the future.  They are provided only for
+          human consumption.
+        </p>
+      </column>
+
+      <column name="status" key="sec_since_connect"
+              type='{"type": "integer", "minInteger": 0}'>
+        The amount of time since this client last successfully connected
+        to the database (in seconds). Value is empty if client has never
+        successfully been connected.
+      </column>
+
+      <column name="status" key="sec_since_disconnect"
+              type='{"type": "integer", "minInteger": 0}'>
+        The amount of time since this client last disconnected from the
+        database (in seconds). Value is empty if client has never
+        disconnected.
+      </column>
+
+      <column name="status" key="locks_held">
+        Space-separated list of the names of OVSDB locks that the connection
+        holds.  Omitted if the connection does not hold any locks.
+      </column>
+
+      <column name="status" key="locks_waiting">
+        Space-separated list of the names of OVSDB locks that the connection is
+        currently waiting to acquire.  Omitted if the connection is not waiting
+        for any locks.
+      </column>
+
+      <column name="status" key="locks_lost">
+        Space-separated list of the names of OVSDB locks that the connection
+        has had stolen by another OVSDB client.  Omitted if no locks have been
+        stolen from this connection.
+      </column>
+
+      <column name="status" key="n_connections"
+              type='{"type": "integer", "minInteger": 2}'>
+        When <ref column="target"/> specifies a connection method that
+        listens for inbound connections (e.g. <code>ptcp:</code> or
+        <code>pssl:</code>) and more than one connection is actually active,
+        the value is the number of active connections.  Otherwise, this
+        key-value pair is omitted.
+      </column>
+
+      <column name="status" key="bound_port" type='{"type": "integer"}'>
+        When <ref column="target"/> is <code>ptcp:</code> or
+        <code>pssl:</code>, this is the TCP port on which the OVSDB server is
+        listening.  (This is particularly useful when <ref
+        column="target"/> specifies a port of 0, allowing the kernel to
+        choose any available port.)
+      </column>
+    </group>
+
+    <group title="Common Columns">
+      The overall purpose of these columns is described under <code>Common
+      Columns</code> at the beginning of this document.
+
+      <column name="external_ids"/>
+      <column name="other_config"/>
+    </group>
+  </table>
+
+  <table name="SSL">
+    SSL/TLS configuration for ovn-nb database access.
+
+    <column name="private_key">
+      Name of a PEM file containing the private key used as the switch's
+      identity for SSL/TLS connections to the controller.
+    </column>
+
+    <column name="certificate">
+      Name of a PEM file containing a certificate, signed by the
+      certificate authority (CA) used by the controller and manager,
+      that certifies the switch's private key, identifying a trustworthy
+      switch.
+    </column>
+
+    <column name="ca_cert">
+      Name of a PEM file containing the CA certificate used to verify
+      that the switch is connected to a trustworthy controller.
+    </column>
+
+    <column name="bootstrap_ca_cert">
+      If set to <code>true</code>, then Open vSwitch will attempt to
+      obtain the CA certificate from the controller on its first SSL/TLS
+      connection and save it to the named PEM file. If it is successful,
+      it will immediately drop the connection and reconnect, and from then
+      on all SSL/TLS connections must be authenticated by a certificate signed
+      by the CA certificate thus obtained.  <em>This option exposes the
+      SSL/TLS connection to a man-in-the-middle attack obtaining the initial
+      CA certificate.</em>  It may still be useful for bootstrapping.
+    </column>
+
+    <column name="ssl_protocols">
+      <p>
+        Range or a comma- or space-delimited list of the SSL/TLS protocols to
+        enable for SSL/TLS connections.
+      </p>
+      <p>
+        Supported protocols include <code>TLSv1.2</code> and
+        <code>TLSv1.3</code>.  Ranges can be provided in a form of two protocol
+        names separated with a dash (<code>TLSv1.2-TLSv1.3</code>), or as a
+        single protocol name with a plus sign (<code>TLSv1.2+</code>).  The
+        value can be a list of protocols or exactly one range.  The range is a
+        preferred way of specifying protocols and the configuration always
+        behaves as if the range between the minimum and the maximum specified
+        version is provided, i.e., if the value is set to
+        <code>TLSv1.X,TLSv1.(X+2)</code>, the <code>TLSv1.(X+1)</code> will
+        also be enabled as if it was a range.
+        Regardless of order, the highest protocol supported by both sides will
+        be chosen when making the connection.
+      </p>
+      <p>
+        The default when this option is omitted is <code>TLSv1.2+</code>.
+      </p>
+    </column>
+
+    <column name="ssl_ciphers">
+      List of ciphers (in OpenSSL cipher string format) to be supported
+      for SSL/TLS connections with TLSv1.2.  The default when this option
+      is omitted is <code>DEFAULT:@SECLEVEL=2</code>.
+    </column>
+
+    <column name="ssl_ciphersuites">
+      List of ciphersuites (in OpenSSL ciphersuites string format) to be
+      supported for SSL/TLS connections with TLSv1.3 and later.  Default value
+      from OpenSSL will be used when this option is omitted.
+    </column>
+
+    <group title="Common Columns">
+      The overall purpose of these columns is described under <code>Common
+      Columns</code> at the beginning of this document.
+
+      <column name="external_ids"/>
+    </group>
+  </table>
+
+  <table name="Bridge" title="OVS Bridge to control">
+    <column name="name">
+      Name of the OVS bridge.  This bridge should exist in the local OVS
+      database.
+    </column>
+
+    <column name="options">
+      Reserved for future use.
+    </column>
+
+    <column name="external_ids">
+      See <em>External IDs</em> at the beginning of this document.
+    </column>
+  </table>
+
+  <table name="Logical_Flow" title="Logical flow">
+    <column name="bridge">
+      The bridge to which the logical flow belongs to.
+    </column>
+
+    <column name="table_id">
+      The stage in the logical pipeline, analogous to an OpenFlow table number.
+    </column>
+
+    <column name="priority">
+      The flow's priority.  Flows with numerically higher priority take
+      precedence over those with lower.  If two logical flows with the
+      same priority both match, then the one actually applied to the packet is
+      undefined.
+    </column>
+
+    <column name="match">
+      <p>
+        A matching expression.  OVN provides a superset of OpenFlow matching
+        capabilities, using a syntax similar to Boolean expressions in a
+        programming language.
+      </p>
+
+      <p>
+        Please see the documentation of the
+        <ref
+        column="match" table="Logical_Flow" db="OVN_Southbound"/>
+        column of the <ref table="Logical_Flow"
+        db="OVN_Southbound"/> table in the <ref db="OVN_Southbound"/> database.
+      </p>
+    </column>
+
+    <column name="actions">
+      <p>
+        Logical datapath actions, to be executed when the logical flow
+        represented by this row is the highest-priority match.
+      </p>
+
+      <p>
+        Actions share lexical syntax with the <ref column="match"/> column.  An
+        empty set of actions (or one that contains just white space or
+        comments), or a set of actions that consists of just
+        <code>drop;</code>, causes the matched packets to be dropped.
+        Otherwise, the column should contain a sequence of actions, each
+        terminated by a semicolon.
+      </p>
+
+      <p>
+        Please see the documentation of the
+        <ref
+        column="actions" table="Logical_Flow" db="OVN_Southbound"/>
+        column of the <ref table="Logical_Flow"
+        db="OVN_Southbound"/> table in the <ref db="OVN_Southbound"/> database.
+      </p>
+    </column>
+
+    <column name="external_ids">
+      See <em>External IDs</em> at the beginning of this document.
+    </column>
+  </table>
+</database>
diff --git a/tutorial/ovn-sandbox b/tutorial/ovn-sandbox
index ed334d1c31..239db58f35 100755
--- a/tutorial/ovn-sandbox
+++ b/tutorial/ovn-sandbox
@@ -64,6 +64,8 @@ gdb_ovn_controller=false
  gdb_ovn_controller_ex=false
  gdb_ovn_controller_vtep=false
  gdb_ovn_controller_vtep_ex=false
+gdb_ovn_br_controller=false
+gdb_ovn_br_controller_ex=false
  builddir=
  ovsbuilddir=
  srcdir=
@@ -90,6 +92,7 @@ ic_nb_servers=3
  ic_sb_model=clustered
  ic_sb_servers=3
  dummy=override
+ovn_br=false
for option; do
      # This option-parsing mechanism borrowed from a Autoconf-generated
@@ -339,6 +342,9 @@ EOF
          --ic-sb-m*)
              prev=ic_sb_model
              ;;
+        --ovn-br)
+            ovn_br=true
+            ;;
          -R|--gdb-run)
              gdb_vswitchd_ex=true
              gdb_ovsdb_ex=true
@@ -422,6 +428,11 @@ if $built; then
          echo >&2 'source directory not found, please use --srcdir'
          exit 1
      fi
+    br_schema=$srcdir/ovn-br.ovsschema
+    if test ! -e "$br_schema"; then
+        echo >&2 'source directory not found, please use --srcdir'
+        exit 1
+    fi
# Put built tools early in $PATH.
      if test ! -e $ovsbuilddir/vswitchd/ovs-vswitchd; then
@@ -429,7 +440,7 @@ if $built; then
          exit 1
      fi
      
PATH=$ovsbuilddir/ovsdb:$ovsbuilddir/vswitchd:$ovsbuilddir/utilities:$ovsbuilddir/vtep:$PATH
-    
PATH=$builddir/controller:$builddir/controller-vtep:$builddir/northd:$builddir/ic:$builddir/utilities:$PATH
+    
PATH=$builddir/controller:$builddir/controller-vtep:$builddir/northd:$builddir/ic:$builddir/utilities:$builddir/br-controller:$PATH
      export PATH
  else
      case $schema in
@@ -616,6 +627,10 @@ ovn_start_db sb "$sbdb_model" "$sbdb_servers" 
"$ovnsb_source"
  ovn_start_db ic_nb "$ic_nb_model" "$ic_nb_servers" "$ic_nb_schema"
  ovn_start_db ic_sb "$ic_sb_model" "$ic_sb_servers" "$ic_sb_schema"
+if $ovn_br; then
+    ovn_start_db br standalone 1 "$br_schema"
+fi
+
  #Add a small delay to allow ovsdb-server to launch.
  sleep 0.1
@@ -692,6 +707,13 @@ rungdb $gdb_ovn_controller_vtep $gdb_ovn_controller_vtep_ex \
      $OVN_CTRLR_PKI --log-file -vsyslog:off \
      --ovnsb-db="$OVN_SB_DB"
+if $ovn_br; then
+    run ovs-vsctl set open . external-ids:ovn-br-remote=$OVN_BR_DB
+    rungdb $gdb_ovn_br_controller $gdb_ovn_br_controller_ex ovn-br-controller \
+        --detach --no-chdir -vsyslog:off \
+        --log-file=ovn-br-controller.log \
+        --pidfile=ovn-br-controller.pid -vconsole:off
+fi
  cat <<EOF
diff --git a/utilities/automake.mk b/utilities/automake.mk
index 1de33614f9..3e3f2e08c9 100644
--- a/utilities/automake.mk
+++ b/utilities/automake.mk
@@ -119,4 +119,12 @@ bin_PROGRAMS += utilities/ovn-debug
  utilities_ovn_debug_SOURCES = utilities/ovn-debug.c
  utilities_ovn_debug_LDADD = lib/libovn.la $(OVSDB_LIBDIR)/libovsdb.la 
$(OVS_LIBDIR)/libopenvswitch.la
+# ovn-brctl
+bin_PROGRAMS += utilities/ovn-brctl
+utilities_ovn_brctl_SOURCES = \
+    utilities/ovn-dbctl.c \
+    utilities/ovn-dbctl.h \
+    utilities/ovn-brctl.c
+utilities_ovn_brctl_LDADD = lib/libovn.la $(OVSDB_LIBDIR)/libovsdb.la 
$(OVS_LIBDIR)/libopenvswitch.la
+
  include utilities/bugtool/automake.mk
diff --git a/utilities/ovn-brctl.c b/utilities/ovn-brctl.c
new file mode 100644
index 0000000000..051a897748
--- /dev/null
+++ b/utilities/ovn-brctl.c
@@ -0,0 +1,585 @@
+/*
+ * 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.
+ */
+
+ #include <config.h>
+
+#include <getopt.h>
+#include <inttypes.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+/* OVS includes. */
+#include "command-line.h"
+#include "daemon.h"
+#include "db-ctl-base.h"
+#include "dirs.h"
+#include "openvswitch/vlog.h"
+
+/* OVN includes. */
+#include "lib/ovn-br-idl.h"
+#include "lib/ovn-dirs.h"
+#include "lib/ovn-util.h"
+#include "lib/vec.h"
+#include "ovn-dbctl.h"
+
+VLOG_DEFINE_THIS_MODULE(brctl);
+
+static char * OVS_WARN_UNUSED_RESULT br_by_name_or_uuid(
+    struct ctl_context *ctx, const char *id, bool must_exist,
+    const struct ovnbrrec_bridge **br_p);
+
+static void
+brctl_add_base_prerequisites(struct ovsdb_idl *idl,
+                             enum nbctl_wait_type wait_type OVS_UNUSED)
+{
+    ovsdb_idl_add_table(idl, &ovnbrrec_table_br_global);
+}
+
+static void
+brctl_pre_execute(struct ovsdb_idl *idl, struct ovsdb_idl_txn *txn,
+                  enum nbctl_wait_type wait_type OVS_UNUSED)
+{
+    const struct ovnbrrec_br_global *br = ovnbrrec_br_global_first(idl);
+    if (!br) {
+        br = ovnbrrec_br_global_insert(txn);
+    }
+}
+
+static int
+get_inactivity_probe(struct ovsdb_idl *idl)
+{
+    const struct ovnbrrec_br_global *pr = ovnbrrec_br_global_first(idl);
+    int interval = DEFAULT_UTILS_PROBE_INTERVAL_MSEC;
+
+    if (pr) {
+        interval = smap_get_int(&pr->options, "brctl_probe_interval",
+                                interval);
+    }
+
+    return interval;
+}
+
+/* ovn-brctl specific context.  Inherits the 'struct ctl_context' as base. */
+struct brctl_context {
+    struct ctl_context base;
+
+    /* A cache of the contents of the database.
+     *
+     * A command that needs to use any of this information must first call
+     * sbctl_context_populate_cache().  A command that changes anything that
+     * could invalidate the cache must either call
+     * sbctl_context_invalidate_cache() or manually update the cache to

The comment references sbctl_ functions instead of brctl_ functions. However, neither sbctl_context_populate_cache() nor brctl_context_populate_cache() actually exist. In the case of ovn-sbctl, it maintains a cache of some objects, but ovn-brctl is not doing that. I think that you can remove the cache_valid field altogether.

+     * maintain its correctness. */
+    bool cache_valid;
+};
+
+/* Casts 'base' into 'struct sbctl_context'. */
+static struct brctl_context *
+brctl_context_cast(struct ctl_context *base)
+{
+    return CONTAINER_OF(base, struct brctl_context, base);
+}
+
+static struct ctl_context *
+brctl_ctx_create(void)
+{
+    struct brctl_context *prctx = xmalloc(sizeof *prctx);
+    *prctx = (struct brctl_context) {
+        .cache_valid = false,
+    };
+    return &prctx->base;
+}
+
+static void
+brctl_context_invalidate_cache(struct ctl_context *ctx)
+{
+    struct brctl_context *brctl_ctx = brctl_context_cast(ctx);
+
+    if (!brctl_ctx->cache_valid) {
+        return;
+    }
+    brctl_ctx->cache_valid = false;
+}
+
+static void
+brctl_ctx_destroy(struct ctl_context *ctx)
+{
+    brctl_context_invalidate_cache(ctx);
+    free(ctx);
+}
+
+static void
+print_br(const struct ovnbrrec_bridge *br, struct ds *s)
+{
+    ds_put_format(s, "bridge "UUID_FMT" (%s)\n",
+                  UUID_ARGS(&br->header_.uuid), br->name);
+}
+
+static void
+brctl_init(struct ctl_context *ctx OVS_UNUSED)
+{
+}
+
+static void
+brctl_pre_show(struct ctl_context *ctx)
+{
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_bridge_col_name);
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_bridge_col_options);
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_bridge_col_external_ids);
+}
+
+static const struct ctl_table_class tables[OVNBRREC_N_TABLES] = {
+    [OVNBRREC_TABLE_BRIDGE].row_ids[0]
+    = {&ovnbrrec_bridge_col_name, NULL, NULL},
+
+    [OVNBRREC_TABLE_CONNECTION].row_ids[0]
+    = {&ovnbrrec_connection_col_target, NULL, NULL},
+};
+
+static void
+brctl_show(struct ctl_context *ctx)
+{
+    const struct ovnbrrec_bridge *br;
+
+    if (ctx->argc == 2) {
+        char *error = br_by_name_or_uuid(ctx, ctx->argv[1], true, &br);
+        if (error) {
+            ctx->error = error;
+            return;
+        }
+
+        print_br(br, &ctx->output);
+    } else {
+        OVNBRREC_BRIDGE_FOR_EACH (br, ctx->idl) {
+            print_br(br, &ctx->output);
+        }
+    }
+}
+
+static void
+pre_get_info(struct ctl_context *ctx)
+{
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_bridge_col_name);
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_bridge_col_options);
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_bridge_col_external_ids);
+
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_logical_flow_col_actions);
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_logical_flow_col_bridge);
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_logical_flow_col_match);
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_logical_flow_col_priority);
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_logical_flow_col_table_id);
+    ovsdb_idl_add_column(ctx->idl, &ovnbrrec_logical_flow_col_external_ids);
+}
+
+static bool
+is_uuid_with_prefix(const char *uuid)
+{
+     return uuid[0] == '0' && (uuid[1] == 'x' || uuid[1] == 'X');
+}
+
+static const char *
+strip_leading_zero(const char *s)
+{
+    return s + strspn(s, "0");
+}
+
+static bool
+is_partial_uuid_match(const struct uuid *uuid, const char *match)
+{
+    char uuid_s[UUID_LEN + 1];
+    snprintf(uuid_s, sizeof uuid_s, UUID_FMT, UUID_ARGS(uuid));
+
+    /* We strip leading zeros because we want to accept cookie values derived
+     * from UUIDs, and cookie values are printed without leading zeros because
+     * they're just numbers. */
+    const char *s1 = strip_leading_zero(uuid_s);
+    const char *s2 = match;
+    if (is_uuid_with_prefix(s2)) {
+        s2 = s2 + 2;
+    }
+    s2 = strip_leading_zero(s2);
+    return !strncmp(s1, s2, strlen(s2));
+}

strip_leading_zero() and is_partial_uuid_match() are ripped straight from ovn-sbctl.c . These should probably be placed in ovn-util so that both utilities can reference the same functions.

+
+
+struct brctl_lflow {
+    const struct ovnbrrec_logical_flow *lflow;
+    const struct ovnbrrec_bridge *br;
+};
+
+static int
+brctl_lflow_cmp(const void *a_, const void *b_)
+{
+    const struct brctl_lflow *a_ctl_lflow = a_;
+    const struct brctl_lflow *b_ctl_lflow = b_;
+
+    const struct ovnbrrec_logical_flow *a = a_ctl_lflow->lflow;
+    const struct ovnbrrec_logical_flow *b = b_ctl_lflow->lflow;
+
+    const struct ovnbrrec_bridge *abr = a_ctl_lflow->br;
+    const struct ovnbrrec_bridge *bbr = b_ctl_lflow->br;
+    const char *a_name = abr->name;
+    const char *b_name = bbr->name;
+    int cmp = strcmp(a_name, b_name);
+    if (cmp) {
+        return cmp;
+    }
+
+    cmp = uuid_compare_3way(&abr->header_.uuid, &bbr->header_.uuid);
+    if (cmp) {
+        return cmp;
+    }
+
+    cmp = (a->table_id > b->table_id ? 1
+           : a->table_id < b->table_id ? -1
+           : a->priority > b->priority ? -1
+           : a->priority < b->priority ? 1
+           : strcmp(a->match, b->match));
+    return cmp ? cmp : strcmp(a->actions, b->actions);
+}
+
+static void
+brctl_lflow_add(struct vector *lflows,
+                const struct ovnbrrec_logical_flow *lflow,
+                const struct ovnbrrec_bridge *br)
+{
+    struct brctl_lflow brctl_lflow = (struct brctl_lflow) {
+        .lflow = lflow,
+        .br = br,
+    };
+    vector_push(lflows, &brctl_lflow);
+}
+
+static void
+print_uuid_part(const struct uuid *uuid, bool do_print, struct ds *s)
+{
+    if (!do_print) {
+        return;
+    }
+    ds_put_format(s, "uuid=0x%08"PRIx32", ", uuid->parts[0]);
+}
+
+static void
+print_bridge_prompt(const struct ovnbrrec_bridge *br,
+                    const struct uuid *uuid, struct ds *s) {
+        ds_put_format(s, "Bridge: %s", br->name);
+        ds_put_format(s, " ("UUID_FMT")\n",
+                      UUID_ARGS(uuid));
+}
+
+static char * OVS_WARN_UNUSED_RESULT
+parse_priority(const char *arg, int64_t *priority_p)
+{
+    /* Validate priority. */
+    int64_t priority;
+    if (!ovs_scan(arg, "%"SCNd64, &priority)
+        || priority < 0 || priority > 32767) {
+        /* Priority_p could be uninitialized as no valid priority was
+         * input, initialize it to a valid value of 0 before returning */
+        *priority_p = 0;
+        return xasprintf("%s: priority must in range 0...32767", arg);
+    }
+    *priority_p = priority;
+    return NULL;
+}
+
+static char * OVS_WARN_UNUSED_RESULT
+parse_table_id(const char *arg, int64_t *table_p)
+{
+    /* Validate table id. */
+    int64_t table;
+    if (!ovs_scan(arg, "%"SCNd64, &table)
+        || table < 0 || table > 55) {
+        /* table_p could be uninitialized as no valid table id was
+         * input, initialize it to a valid value of 0 before returning */
+        *table_p = 0;
+        return xasprintf("%s: table must in range 0...55", arg);
+    }
+    *table_p = table;
+    return NULL;
+}
+
+static char * OVS_WARN_UNUSED_RESULT
+br_by_name_or_uuid(struct ctl_context *ctx, const char *id, bool must_exist,
+                   const struct ovnbrrec_bridge **br_p)
+{
+    const struct ovsdb_idl_row *row = NULL;
+
+    char *error = ctl_get_row(ctx, &ovnbrrec_table_bridge,
+                              id, must_exist, &row);
+    *br_p = (const struct ovnbrrec_bridge *) row;
+    return error;
+}
+
+static void
+cmd_br_add(struct ctl_context *ctx)
+{
+    const struct ovnbrrec_bridge *bridge = NULL;
+
+    char *error = br_by_name_or_uuid(ctx, ctx->argv[1], false, &bridge);
+    if (error) {
+        ctl_error(ctx, "%s", error);
+        free(error);
+        return;
+    }
+
+    if (bridge) {
+        ctl_error(ctx, "Bridge %s already exists", ctx->argv[1]);
+    }
+
+    bridge = ovnbrrec_bridge_insert(ctx->txn);
+    ovnbrrec_bridge_set_name(bridge, ctx->argv[1]);
+}
+
+static void
+cmd_br_del(struct ctl_context *ctx)
+{
+    const struct ovnbrrec_bridge *bridge = NULL;
+
+    char *error = br_by_name_or_uuid(ctx, ctx->argv[1], true, &bridge);
+    if (error) {
+        ctl_error(ctx, "%s", error);
+        free(error);
+        return;
+    }
+
+    ovnbrrec_bridge_delete(bridge);
+}
+
+static void
+cmd_lflow_list(struct ctl_context *ctx)
+{
+    struct vector lflows = VECTOR_EMPTY_INITIALIZER(struct brctl_lflow);
+    const struct ovnbrrec_bridge *bridge = NULL;
+
+    if (ctx->argc > 1) {
+        char *error = br_by_name_or_uuid(ctx, ctx->argv[1], false, &bridge);
+        if (error) {
+            ctl_error(ctx, "%s", error);
+            free(error);
+            return;
+        }
+
+        if (bridge) {
+            ctx->argc--;
+            ctx->argv++;
+        }
+    }
+
+    const struct ovnbrrec_logical_flow *lflow;
+    OVNBRREC_LOGICAL_FLOW_FOR_EACH (lflow, ctx->idl) {
+        if (bridge && lflow->bridge != bridge) {
+            continue;
+        }
+
+        brctl_lflow_add(&lflows, lflow, lflow->bridge);
+    }
+
+    vector_qsort(&lflows, brctl_lflow_cmp);
+
+    bool print_uuid = shash_find(&ctx->options, "--uuid") != NULL;
+
+    const struct brctl_lflow *curr, *prev = NULL;
+    VECTOR_FOR_EACH_PTR (&lflows, curr) {
+        /* Figure out whether to print this particular flow.  By default, we
+         * print all flows, but if any UUIDs were listed on the command line
+         * then we only print the matching ones. */
+        bool include;
+        if (ctx->argc > 1) {
+            include = false;
+            for (size_t j = 1; j < ctx->argc; j++) {
+                if (is_partial_uuid_match(&curr->lflow->header_.uuid,
+                                          ctx->argv[j])) {
+                    include = true;
+                    break;
+                }
+            }
+        } else {
+            include = true;
+        }
+        if (!include) {
+            continue;
+        }
+
+        /* Print a header line for this datapath or pipeline, if we haven't
+         * already done so. */
+        if (!prev || prev->br != curr->br) {
+               print_bridge_prompt(curr->br, &curr->br->header_.uuid,
+                                     &ctx->output);
+        }
+
+        /* Print the flow. */
+        ds_put_cstr(&ctx->output, "  ");
+        print_uuid_part(&curr->lflow->header_.uuid, print_uuid, &ctx->output);
+        ds_put_format(&ctx->output,
+                      "table=%-2"PRId64", priority=%-5"PRId64
+                      ", match=(%s), action=(%s)\n",
+                      curr->lflow->table_id,
+                      curr->lflow->priority, curr->lflow->match,
+                      curr->lflow->actions);
+        prev = curr;
+    }
+
+    vector_destroy(&lflows);
+}
+
+static void
+cmd_lflow_add(struct ctl_context *ctx)
+{
+    const struct ovnbrrec_bridge *bridge = NULL;
+
+    char *error = br_by_name_or_uuid(ctx, ctx->argv[1], true, &bridge);
+    if (error) {
+        ctl_error(ctx, "%s", error);
+        free(error);
+        return;
+    }
+
+    int64_t table_id;
+    error = parse_table_id(ctx->argv[2], &table_id);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    int64_t priority;
+    error = parse_priority(ctx->argv[3], &priority);
+    if (error) {
+        ctx->error = error;
+        return;
+    }
+
+    struct ovnbrrec_logical_flow *lflow =
+        ovnbrrec_logical_flow_insert(ctx->txn);
+    ovnbrrec_logical_flow_set_bridge(lflow, bridge);
+    ovnbrrec_logical_flow_set_table_id(lflow, table_id);
+    ovnbrrec_logical_flow_set_priority(lflow, priority);
+    ovnbrrec_logical_flow_set_match(lflow, ctx->argv[4]);
+    ovnbrrec_logical_flow_set_actions(lflow, ctx->argv[5]);
+}
+
+static void
+cmd_lflow_del(struct ctl_context *ctx)
+{
+    const struct ovnbrrec_logical_flow *lflow;
+    const struct ovsdb_idl_row *row;
+
+    char *error = ctl_get_row(ctx, &ovnbrrec_table_logical_flow,
+                              ctx->argv[1], true, &row);
+    if (error) {
+        ctl_error(ctx, "%s", error);
+        free(error);
+        return;
+    }
+
+    lflow = (const struct ovnbrrec_logical_flow *) row;
+    ovnbrrec_logical_flow_delete(lflow);
+}
+
+static void
+cmd_lflows_del(struct ctl_context *ctx)
+{
+    const struct ovnbrrec_bridge *bridge = NULL;
+
+    if (ctx->argc > 1) {
+        char *error = br_by_name_or_uuid(ctx, ctx->argv[1], true, &bridge);
+        if (error) {
+            ctl_error(ctx, "%s", error);
+            free(error);
+            return;
+        }
+    }
+
+    const struct ovnbrrec_logical_flow *lflow;
+    OVNBRREC_LOGICAL_FLOW_FOR_EACH_SAFE (lflow, ctx->idl) {
+        if (!bridge || lflow->bridge == bridge) {
+            ovnbrrec_logical_flow_delete(lflow);
+        }
+    }
+}
+
+static const struct ctl_command_syntax brctl_commands[] = {
+    { "init", 0, 0, "", NULL, brctl_init, NULL, "", RW },
+    { "show", 0, 1, "[BRIDGE]", brctl_pre_show, brctl_show, NULL, "", RO },
+
+    /* Bridge commands. */
+    {"add-br", 1, 1, "BRIDGE", pre_get_info, cmd_br_add, NULL,
+     "", RW},
+    {"del-br", 1, 1, "BRIDGE", pre_get_info, cmd_br_del, NULL,
+     "", RW},
+
+    /* Logical flow commands */
+    {"lflow-list", 0, INT_MAX, "[BRIDGE] [LFLOW...]",
+     pre_get_info, cmd_lflow_list, NULL,
+     "--uuid,--ovs?,--stats,--vflows?", RO},
+    {"dump-flows", 0, INT_MAX, "[DATAPATH] [LFLOW...]",
+     pre_get_info, cmd_lflow_list, NULL,
+     "--uuid,--ovs?,--stats,--vflows?",
+     RO}, /* Friendly alias for lflow-list */
+    {"add-flow", 5, 5, "BRIDGE TABLE PRIORITY MATCH ACTION",
+     pre_get_info, cmd_lflow_add, NULL,
+     "", RW},
+     {"del-flow", 1, 1, "UUID", pre_get_info, cmd_lflow_del, NULL,
+     "", RW},
+     {"del-flows", 0, 1, "[BRIDGE]", pre_get_info, cmd_lflows_del, NULL,
+     "", RW},
+     {NULL, 0, 0, NULL, NULL, NULL, NULL, NULL, RO},
+};
+
+static void
+brctl_usage(void)
+{
+    printf("\
+%s: OVN Provider DB management utility\n\
+usage: %s [OPTIONS] COMMAND [ARG...]\n\
+\n\
+General commands:\n\
+  init                      initialize the database\n\
+  show                      print overview of database contents\n\
+\n\
+Logical flow commands:\n\
+  lflow-list  [BRIDGE] [LFLOW...] list logical flows for BRIDGE\n\
+  dump-flows  [BRIDGE] [LFLOW...] alias for lflow-list\n\
+\n\
+\n\n", program_name, program_name);
+}
+
+int
+main(int argc, char *argv[])
+{
+    struct ovn_dbctl_options dbctl_options = {
+        .db_version = ovnbrrec_get_db_version(),
+        .default_db = default_br_db(),
+        .allow_wait = false,
+
+        .options_env_var_name = "OVN_brctl_OPTIONS",
+        .daemon_env_var_name = "OVN_PR_DAEMON",
+
+        .idl_class = &ovnbrrec_idl_class,
+        .tables = tables,
+        .cmd_show_table = NULL,
+        .commands = brctl_commands,
+
+        .usage = brctl_usage,
+        .add_base_prerequisites = brctl_add_base_prerequisites,
+        .pre_execute = brctl_pre_execute,
+        .post_execute = NULL,
+        .get_inactivity_probe = get_inactivity_probe,
+
+        .ctx_create = brctl_ctx_create,
+        .ctx_destroy = brctl_ctx_destroy,
+    };
+
+    return ovn_dbctl_main(argc, argv, &dbctl_options);
+}

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to