Packages

A pure-Elixir implementation of the Open vSwitch Database Management Protocol (OVSDB), per RFC 7047. Provides protocol primitives, operation and transaction builders, schema parsing, client/server session handling over TCP/TLS, and an in-memory IDL replica.

Current section

Files

Jump to
ovsdb_ex lib ovsdb operation.ex
Raw

lib/ovsdb/operation.ex

defmodule OVSDB.Operation do
@moduledoc """
Builders for the ten database operations defined by
[RFC 7047 §5.2][rfc-ops]. Each builder returns a plain
string-keyed Elixir map — the shape that goes into the `"params"`
array of a `transact` request.
[rfc-ops]: https://www.rfc-editor.org/rfc/rfc7047#section-5.2
## Operations
* `insert/3` — §5.2.1
* `select/3` — §5.2.2
* `update/3` — §5.2.3
* `mutate/3` — §5.2.4
* `delete/2` — §5.2.5
* `wait/4` — §5.2.6
* `commit/1` — §5.2.7
* `abort/0` — §5.2.8
* `comment/1` — §5.2.9
* `assert/1` — §5.2.10
## Value encoding
Every value in a row or mutation is walked through
`OVSDB.Value.encode/1` so that wrapped `OVSDB.UUID`, `OVSDB.NamedUUID`,
`OVSDB.Set`, and `OVSDB.Map` structs produce their correct wire
forms. Atomic values pass through unchanged.
## Row inputs
Where an operation accepts a row (`insert`, `update`, `wait`), both
of these are accepted:
* An `OVSDB.Row.t()` struct (its `columns` field is used; `_uuid`
and `_version` metadata are ignored — clients don't write those).
* A plain `%{column_name => value}` map.
Column values may be any `OVSDB.Value.value()` (atomic, Set, Map,
UUID, NamedUUID) and are encoded to wire form.
## No schema validation
These builders produce well-formed wire shapes but do not validate
column names, types, or constraints against a schema — that's the
job of `OVSDB.Schema.validate_row/3`. Wrong column types will produce
a server-side error at transaction time.
"""
alias OVSDB.{Condition, Row, Value}
@type table :: String.t()
@type column :: String.t()
@typedoc """
A row as passed to an operation builder. Either a `Row` struct
(metadata stripped) or a raw column map. Values are Elixir-native
and will be encoded via `OVSDB.Value.encode/1`.
"""
@type row_input :: Row.t() | %{optional(column()) => Value.value()}
@typedoc """
The wire-form representation of an operation — a JSON object with
`"op"` plus operation-specific fields.
"""
@type t :: %{required(String.t()) => term()}
# ---------------------------------------------------------------------------
# §5.2.1 — insert
# ---------------------------------------------------------------------------
@doc """
Builds an `insert` operation.
A `uuid_name:` option gives the inserted row a symbolic name that
other operations in the same transaction can reference via
`OVSDB.NamedUUID` — see RFC 7047 §5.1.
iex> OVSDB.Operation.insert("Bridge", %{"name" => "br-lan"})
%{"op" => "insert", "table" => "Bridge", "row" => %{"name" => "br-lan"}}
iex> OVSDB.Operation.insert("Bridge", %{"name" => "br-lan"}, uuid_name: "new_br")
%{
"op" => "insert",
"table" => "Bridge",
"row" => %{"name" => "br-lan"},
"uuid-name" => "new_br"
}
"""
@spec insert(table(), row_input(), keyword()) :: t()
def insert(table, row, opts \\ []) when is_binary(table) do
base = %{
"op" => "insert",
"table" => table,
"row" => encode_row(row)
}
case Keyword.get(opts, :uuid_name) do
nil -> base
name when is_binary(name) -> Elixir.Map.put(base, "uuid-name", name)
end
end
# ---------------------------------------------------------------------------
# §5.2.2 — select
# ---------------------------------------------------------------------------
@doc """
Builds a `select` operation.
The third argument is an optional list of column names to project.
When omitted (or `nil`), the server returns all columns of matching
rows.
iex> OVSDB.Operation.select("Bridge", [OVSDB.Condition.eq("name", "br-lan")])
%{
"op" => "select",
"table" => "Bridge",
"where" => [["name", "==", "br-lan"]]
}
iex> OVSDB.Operation.select("Bridge",
...> [OVSDB.Condition.eq("name", "br-lan")],
...> ["_uuid", "ports"])
%{
"op" => "select",
"table" => "Bridge",
"where" => [["name", "==", "br-lan"]],
"columns" => ["_uuid", "ports"]
}
"""
@spec select(table(), [Condition.t()], [column()] | nil) :: t()
def select(table, where, columns \\ nil) when is_binary(table) and is_list(where) do
base = %{
"op" => "select",
"table" => table,
"where" => Condition.encode_all(where)
}
case columns do
nil -> base
list when is_list(list) -> Elixir.Map.put(base, "columns", list)
end
end
# ---------------------------------------------------------------------------
# §5.2.3 — update
# ---------------------------------------------------------------------------
@doc """
Builds an `update` operation.
iex> OVSDB.Operation.update("AWLAN_Node",
...> [OVSDB.Condition.eq("serial_number", "SIM-DEADBEEF")],
...> %{"manager_addr" => "ssl:mgr.osync.local:443"})
%{
"op" => "update",
"table" => "AWLAN_Node",
"where" => [["serial_number", "==", "SIM-DEADBEEF"]],
"row" => %{"manager_addr" => "ssl:mgr.osync.local:443"}
}
"""
@spec update(table(), [Condition.t()], row_input()) :: t()
def update(table, where, row) when is_binary(table) and is_list(where) do
%{
"op" => "update",
"table" => table,
"where" => Condition.encode_all(where),
"row" => encode_row(row)
}
end
# ---------------------------------------------------------------------------
# §5.2.4 — mutate
# ---------------------------------------------------------------------------
@typedoc """
An atom representing one of the RFC 7047 §5.1 mutator functions:
* Arithmetic (integer/real only): `:add`, `:sub`, `:mul`, `:div`, `:mod`
* Set/map: `:insert`, `:delete`
"""
@type mutator ::
:add | :sub | :mul | :div | :mod | :insert | :delete
@type mutation :: {column(), mutator(), Value.value()}
@mutators %{
add: "+=",
sub: "-=",
mul: "*=",
div: "/=",
mod: "%=",
insert: "insert",
delete: "delete"
}
@doc """
Returns the list of atom mutators supported by `mutate/3`.
iex> OVSDB.Operation.mutators() |> Enum.sort()
[:add, :delete, :div, :insert, :mod, :mul, :sub]
"""
@spec mutators() :: [mutator()]
def mutators, do: Elixir.Map.keys(@mutators)
@doc """
Returns the RFC 7047 wire string for a mutator atom.
iex> OVSDB.Operation.mutator_string(:add)
"+="
iex> OVSDB.Operation.mutator_string(:insert)
"insert"
"""
@spec mutator_string(mutator()) :: String.t()
def mutator_string(mut) when is_atom(mut) do
case Elixir.Map.fetch(@mutators, mut) do
{:ok, s} -> s
:error -> raise ArgumentError, "unknown mutator: #{inspect(mut)}"
end
end
@doc """
Builds a `mutate` operation.
Each mutation is `{column, mutator_atom, value}`. See `mutators/0`
for the supported atoms.
iex> mutations = [
...> {"client_count", :add, 1},
...> {"ports", :insert, OVSDB.Set.new([OVSDB.UUID.new("550e8400-e29b-41d4-a716-446655440000")])}
...> ]
iex> OVSDB.Operation.mutate("Bridge",
...> [OVSDB.Condition.eq("name", "br-lan")],
...> mutations)
%{
"op" => "mutate",
"table" => "Bridge",
"where" => [["name", "==", "br-lan"]],
"mutations" => [
["client_count", "+=", 1],
["ports", "insert", ["uuid", "550e8400-e29b-41d4-a716-446655440000"]]
]
}
"""
@spec mutate(table(), [Condition.t()], [mutation()]) :: t()
def mutate(table, where, mutations)
when is_binary(table) and is_list(where) and is_list(mutations) do
%{
"op" => "mutate",
"table" => table,
"where" => Condition.encode_all(where),
"mutations" => Enum.map(mutations, &encode_mutation/1)
}
end
@spec encode_mutation(mutation()) :: [String.t() | Value.wire(), ...]
defp encode_mutation({column, mut, value}) when is_binary(column) and is_atom(mut) do
[column, mutator_string(mut), Value.encode(value)]
end
# ---------------------------------------------------------------------------
# §5.2.5 — delete
# ---------------------------------------------------------------------------
@doc """
Builds a `delete` operation.
iex> OVSDB.Operation.delete("Bridge",
...> [OVSDB.Condition.eq("name", "br-lan")])
%{
"op" => "delete",
"table" => "Bridge",
"where" => [["name", "==", "br-lan"]]
}
"""
@spec delete(table(), [Condition.t()]) :: t()
def delete(table, where) when is_binary(table) and is_list(where) do
%{
"op" => "delete",
"table" => table,
"where" => Condition.encode_all(where)
}
end
# ---------------------------------------------------------------------------
# §5.2.6 — wait
# ---------------------------------------------------------------------------
@typedoc """
The `until` field of a `wait` operation. `"=="` blocks until all
rows match the given values; `"!="` blocks until none match.
"""
@type wait_until :: :eq | :ne
@doc """
Builds a `wait` operation. Per RFC 7047 §5.2.6, this blocks the
transaction until the rows matching `where` have `columns` equal
(`until: :eq`) or unequal (`until: :ne`) to the given `rows`.
## Options
* `:until` — `:eq` (default) or `:ne`.
* `:timeout` — milliseconds to wait before failing. Default `0`
means the condition is checked immediately and fails if not
met. The RFC permits `0` to mean "fail immediately if not met"
rather than "wait forever" — consistent with its semantics.
## Example
iex> OVSDB.Operation.wait("AWLAN_Node",
...> [OVSDB.Condition.eq("serial_number", "SIM-DEADBEEF")],
...> ["applied_generation"],
...> [%{"applied_generation" => 7}],
...> until: :eq,
...> timeout: 5_000)
%{
"op" => "wait",
"table" => "AWLAN_Node",
"where" => [["serial_number", "==", "SIM-DEADBEEF"]],
"columns" => ["applied_generation"],
"until" => "==",
"rows" => [%{"applied_generation" => 7}],
"timeout" => 5_000
}
"""
@spec wait(table(), [Condition.t()], [column()], [row_input()], keyword()) :: t()
def wait(table, where, columns, rows, opts \\ [])
when is_binary(table) and is_list(where) and is_list(columns) and is_list(rows) do
until =
case Keyword.get(opts, :until, :eq) do
:eq -> "=="
:ne -> "!="
other -> raise ArgumentError, "wait/5 :until must be :eq or :ne, got: #{inspect(other)}"
end
timeout = Keyword.get(opts, :timeout, 0)
unless is_integer(timeout) and timeout >= 0 do
raise ArgumentError, "wait/5 :timeout must be a non-negative integer, got: #{inspect(timeout)}"
end
%{
"op" => "wait",
"table" => table,
"where" => Condition.encode_all(where),
"columns" => columns,
"until" => until,
"rows" => Enum.map(rows, &encode_row/1),
"timeout" => timeout
}
end
# ---------------------------------------------------------------------------
# §5.2.7 — commit
# ---------------------------------------------------------------------------
@doc """
Builds a `commit` operation. When `durable?` is `true`, the server
will not return success until the transaction is written to
non-volatile storage (RFC 7047 §1.2's "Durable").
iex> OVSDB.Operation.commit()
%{"op" => "commit", "durable" => false}
iex> OVSDB.Operation.commit(true)
%{"op" => "commit", "durable" => true}
"""
@spec commit(boolean()) :: t()
def commit(durable? \\ false) when is_boolean(durable?) do
%{"op" => "commit", "durable" => durable?}
end
# ---------------------------------------------------------------------------
# §5.2.8 — abort
# ---------------------------------------------------------------------------
@doc """
Builds an `abort` operation. Causes the transaction to fail
unconditionally — useful for testing rollback behavior.
iex> OVSDB.Operation.abort()
%{"op" => "abort"}
"""
@spec abort() :: t()
def abort, do: %{"op" => "abort"}
# ---------------------------------------------------------------------------
# §5.2.9 — comment
# ---------------------------------------------------------------------------
@doc """
Builds a `comment` operation. Comments appear in the server's
transaction log and are useful for correlating client actions with
server-side audit trails.
iex> OVSDB.Operation.comment("applying manager.addr for node X")
%{"op" => "comment", "comment" => "applying manager.addr for node X"}
"""
@spec comment(String.t()) :: t()
def comment(text) when is_binary(text) do
%{"op" => "comment", "comment" => text}
end
# ---------------------------------------------------------------------------
# §5.2.10 — assert
# ---------------------------------------------------------------------------
@doc """
Builds an `assert` operation. Requires the client to currently hold
the named lock for the transaction to proceed — see RFC 7047 §4.1.8
for lock semantics.
iex> OVSDB.Operation.assert_lock("my_lock")
%{"op" => "assert", "lock" => "my_lock"}
Note: this function is named `assert_lock` rather than `assert` to
avoid shadowing the `assert` macro from `ExUnit.Assertions`, which
callers might inadvertently reach for in test contexts.
"""
@spec assert_lock(String.t()) :: t()
def assert_lock(lock) when is_binary(lock) do
%{"op" => "assert", "lock" => lock}
end
# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------
# Normalize a row input to a wire-ready column map.
defp encode_row(%Row{columns: columns}), do: encode_row(columns)
defp encode_row(map) when is_map(map) do
for {k, v} <- map, into: %{}, do: {k, Value.encode(v)}
end
defp encode_row(other) do
raise ArgumentError,
"row must be an OVSDB.Row or a column map, got: #{inspect(other)}"
end
end