Skip to content

Repository files navigation

@sourceregistry/node-ovsdb

License npm version codecov CI

Low-level OVSDB client for Node.js with:

  • Unix socket, TCP, and TLS transports
  • RFC 7047 core RPC support
  • Open vSwitch monitor extensions
  • works against OVN's OVN_Northbound/OVN_Southbound too — same protocol, different schema
  • typed transaction and monitor payloads
  • event-driven notifications
  • schema-to-TypeScript generation CLI
  • TSDoc-ready public API for Typedoc

Installation

npm install @sourceregistry/node-ovsdb

Generate types from a schema with:

npx ovsdb-generate --help

What It Supports

The client is intentionally low-level and maps closely to the wire protocol.

  • list_dbs
  • get_schema
  • transact
  • cancel
  • monitor
  • monitor_cond
  • monitor_cond_change
  • monitor_cond_since
  • monitor_cancel
  • lock
  • steal
  • unlock
  • echo
  • set_db_change_aware
  • get_server_id
  • convert
  • notifications: update, update2, update3, locked, stolen

Quick Start

Unix socket:

import {OVSDBClient} from "@sourceregistry/node-ovsdb";

const client = new OVSDBClient({
  socketPath: "/var/run/openvswitch/db.sock",
  timeout: 5000
});

try {
  await client.connect();

  const databases = await client.listDbs();
  const schema = await client.getSchema("Open_vSwitch");

  console.log(databases, schema.version);
} finally {
  await client.close();
}

Plain TCP:

import {OVSDBClient} from "@sourceregistry/node-ovsdb";

const client = new OVSDBClient({
  host: "127.0.0.1",
  port: 6640
});

TLS:

import {OVSDBClient} from "@sourceregistry/node-ovsdb";

const client = new OVSDBClient({
  host: "ovsdb.example.internal",
  port: 6640,
  tls: true,
  tlsOptions: {
    servername: "ovsdb.example.internal",
    rejectUnauthorized: true
  }
});

Schema Generation

The package includes an ovsdb-generate CLI that emits TypeScript row and database model types you can use with OVSDBClient<...>.

Generate from a checked-in schema file:

npx ovsdb-generate --schema ./Open_vSwitch.schema.json --out ./src/generated/ovsdb.ts

Generate directly from a live OVSDB server:

npx ovsdb-generate --socket /var/run/openvswitch/db.sock --db Open_vSwitch --out ./src/generated/ovsdb.ts

Generate from a live TCP endpoint:

npx ovsdb-generate --host 127.0.0.1 --port 6640 --db Open_vSwitch --out ./src/generated/ovsdb.ts

Generate from a live TLS endpoint:

npx ovsdb-generate \
  --host ovsdb.example.internal \
  --port 6640 \
  --tls \
  --tls-ca-file ./pki/ca.pem \
  --tls-cert-file ./pki/client.pem \
  --tls-key-file ./pki/client.key \
  --db Open_vSwitch \
  --out ./src/generated/ovsdb.ts

You can override the generated top-level type name with --name OpenVSwitchDb.

Typed Transactions

You can provide your own table model to get typed table names, rows, selected columns, conditions, mutations, and tuple-shaped transaction results.

import {OVSDBClient, type DatabaseOperation, type OvsSet} from "@sourceregistry/node-ovsdb";

type OpenVSwitchDb = {
  Bridge: {
    name: string;
    ports: OvsSet<string>;
  };
  Port: {
    name: string;
    interfaces: OvsSet<string>;
  };
};

const client = new OVSDBClient<OpenVSwitchDb>();
await client.connect();

const operations = [
  {
    op: "select",
    table: "Bridge",
    where: [["name", "==", "br-int"]],
    columns: ["name", "ports"]
  },
  {
    op: "insert",
    table: "Port",
    row: {
      name: "uplink0",
      interfaces: ["set", []]
    }
  }
] satisfies [DatabaseOperation<OpenVSwitchDb>, DatabaseOperation<OpenVSwitchDb>];

const [bridges, insertedPort] = await client.transact("Open_vSwitch", operations);

For a higher-level staged flow, use client.transaction(...). The callback can build operations against a transaction-scoped helper, and the library will send one transact request only if the callback completes successfully. By default it appends a trailing commit operation automatically.

const outcome = await client.transaction("Open_vSwitch", (tx) => {
  tx.comment("prepare bridge lookup");
  tx.select({
    op: "select",
    table: "Bridge",
    where: [["name", "==", "br-int"]],
    columns: ["name"]
  });

  return "ok";
});

Monitoring

import {OVSDBClient} from "@sourceregistry/node-ovsdb";

const client = new OVSDBClient();
await client.connect();

client.on("update", (notification) => {
  const [monitorId, updates] = notification.params;
  console.log("monitor", monitorId, updates);
});

await client.monitor("Open_vSwitch", "bridges", {
  Bridge: {
    columns: ["name"],
    select: {
      initial: true,
      insert: true,
      modify: true,
      delete: true
    }
  }
});

For conditional monitoring, use monitorCond() or monitorCondSince(). Use monitorCondChange() to change an existing conditional monitor's tables, columns, or conditions (optionally renaming it) without a cancel/re-monitor round trip.

Detect When an Interface Is Attached to a Bridge

OVSDB does not usually emit a single semantic event like "interface attached to bridge". Instead, you observe the row changes that together mean an attachment happened:

  • a new Interface row may appear
  • a new Port row may appear
  • an existing Bridge row may be modified so its ports set now includes that port

In practice, the bridge update is usually the strongest signal that something was attached to the virtual switch.

Why this works:

  • the Bridge.ports column is the relationship that tells you which ports are attached to the bridge
  • when that set grows, something new was connected to the bridge
  • you can then inspect Port and Interface tables to resolve names or metadata for the newly attached objects

If you want richer correlation, monitor Bridge, Port, and Interface together and keep a small in-memory cache keyed by UUID so you can map a changed bridge port set back to the concrete port and interface names.

Example: examples/detect-interface-added.ts

Common OVS Workflows

These examples focus on patterns that show up often in virtualized environments, where OVS is used to connect VM or container networking to a virtual switch.

Create a Bridge With an Internal Interface

What this does:

  • creates an Interface row of type internal
  • creates a Port that owns that interface
  • creates a Bridge that owns that port

Why it is done this way:

  • in OVS, a bridge usually owns ports, and ports own interfaces
  • creating all three rows in one transaction keeps the change atomic
  • named UUIDs let later operations refer to rows inserted earlier in the same transaction

Example: examples/bridge-port-interface.ts

Attach a New Interface to an Existing Bridge

What this does:

  • creates a new Interface
  • creates a Port that references that interface
  • mutates the existing bridge so the new port is added to its ports set

Why this is a common pattern:

  • hypervisors and container hosts often attach new virtual NICs dynamically
  • mutating the bridge ports set avoids rewriting the whole bridge row
  • keeping it in one transaction prevents partial attachment state

Example: examples/attach-interface-to-bridge.ts

In practice, type: "internal" is useful when you want OVS itself to create the interface device. Leaving type unset is common when attaching an already existing device such as a tap interface created by a hypervisor.

Using with OVN

This client also works against OVN (Open Virtual Network) — it's the same OVSDB wire protocol, just different databases. OVN splits state across two databases instead of one:

  • OVN_Northbound (usually /var/run/ovn/ovnnb_db.sock, port 6641) — intent: logical switches, ports, routers, ACLs, NAT, load balancers. ovn-northd compiles this into OVN_Southbound.
  • OVN_Southbound (usually /var/run/ovn/ovnsb_db.sock, port 6642) — compiled runtime state: logical flows, chassis, port bindings. Written by ovn-northd and ovn-controller; treat it as read-mostly from client code.

A hypervisor host still runs local OVS too. The mechanism that ties a VM into OVN is the local Interface.external_ids:iface-id column: set it to the name of a Logical_Switch_Port, and ovn-controller (running on that host, watching local OVS) binds the port and programs flows for it. No RPC beyond what's already documented above is involved.

Because OVN's schemas are large, generate types instead of hand-writing them:

npx ovsdb-generate --schema ./ovn-nb.ovsschema --out ./generated/ovn-nb.types.ts --name OvnNorthboundDatabase
npx ovsdb-generate --schema ./ovn-sb.ovsschema --out ./generated/ovn-sb.types.ts --name OvnSouthboundDatabase

examples/ovn/ has a focused, runnable set covering the pieces above, with pre-generated NB/SB types checked in under examples/ovn/types/:

  • create-logical-network.ts — create a Logical_Switch + Logical_Switch_Port in OVN_Northbound
  • bind-vm-interface.ts — attach a VM's tap device to the local integration bridge and bind it to that logical port via external_ids:iface-id
  • watch-port-binding.ts — monitor OVN_Southbound.Port_Binding to detect when a logical port actually becomes bound to a chassis
  • logical-router-and-security.ts — a logical router with a default route and SNAT, plus an ACL attached to a switch

Reconnecting

By default, a dropped connection stays dropped. Pass reconnect: true to have the client automatically re-establish the connection with exponential backoff after an unexpected disconnect (a transport error or a socket close that you did not request via close()):

const client = new OVSDBClient({
  host: "127.0.0.1",
  port: 6640,
  reconnect: true,
  reconnectDelay: 1000,    // first retry after 1s
  reconnectMaxDelay: 30000 // capped at 30s, doubling each attempt
});

client.on("reconnecting", (attempt, delayMs) => {
  console.log(`reconnect attempt ${attempt} in ${delayMs}ms`);
});

client.on("connect", async () => {
  // fires on the first connect and again after every successful reconnect
  await client.monitorCond("Open_vSwitch", "bridges", {
    Bridge: {columns: ["name", "ports"]}
  });
});

await client.connect();

Things to keep in mind:

  • reconnect only applies after a connection has succeeded at least once. The initial connect() call always resolves or rejects exactly once and is never retried automatically.
  • Calling close() cancels any pending reconnect attempt; call connect() again to resume.
  • Reconnecting opens a new transport, so in-flight requests are rejected (as always) and any active monitors are gone. Re-subscribe from the connect listener, which fires on every successful (re)connect.

Resource Management

The client implements AsyncDisposable, so it also works with await using in runtimes that support explicit resource management.

await using client = new OVSDBClient();
await client.connect();
const dbs = await client.listDbs();

Error Handling

  • Transport/request failures reject with Error
  • OVSDB JSON-RPC errors reject with OvsdbRpcError
  • malformed inbound frames emit protocolError
  • socket-level failures emit transportError

Documentation

Generate API docs with Typedoc:

npm run docs:build

The public API is documented with TSDoc so the generated output is usable as a reference, not just a symbol dump.

Roadmap

Planned work for the next iterations of the library:

  • relation-aware schema generation so UUID reference columns can emit stronger types such as PortRef or InterfaceRef instead of plain Uuid
  • richer codegen metadata for table relationships derived from refTable and refType
  • helper utilities for working with generated reference types in transactions and monitor snapshots
  • live TLS integration coverage for the transport and generator CLI
  • stricter runtime validation for inbound notifications and response payloads

The intended direction is to make the generator more relation-aware first, before attempting a larger ORM-style layer.

Development

npm test
npm run build
npm run docs:build

License

Apache-2.0. See LICENSE.

About

Open vSwitch Database Management Protocol typescript client

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages