UNPKG

@sourceregistry/node-ovsdb

Version:
743 lines (742 loc) 23 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __exportStar = (this && this.__exportStar) || function(m, exports) { for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.OVSDBClient = exports.OvsdbTransaction = exports.OvsdbTransactionError = exports.OvsdbProtocolError = exports.OvsdbRpcError = void 0; exports.resolveConnectionOptions = resolveConnectionOptions; const node_fs_1 = require("node:fs"); const node_net_1 = require("node:net"); const node_events_1 = require("node:events"); const node_tls_1 = require("node:tls"); /** * JSON-RPC transport error returned by the OVSDB server. */ class OvsdbRpcError extends Error { /** * The error payload returned by the server. */ response; /** * Creates a new RPC error wrapper. */ constructor(response) { super(response.details ? `${response.error}: ${response.details}` : response.error); this.name = "OvsdbRpcError"; this.response = response; } } exports.OvsdbRpcError = OvsdbRpcError; /** * Raised when a message does not match the expected JSON-RPC envelope. */ class OvsdbProtocolError extends Error { /** * The raw payload that failed validation. */ payload; /** * Creates a new protocol error wrapper. */ constructor(message, payload) { super(message); this.name = "OvsdbProtocolError"; this.payload = payload; } } exports.OvsdbProtocolError = OvsdbProtocolError; /** * Raised when an OVSDB transaction response contains an operation-level error. */ class OvsdbTransactionError extends Error { /** * Zero-based index of the failed operation in the submitted transaction. */ operationIndex; /** * Operation that produced the error. */ operation; /** * OVSDB error payload returned for the failed operation. */ result; /** * Raw transaction results returned by the server. */ results; /** * Creates a new transaction error wrapper. */ constructor(options) { super(`Transaction operation ${options.operationIndex} failed: ${options.result.error}`); this.name = "OvsdbTransactionError"; this.operationIndex = options.operationIndex; this.operation = options.operation; this.result = options.result; this.results = options.results; } } exports.OvsdbTransactionError = OvsdbTransactionError; /** * Stages OVSDB operations before sending them as a single `transact` request. */ class OvsdbTransaction { stagedOperations = []; /** * Returns the currently staged operations. */ get operations() { return this.stagedOperations; } /** * Adds an operation to the transaction. */ add(operation) { this.stagedOperations.push(operation); return operation; } /** * Stages an insert operation. */ insert(operation) { return this.add(operation); } /** * Stages a select operation. */ select(operation) { return this.add(operation); } /** * Stages an update operation. */ update(operation) { return this.add(operation); } /** * Stages a mutate operation. */ mutate(operation) { return this.add(operation); } /** * Stages a delete operation. */ delete(operation) { return this.add(operation); } /** * Stages a wait operation. */ wait(operation) { return this.add(operation); } /** * Stages a comment operation. */ comment(comment) { return this.add({ op: "comment", comment }); } /** * Stages an assert operation. */ assert(lock) { return this.add({ op: "assert", lock }); } /** * Stages a commit operation. */ commit(durable = false) { return this.add({ op: "commit", durable }); } /** * Stages an abort operation. */ abort() { return this.add({ op: "abort" }); } } exports.OvsdbTransaction = OvsdbTransaction; /** * A low-level, event-driven OVSDB client for Unix sockets, TCP, or TLS. * * The client exposes the RFC 7047 primitives together with common * Open vSwitch protocol extensions while keeping the API small and predictable. */ class OVSDBClient extends node_events_1.EventEmitter { timeout; connectionOptions; connectionFactory; socket = null; requestId = 1; receiveBuffer = ""; pendingRequests = new Map(); connected = false; closeEmitted = false; /** * Creates a new OVSDB client instance. */ constructor(options = {}) { super(); this.timeout = options.timeout ?? 5000; this.connectionOptions = resolveConnectionOptions(options); this.connectionFactory = options.connectionFactory; } /** * Returns `true` when the underlying socket is currently connected. */ get isConnected() { return this.connected; } /** * Opens the transport connection. * * @returns The connected client instance for chaining. */ async connect() { if (this.connected) { return this; } if (!this.connectionFactory && this.connectionOptions.transport === "unix" && !(0, node_fs_1.existsSync)(this.connectionOptions.socketPath)) { throw new Error(`OVSDB socket not found: ${this.connectionOptions.socketPath}`); } const socket = this.connectionFactory ? this.connectionFactory(this.connectionOptions) : createTransport(this.connectionOptions); this.attachSocket(socket); const connectEvent = this.connectionOptions.transport === "tls" ? "secureConnect" : "connect"; await new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { cleanup(); this.disposeTransport(new Error(`Connection timeout after ${this.timeout}ms`)); reject(new Error(`Connection timeout after ${this.timeout}ms`)); }, this.timeout); const onConnect = () => { cleanup(); this.connected = true; this.closeEmitted = false; this.emit("connect"); resolve(); }; const onError = (error) => { cleanup(); reject(error); }; const cleanup = () => { clearTimeout(timeoutId); socket.off(connectEvent, onConnect); socket.off("error", onError); }; socket.once(connectEvent, onConnect); socket.once("error", onError); }); return this; } /** * Sends a raw JSON-RPC request and resolves with its `result` payload. * * @param method RPC method name. * @param params RPC parameters. */ async request(method, params = []) { this.assertConnected(); const id = this.requestId++; const payload = { method, params, id }; return await new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { this.pendingRequests.delete(id); reject(new Error(`Request timeout for method: ${method}`)); }, this.timeout); this.pendingRequests.set(id, { resolve: (value) => resolve(value), reject, timeoutId }); this.writeMessage(payload).catch((error) => { clearTimeout(timeoutId); this.pendingRequests.delete(id); reject(error); }); }); } /** * Sends a JSON-RPC notification without waiting for a response. * * @param method RPC method name. * @param params RPC parameters. */ async notify(method, params = []) { this.assertConnected(); await this.writeMessage({ method, params }); } /** * Returns the database names exposed by the connected OVSDB server. */ async listDbs() { return await this.request("list_dbs", []); } /** * Returns the schema definition for a database. * * @param dbName Database name. */ async getSchema(dbName = "Open_vSwitch") { return await this.request("get_schema", [dbName]); } /** * Executes a single transaction. * * The response preserves operation ordering, so tuple inputs infer tuple outputs. * * @param dbName Database name. * @param operations Transaction operations. */ async transact(dbName, operations) { return await this.request("transact", [dbName, ...operations]); } /** * Stages a transaction in a callback and submits it only if the callback * completes successfully. * * This helper is convenient when you want a scoped, imperative API while * still sending exactly one OVSDB `transact` request. * * @param dbName Database name. * @param callback Callback that stages operations on the transaction object. * @param options Auto-commit behavior for the staged transaction. */ async transaction(dbName, callback, options = {}) { const transaction = new OvsdbTransaction(); const value = await callback(transaction); const operations = [...transaction.operations]; const autoCommit = options.autoCommit ?? true; const hasFinalizer = operations.some((operation) => operation.op === "commit" || operation.op === "abort"); if (autoCommit && !hasFinalizer) { operations.push({ op: "commit", durable: options.durable ?? false }); } if (operations.length === 0) { return { value, operations, results: [] }; } const results = await this.request("transact", [dbName, ...operations]); for (const [index, result] of results.entries()) { if (isOvsdbError(result)) { throw new OvsdbTransactionError({ operationIndex: index, operation: operations[index], result, results }); } } return { value, operations, results: results }; } /** * Cancels a previously issued request by id. * * @param requestId JSON-RPC request id to cancel. */ async cancel(requestId) { return await this.request("cancel", [requestId]); } /** * Starts a standard RFC 7047 monitor. * * @param dbName Database name. * @param monitorId Application-defined monitor id. * @param monitorRequests Per-table monitor definitions. */ async monitor(dbName, monitorId, monitorRequests) { return await this.request("monitor", [ dbName, monitorId, monitorRequests ]); } /** * Starts an Open vSwitch conditional monitor. * * @param dbName Database name. * @param monitorId Application-defined monitor id. * @param monitorRequests Per-table conditional monitor definitions. */ async monitorCond(dbName, monitorId, monitorRequests) { return await this.request("monitor_cond", [ dbName, monitorId, monitorRequests ]); } /** * Starts an Open vSwitch conditional monitor from a known transaction id. * * @param dbName Database name. * @param monitorId Application-defined monitor id. * @param monitorRequests Per-table conditional monitor definitions. * @param lastTransactionId Last seen transaction id, or `null` for a fresh snapshot. */ async monitorCondSince(dbName, monitorId, monitorRequests, lastTransactionId = null) { return await this.request("monitor_cond_since", [ dbName, monitorId, monitorRequests, lastTransactionId ]); } /** * Cancels a monitor by its monitor id. * * @param monitorId Monitor id used when the monitor was created. */ async monitorCancel(monitorId) { return await this.request("monitor_cancel", [monitorId]); } /** * Acquires a named database lock. * * @param lockId Lock identifier. */ async lock(lockId) { return await this.request("lock", [lockId]); } /** * Forces ownership of a named database lock. * * @param lockId Lock identifier. */ async steal(lockId) { return await this.request("steal", [lockId]); } /** * Releases a previously acquired named database lock. * * @param lockId Lock identifier. */ async unlock(lockId) { return await this.request("unlock", [lockId]); } /** * Sends an echo request to validate transport liveness. * * @param payload Values to be echoed back by the server. */ async echo(...payload) { return await this.request("echo", payload); } /** * Enables or disables Open vSwitch database change awareness. * * @param enabled Whether the server should report change awareness metadata. */ async setDbChangeAware(enabled = true) { return await this.request("set_db_change_aware", [enabled]); } /** * Closes the connection and rejects all pending requests. */ async close() { this.disposeTransport(); } /** * Implements `AsyncDisposable`. */ async [Symbol.asyncDispose]() { await this.close(); } attachSocket(socket) { this.socket = socket; this.receiveBuffer = ""; socket.on("data", this.handleData); socket.on("error", this.handleSocketError); socket.on("close", this.handleSocketClose); } handleData = (chunk) => { this.receiveBuffer += typeof chunk === "string" ? chunk : chunk.toString("utf8"); let frame = extractJsonFrame(this.receiveBuffer); while (frame) { this.receiveBuffer = frame.rest; this.parseFrame(frame.frame); frame = extractJsonFrame(this.receiveBuffer); } const trimmed = this.receiveBuffer.trimStart(); if (trimmed && trimmed[0] !== "{" && trimmed[0] !== "[") { this.emitProtocolError("Received non-JSON data on the transport", this.receiveBuffer); this.receiveBuffer = ""; } }; handleSocketError = (error) => { this.emit("transportError", error); this.disposeTransport(error); }; handleSocketClose = () => { this.disposeTransport(); }; parseFrame(frame) { try { const payload = JSON.parse(frame); this.handleMessage(payload); } catch (error) { const protocolError = new OvsdbProtocolError("Failed to parse JSON message", frame); this.emit("protocolError", protocolError, frame); if (error instanceof Error) { void error; } } } handleMessage(payload) { if (!payload || typeof payload !== "object") { this.emitProtocolError("Expected a JSON object message", payload); return; } if ("method" in payload && typeof payload.method === "string") { const message = payload; if (message.id !== undefined && message.id !== null) { void this.handleIncomingRequest(message.method, message.params ?? [], message.id); return; } this.handleNotification(payload); return; } if ("id" in payload) { this.handleResponse(payload); return; } this.emitProtocolError("Received message without method or id", payload); } handleResponse(response) { const pendingRequest = this.pendingRequests.get(response.id); if (!pendingRequest) { this.emitProtocolError("Received response for an unknown request id", response); return; } this.pendingRequests.delete(response.id); clearTimeout(pendingRequest.timeoutId); if (response.error) { pendingRequest.reject(new OvsdbRpcError(response.error)); return; } pendingRequest.resolve(response.result); } async handleIncomingRequest(method, params, id) { if (method === "echo") { await this.writeMessage({ id, result: params, error: null }); return; } await this.writeMessage({ id, result: null, error: { error: "not supported", details: `Unsupported server request: ${method}` } }); } handleNotification(notification) { this.emit("notification", notification); switch (notification.method) { case "update": this.emit("update", notification); break; case "update2": this.emit("update2", notification); break; case "update3": this.emit("update3", notification); break; case "locked": this.emit("locked", notification); break; case "stolen": this.emit("stolen", notification); break; default: this.emitProtocolError("Received an unknown notification method", notification); break; } } emitProtocolError(message, payload) { const error = new OvsdbProtocolError(message, payload); this.emit("protocolError", error, payload); } async writeMessage(payload) { this.assertConnected(); await new Promise((resolve, reject) => { this.socket?.write(`${JSON.stringify(payload)}\n`, (error) => { if (error) { reject(error); return; } resolve(); }); }); } assertConnected() { if (!this.connected || !this.socket) { throw new Error("Not connected to OVSDB"); } } disposeTransport(reason) { const socket = this.socket; this.socket = null; this.connected = false; this.receiveBuffer = ""; if (socket) { socket.off("data", this.handleData); socket.off("error", this.handleSocketError); socket.off("close", this.handleSocketClose); if (!socket.destroyed) { socket.destroy(); } } const closeError = reason ?? new Error("Connection closed"); for (const pendingRequest of this.pendingRequests.values()) { clearTimeout(pendingRequest.timeoutId); pendingRequest.reject(closeError); } this.pendingRequests.clear(); if (!this.closeEmitted) { this.closeEmitted = true; this.emit("close"); } } } exports.OVSDBClient = OVSDBClient; __exportStar(require("./types"), exports); /** * Resolves the user-supplied transport options into an explicit connection mode. */ function resolveConnectionOptions(options = {}) { if (options.host) { const port = options.port ?? 6640; if (options.tls) { return { transport: "tls", host: options.host, port, tlsOptions: { host: options.host, port, ...options.tlsOptions } }; } return { transport: "tcp", host: options.host, port }; } return { transport: "unix", socketPath: options.socketPath ?? "/var/run/openvswitch/db.sock" }; } function createTransport(options) { switch (options.transport) { case "unix": return (0, node_net_1.createConnection)(options.socketPath); case "tcp": return (0, node_net_1.createConnection)(options.port, options.host); case "tls": return (0, node_tls_1.connect)(options.tlsOptions); } } function isOvsdbError(value) { return typeof value === "object" && value !== null && "error" in value && typeof value.error === "string"; } function extractJsonFrame(buffer) { let startIndex = 0; while (startIndex < buffer.length && /\s/u.test(buffer[startIndex])) { startIndex += 1; } if (startIndex >= buffer.length) { return null; } const opening = buffer[startIndex]; const closing = opening === "{" ? "}" : opening === "[" ? "]" : null; if (!closing) { return null; } let depth = 0; let inString = false; let escaping = false; for (let index = startIndex; index < buffer.length; index += 1) { const char = buffer[index]; if (inString) { if (escaping) { escaping = false; continue; } if (char === "\\") { escaping = true; continue; } if (char === "\"") { inString = false; } continue; } if (char === "\"") { inString = true; continue; } if (char === "{" || char === "[") { depth += 1; continue; } if (char === "}" || char === "]") { depth -= 1; if (depth === 0) { return { frame: buffer.slice(startIndex, index + 1), rest: buffer.slice(index + 1) }; } } } return null; }