Add support for configuring meters through the Meter and Meter_Band
tables in the Northbound database. This commit also has ovn-northd
sync those tables between the Northbound and Southbound databases.
Add support for configuring meters with ovn-nbctl.
Signed-off-by: Justin Pettit <jpet...@ovn.org>
---
ovn/northd/ovn-northd.c | 145 ++++++++++++++++++++++++++++++++++
ovn/ovn-nb.ovsschema | 33 +++++++-
ovn/ovn-nb.xml | 80 +++++++++++++++++++
ovn/ovn-sb.ovsschema | 27 ++++++-
ovn/ovn-sb.xml | 72 +++++++++++++++++
ovn/utilities/ovn-nbctl.8.xml | 50 ++++++++++++
ovn/utilities/ovn-nbctl.c | 143 +++++++++++++++++++++++++++++++++
tests/ovn-nbctl.at | 58 ++++++++++++++
8 files changed, 604 insertions(+), 4 deletions(-)
diff --git a/ovn/northd/ovn-northd.c b/ovn/northd/ovn-northd.c
index 04a072ba8de7..45557170edc8 100644
--- a/ovn/northd/ovn-northd.c
+++ b/ovn/northd/ovn-northd.c
@@ -6606,6 +6606,140 @@ sync_port_groups(struct northd_context *ctx)
shash_destroy(&sb_port_groups);
}
+struct band_entry {
+ int64_t rate;
+ int64_t burst_size;
+ const char *action;
+};
+
+static int
+band_cmp(const void *band1_, const void *band2_)
+{
+ const struct band_entry *band1p = band1_;
+ const struct band_entry *band2p = band2_;
+
+ if (band1p->rate != band2p->rate) {
+ return band1p->rate > band2p->rate ? -1 : 1;
+ } else if (band1p->burst_size != band2p->burst_size) {
+ return band1p->burst_size > band2p->burst_size ? -1 : 1;
+ } else {
+ return strcmp(band1p->action, band2p->action);
+ }
+}
+
+static bool
+bands_need_update(const struct nbrec_meter *nb_meter,
+ const struct sbrec_meter *sb_meter)
+{
+ if (nb_meter->n_bands != sb_meter->n_bands) {
+ return true;
+ }
+
+ /* A single band is the most common scenario, so speed up that
+ * check. */
+ if (nb_meter->n_bands == 1) {
+ struct nbrec_meter_band *nb_band = nb_meter->bands[0];
+ struct sbrec_meter_band *sb_band = sb_meter->bands[0];
+
+ return !(nb_band->rate == sb_band->rate
+ && nb_band->burst_size == sb_band->burst_size
+ && !strcmp(sb_band->action, nb_band->action));
+ }
+
+ /* Place the Northbound entries in sorted order. */
+ struct band_entry *nb_bands;
+ nb_bands = xmalloc(sizeof *nb_bands * nb_meter->n_bands);
+ for (size_t i = 0; i < nb_meter->n_bands; i++) {
+ struct nbrec_meter_band *nb_band = nb_meter->bands[i];
+
+ nb_bands[i].rate = nb_band->rate;
+ nb_bands[i].burst_size = nb_band->burst_size;
+ nb_bands[i].action = nb_band->action;
+ }
+ qsort(nb_bands, nb_meter->n_bands, sizeof *nb_bands, band_cmp);
+
+ /* Place the Southbound entries in sorted order. */
+ struct band_entry *sb_bands;
+ sb_bands = xmalloc(sizeof *sb_bands * sb_meter->n_bands);
+ for (size_t i = 0; i < sb_meter->n_bands; i++) {
+ struct sbrec_meter_band *sb_band = sb_meter->bands[i];
+
+ sb_bands[i].rate = sb_band->rate;
+ sb_bands[i].burst_size = sb_band->burst_size;
+ sb_bands[i].action = sb_band->action;
+ }
+ qsort(sb_bands, sb_meter->n_bands, sizeof *sb_bands, band_cmp);
+
+ bool need_update = false;
+ for (size_t i = 0; i < nb_meter->n_bands; i++) {
+ if (nb_bands[i].rate != sb_bands[i].rate
+ || nb_bands[i].burst_size != sb_bands[i].burst_size
+ || strcmp(nb_bands[i].action, nb_bands[i].action)) {
+ need_update = true;
+ goto done;
+ }
+ }
+
+done:
+ free(nb_bands);
+ free(sb_bands);
+
+ return need_update;
+}
+
+/* Each entry in the Meter and Meter_Band tables in OVN_Northbound have
+ * a corresponding entries in the Meter and Meter_Band tables in
+ * OVN_Southbound.
+ */
+static void
+sync_meters(struct northd_context *ctx)
+{
+ struct shash sb_meters = SHASH_INITIALIZER(&sb_meters);
+
+ const struct sbrec_meter *sb_meter;
+ SBREC_METER_FOR_EACH (sb_meter, ctx->ovnsb_idl) {
+ shash_add(&sb_meters, sb_meter->name, sb_meter);
+ }
+
+ const struct nbrec_meter *nb_meter;
+ NBREC_METER_FOR_EACH (nb_meter, ctx->ovnnb_idl) {
+ bool new_sb_meter = false;
+
+ sb_meter = shash_find_and_delete(&sb_meters, nb_meter->name);
+ if (!sb_meter) {
+ sb_meter = sbrec_meter_insert(ctx->ovnsb_txn);
+ sbrec_meter_set_name(sb_meter, nb_meter->name);
+ new_sb_meter = true;
+ }
+
+ if (new_sb_meter || bands_need_update(nb_meter, sb_meter)) {
+ struct sbrec_meter_band **sb_bands;
+ sb_bands = xcalloc(nb_meter->n_bands, sizeof *sb_bands);
+ for (size_t i = 0; i < nb_meter->n_bands; i++) {
+ const struct nbrec_meter_band *nb_band = nb_meter->bands[i];
+
+ sb_bands[i] = sbrec_meter_band_insert(ctx->ovnsb_txn);
+
+ sbrec_meter_band_set_action(sb_bands[i], nb_band->action);
+ sbrec_meter_band_set_rate(sb_bands[i], nb_band->rate);
+ sbrec_meter_band_set_burst_size(sb_bands[i],
+ nb_band->burst_size);
+ }
+ sbrec_meter_set_bands(sb_meter, sb_bands, nb_meter->n_bands);
+ free(sb_bands);
+ }
+
+ sbrec_meter_set_unit(sb_meter, nb_meter->unit);
+ }
+
+ struct shash_node *node, *next;
+ SHASH_FOR_EACH_SAFE (node, next, &sb_meters) {
+ sbrec_meter_delete(node->data);
+ shash_delete(&sb_meters, node);
+ }
+ shash_destroy(&sb_meters);
+}
+
/*
* struct 'dns_info' is used to sync the DNS records between OVN Northbound db
* and Southbound db.
@@ -6726,6 +6860,7 @@ ovnnb_db_run(struct northd_context *ctx,
sync_address_sets(ctx);
sync_port_groups(ctx);
+ sync_meters(ctx);
sync_dns_entries(ctx, &datapaths);
struct ovn_port_group *pg, *next_pg;
@@ -7351,6 +7486,16 @@ main(int argc, char *argv[])
&sbrec_rbac_permission_col_insert_delete);
add_column_noalert(ovnsb_idl_loop.idl, &sbrec_rbac_permission_col_update);
+ ovsdb_idl_add_table(ovnsb_idl_loop.idl, &sbrec_table_meter);
+ ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_meter_col_name);
+ ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_meter_col_unit);
+ ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_meter_col_bands);
+
+ ovsdb_idl_add_table(ovnsb_idl_loop.idl, &sbrec_table_meter_band);
+ ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_meter_band_col_action);
+ ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_meter_band_col_rate);
+ ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_meter_band_col_burst_size);
+
ovsdb_idl_add_table(ovnsb_idl_loop.idl, &sbrec_table_chassis);
ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_chassis_col_nb_cfg);
ovsdb_idl_add_column(ovnsb_idl_loop.idl, &sbrec_chassis_col_name);
diff --git a/ovn/ovn-nb.ovsschema b/ovn/ovn-nb.ovsschema
index 8e6ddec4662f..9a0d8ec70514 100644
--- a/ovn/ovn-nb.ovsschema
+++ b/ovn/ovn-nb.ovsschema
@@ -1,7 +1,7 @@
{
"name": "OVN_Northbound",
- "version": "5.11.0",
- "cksum": "1149260021 18713",
+ "version": "5.12.0",
+ "cksum": "2812995200 20238",
"tables": {
"NB_Global": {
"columns": {
@@ -195,6 +195,35 @@
"type": {"key": "string", "value": "string",
"min": 0, "max": "unlimited"}}},
"isRoot": false},
+ "Meter": {
+ "columns": {
+ "name": {"type": "string"},
+ "unit": {"type": {"key": {"type": "string",
+ "enum": ["set", ["kbps",
"pktps"]]}}},
+ "bands": {"type": {"key": {"type": "uuid",
+ "refTable": "Meter_Band",
+ "refType": "strong"},
+ "min": 1,
+ "max": "unlimited"}},
+ "external_ids": {
+ "type": {"key": "string", "value": "string",
+ "min": 0, "max": "unlimited"}}},
+ "indexes": [["name"]],
+ "isRoot": true},
+ "Meter_Band": {
+ "columns": {
+ "action": {"type": {"key": {"type": "string",
+ "enum": ["set", ["drop"]]}}},
+ "rate": {"type": {"key": {"type": "integer",
+ "minInteger": 1,
+ "maxInteger": 4294967295}}},
+ "burst_size": {"type": {"key": {"type": "integer",
+ "minInteger": 0,
+ "maxInteger": 4294967295}}},
+ "external_ids": {
+ "type": {"key": "string", "value": "string",
+ "min": 0, "max": "unlimited"}}},
+ "isRoot": false},
"Logical_Router": {
"columns": {
"name": {"type": "string"},
diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
index e4e72b27cf36..1feb2af52027 100644
--- a/ovn/ovn-nb.xml
+++ b/ovn/ovn-nb.xml
@@ -1356,6 +1356,86 @@
</column>
</table>
+ <table name="Meter" title="Meter table">
+ <p>
+ Each row in this table represents a meter that can be used for QoS or
+ rate-limiting.
+ </p>
+
+ <column name="name">
+ <p>
+ A name for this meter.
+ </p>
+
+ <p>
+ Names that begin with "__" are reserved for OVN internal use and should
+ not be added manually.
+ </p>
+ </column>
+
+ <column name="unit">
+ <p>
+ The unit for <ref column="rate" table="Meter_Band"/> and
+ <ref column="burst_rate" table="Meter_Band"/> parameters in
+ the <ref column="bands"/> entry. <code>kbps</code> specifies
+ kilobits per second, and <code>pktps</code> specifies packets
+ per second.
+ </p>
+ </column>
+
+ <column name="bands">
+ <p>
+ The bands associated with this meter. Each band specifies a
+ rate above which the band is to take the action
+ <code>action</code>. If multiple bands' rates are exceeded,
+ then the band with the highest rate among the exceeded bands is
+ selected.
+ </p>
+ </column>
+
+ <column name="external_ids">
+ See <em>External IDs</em> at the beginning of this document.
+ </column>
+ </table>
+
+ <table name="Meter_Band" title="Meter_Band table">
+ <p>
+ Each row in this table represents a meter band which specifies the
+ rate above which the configured action should be applied. These bands
+ are referenced by the <ref column="bands" table="Meter"/> column in
+ the <ref table="Meter"/> table.
+ </p>
+
+ <column name="action">
+ <p>
+ The action to execute when this band matches. The only supported
+ action is <code>drop</code>.
+ </p>
+ </column>
+
+ <column name="rate">
+ <p>
+ The relative rate limit for this band, in kilobits per second or
+ bits per second, depending on whether the parent <ref table="Meter"/>
+ entry's <ref column="unit" table="Meter"/> column specified
+ <code>kbps</code> or <code>pktps</code>.
+ </p>
+ </column>
+
+ <column name="burst_size">
+ <p>
+ The maximum burst allowed for the band in kilobits or packets,
+ depending on whether <code>kbps</code> or <code>pktps</code> was
+ selected in the parent <ref table="Meter"/> entry's
+ <ref column="unit" table="Meter"/> column.
+ </p>
+ </column>
+
+ <column name="external_ids">
+ See <em>External IDs</em> at the beginning of this document.
+ </column>
+ </table>
+
<table name="Logical_Router_Port" title="L3 logical router port">
<p>
A port within an L3 logical router.
diff --git a/ovn/ovn-sb.ovsschema b/ovn/ovn-sb.ovsschema
index 9e271d433246..ad6ad3b71da0 100644
--- a/ovn/ovn-sb.ovsschema
+++ b/ovn/ovn-sb.ovsschema
@@ -1,7 +1,7 @@
{
"name": "OVN_Southbound",
- "version": "1.15.0",
- "cksum": "1839738004 13639",
+ "version": "1.16.0",
+ "cksum": "3046632234 14844",
"tables": {
"SB_Global": {
"columns": {
@@ -98,6 +98,29 @@
"indexes": [["datapath", "tunnel_key"],
["datapath", "name"]],
"isRoot": true},
+ "Meter": {
+ "columns": {
+ "name": {"type": "string"},
+ "unit": {"type": {"key": {"type": "string",
+ "enum": ["set", ["kbps",
"pktps"]]}}},
+ "bands": {"type": {"key": {"type": "uuid",
+ "refTable": "Meter_Band",
+ "refType": "strong"},
+ "min": 1,
+ "max": "unlimited"}}},
+ "indexes": [["name"]],
+ "isRoot": true},
+ "Meter_Band": {
+ "columns": {
+ "action": {"type": {"key": {"type": "string",
+ "enum": ["set", ["drop"]]}}},
+ "rate": {"type": {"key": {"type": "integer",
+ "minInteger": 1,
+ "maxInteger": 4294967295}}},
+ "burst_size": {"type": {"key": {"type": "integer",
+ "minInteger": 0,
+ "maxInteger": 4294967295}}}},
+ "isRoot": false},
"Datapath_Binding": {
"columns": {
"tunnel_key": {
diff --git a/ovn/ovn-sb.xml b/ovn/ovn-sb.xml
index f9724d398ce6..57d8a9e042a5 100644
--- a/ovn/ovn-sb.xml
+++ b/ovn/ovn-sb.xml
@@ -1923,6 +1923,78 @@ tcp.flags = RST;
</column>
</table>
+ <table name="Meter" title="Meter table">
+ <p>
+ Each row in this table represents a meter that can be used for QoS or
+ rate-limiting.
+ </p>
+
+ <column name="name">
+ <p>
+ A name for this meter.
+ </p>
+
+ <p>
+ Names that begin with "__" are reserved for OVN internal use and should
+ not be added manually.
+ </p>
+ </column>
+
+ <column name="unit">
+ <p>
+ The unit for <ref column="rate" table="Meter_Band"/> and
+ <ref column="burst_rate" table="Meter_Band"/> parameters in
+ the <ref column="bands"/> entry. <code>kbps</code> specifies
+ kilobits per second, and <code>pktps</code> specifies packets
+ per second.
+ </p>
+ </column>
+
+ <column name="bands">
+ <p>
+ The bands associated with this meter. Each band specifies a
+ rate above which the band is to take the action
+ <code>action</code>. If multiple bands' rates are exceeded,
+ then the band with the highest rate among the exceeded bands is
+ selected.
+ </p>
+ </column>
+ </table>
+
+ <table name="Meter_Band" title="Meter_Band table">
+ <p>
+ Each row in this table represents a meter band which specifies the
+ rate above which the configured action should be applied. These bands
+ are referenced by the <ref column="bands" table="Meter"/> column in
+ the <ref table="Meter"/> table.
+ </p>
+
+ <column name="action">
+ <p>
+ The action to execute when this band matches. The only supported
+ action is <code>drop</code>.
+ </p>
+ </column>
+
+ <column name="rate">
+ <p>
+ The relative rate limit for this band, in kilobits per second or
+ bits per second, depending on whether the parent <ref table="Meter"/>
+ entry's <ref column="unit" table="Meter"/> column specified
+ <code>kbps</code> or <code>pktps</code>.
+ </p>
+ </column>
+
+ <column name="burst_size">
+ <p>
+ The maximum burst allowed for the band in kilobits or packets,
+ depending on whether <code>kbps</code> or <code>pktps</code> was
+ selected in the parent <ref table="Meter"/> entry's
+ <ref column="unit" table="Meter"/> column.
+ </p>
+ </column>
+ </table>
+
<table name="Datapath_Binding" title="Physical-Logical Datapath Bindings">
<p>
Each row in this table represents a logical datapath, which implements a
diff --git a/ovn/utilities/ovn-nbctl.8.xml b/ovn/utilities/ovn-nbctl.8.xml
index 2cd2fab304cd..a8ea7d8cb1e1 100644
--- a/ovn/utilities/ovn-nbctl.8.xml
+++ b/ovn/utilities/ovn-nbctl.8.xml
@@ -172,6 +172,56 @@
</dd>
</dl>
+ <h1>Meter Commands</h1>
+ <dl>
+ <dt><code>meter-add</code> <var>name</var> <var>unit</var> <var>action</var>
<var>rate</var> [<var>burst_size</var>]</dt>
+ <dd>
+ <p>
+ Adds the specified meter. <var>name</var> must be a unique
+ name to identify this meter. The <var>action</var> argument
+ specifies what should happen when this meter is exceeded.
+ The only supported action is <code>drop</code>.
+ </p>
+
+ <p>
+ The <var>unit</var> specifies the unit for the <var>rate</var>
+ argument; valid values are <code>kbps</code> and
+ <code>pktps</code> for kilobits per second and packets per
+ second, respectively. The <var>burst_rate</var> option
+ configures the maximum burst allowed for the band in kilobits
+ or packets depending on whether the <var>unit</var> chosen was
+ <code>kbps</code> or <code>pktps</code>, respectively.
+ </p>
+
+ <p>
+ <code>ovn-nbctl</code> only supports adding a meter with a
+ single band, but the other commands support meters with
+ multiple bands.
+ </p>
+
+ <p>
+ Names that start with "__" are reserved for internal use by OVN,
+ so <code>ovn-nbctl</code> does not allow adding them.
+ </p>
+ </dd>
+
+ <dt><code>meter-del</code> [<var>name</var>]</dt>
+ <dd>
+ <p>
+ Deletes meters. By default, all meters are deleted. If
+ <var>name</var> is supplied, only the meter with that name
+ will be deleted.
+ </p>
+ </dd>
+
+ <dt><code>meter-list</code></dt>
+ <dd>
+ <p>
+ Lists all meters.
+ </p>
+ </dd>
+ </dl>
+
<h1>Logical Switch Port Commands</h1>
<dl>
<dt>[<code>--may-exist</code>] <code>lsp-add</code> <var>switch</var>
<var>port</var></dt>
diff --git a/ovn/utilities/ovn-nbctl.c b/ovn/utilities/ovn-nbctl.c
index 3c3e582cb906..9f0e6347c104 100644
--- a/ovn/utilities/ovn-nbctl.c
+++ b/ovn/utilities/ovn-nbctl.c
@@ -496,6 +496,12 @@ QoS commands:\n\
remove QoS rules from SWITCH\n\
qos-list SWITCH print QoS rules for SWITCH\n\
\n\
+Meter commands:\n\
+ meter-add NAME UNIT ACTION RATE [BURST_SIZE]\n\
+ add a meter\n\
+ meter-del [NAME] remove meters\n\
+ meter-list print meters\n\
+\n\
Logical switch port commands:\n\
lsp-add SWITCH PORT add logical port PORT on SWITCH\n\
lsp-add SWITCH PORT PARENT TAG\n\
@@ -2290,6 +2296,137 @@ nbctl_qos_del(struct ctl_context *ctx)
}
}
+static int
+meter_cmp(const void *meter1_, const void *meter2_)
+{
+ struct nbrec_meter *const *meter1p = meter1_;
+ struct nbrec_meter *const *meter2p = meter2_;
+ const struct nbrec_meter *meter1 = *meter1p;
+ const struct nbrec_meter *meter2 = *meter2p;
+
+ return strcmp(meter1->name, meter2->name);
+}
+
+static void
+nbctl_meter_list(struct ctl_context *ctx)
+{
+ const struct nbrec_meter **meters = NULL;
+ const struct nbrec_meter *meter;
+ size_t n_capacity = 0;
+ size_t n_meters = 0;
+
+ NBREC_METER_FOR_EACH (meter, ctx->idl) {
+ if (n_meters == n_capacity) {
+ meters = x2nrealloc(meters, &n_capacity, sizeof *meters);
+ }
+
+ meters[n_meters] = meter;
+ n_meters++;
+ }
+
+ if (n_meters) {
+ qsort(meters, n_meters, sizeof *meters, meter_cmp);
+ }
+
+ for (size_t i = 0; i < n_meters; i++) {
+ meter = meters[i];
+ ds_put_format(&ctx->output, "%s: unit=%s bands:\n", meter->name,
+ meter->unit);
+
+ for (size_t j = 0; j < meter->n_bands; j++) {
+ const struct nbrec_meter_band *band = meter->bands[j];
+
+ ds_put_format(&ctx->output, " %s: rate=%"PRId64"",
+ band->action, band->rate);
+ if (band->burst_size) {
+ ds_put_format(&ctx->output, ", burst_size=%"PRId64"",
+ band->burst_size);
+ }
+ }
+
+ ds_put_cstr(&ctx->output, "\n");
+ }
+
+ free(meters);
+}
+
+static void
+nbctl_meter_add(struct ctl_context *ctx)
+{
+ const struct nbrec_meter *meter;
+
+ const char *name = ctx->argv[1];
+ NBREC_METER_FOR_EACH (meter, ctx->idl) {
+ if (!strcmp(meter->name, name)) {
+ ctl_fatal("meter with name \"%s\" already exists", name);
+ }
+ }
+
+ if (!strncmp(name, "__", 2)) {
+ ctl_fatal("meter names that begin with \"__\" are reserved");
+ }
+
+ const char *unit = ctx->argv[2];
+ if (strcmp(unit, "kbps") && strcmp(unit, "pktps")) {
+ ctl_fatal("unit must be \"kbps\" or \"pktps\"");
+ }
+
+ const char *action = ctx->argv[3];
+ if (strcmp(action, "drop")) {
+ ctl_fatal("action must be \"drop\"");
+ }
+
+ int64_t rate;
+ if (!ovs_scan(ctx->argv[4], "%"SCNd64, &rate)
+ || rate < 1 || rate > UINT32_MAX) {
+ ctl_fatal("rate must be in the range 1...4294967295");
+ }
+
+ int64_t burst_size = 0;
+ if (ctx->argc > 5) {
+ if (!ovs_scan(ctx->argv[5], "%"SCNd64, &burst_size)
+ || burst_size < 0 || burst_size > UINT32_MAX) {
+ ctl_fatal("burst_size must be in the range 0...4294967295");
+ }
+ }
+
+ /* Create the band. We only support adding a single band. */
+ struct nbrec_meter_band *band = nbrec_meter_band_insert(ctx->txn);
+ nbrec_meter_band_set_action(band, action);
+ nbrec_meter_band_set_rate(band, rate);
+ nbrec_meter_band_set_burst_size(band, burst_size);
+
+ /* Create the meter. */
+ meter = nbrec_meter_insert(ctx->txn);
+ nbrec_meter_set_name(meter, name);
+ nbrec_meter_set_unit(meter, unit);
+ nbrec_meter_set_bands(meter, &band, 1);
+}
+
+static void
+nbctl_meter_del(struct ctl_context *ctx)
+{
+ const struct nbrec_meter *meter, *next;
+
+ /* If a name is not specified, delete all meters. */
+ if (ctx->argc == 1) {
+ NBREC_METER_FOR_EACH_SAFE (meter, next, ctx->idl) {
+ nbrec_meter_delete(meter);
+ }
+ return;
+ }
+
+ /* Remove the matching meter. */
+ NBREC_METER_FOR_EACH (meter, ctx->idl) {
+ if (strcmp(ctx->argv[1], meter->name)) {
+ continue;
+ }
+
+ nbrec_meter_delete(meter);
+ return;
+ }
+}
+
static void
nbctl_lb_add(struct ctl_context *ctx)
{
@@ -4678,6 +4815,12 @@ static const struct ctl_command_syntax nbctl_commands[]
= {
nbctl_qos_del, NULL, "", RW },
{ "qos-list", 1, 1, "SWITCH", NULL, nbctl_qos_list, NULL, "", RO },
+ /* meter commands. */
+ { "meter-add", 4, 5, "NAME UNIT ACTION RATE [BURST_SIZE]", NULL,
+ nbctl_meter_add, NULL, "", RW },
+ { "meter-del", 0, 1, "[NAME]", NULL, nbctl_meter_del, NULL, "", RW },
+ { "meter-list", 0, 0, "", NULL, nbctl_meter_list, NULL, "", RO },
+
/* logical switch port commands. */
{ "lsp-add", 2, 4, "SWITCH PORT [PARENT] [TAG]", NULL, nbctl_lsp_add,
NULL, "--may-exist", RW },
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index 64e217654c2f..7a1445e312ff 100644
--- a/tests/ovn-nbctl.at
+++ b/tests/ovn-nbctl.at
@@ -323,6 +323,64 @@ OVN_NBCTL_TEST_STOP
AT_CLEANUP
dnl ---------------------------------------------------------------------
+
+AT_SETUP([ovn-nbctl - Meters])
+OVN_NBCTL_TEST_START
+
+AT_CHECK([ovn-nbctl meter-add meter1 kbps drop 10])
+AT_CHECK([ovn-nbctl meter-add meter2 kbps drop 3 2])
+AT_CHECK([ovn-nbctl meter-add meter3 kbps drop 100 200])
+
+dnl Add duplicate meter name
+AT_CHECK([ovn-nbctl meter-add meter1 kbps drop 10], [1], [], [stderr])
+AT_CHECK([grep 'already exists' stderr], [0], [ignore])
+
+dnl Add reserved meter name
+AT_CHECK([ovn-nbctl meter-add __meter1 kbps drop 10], [1], [], [stderr])
+AT_CHECK([grep 'reserved' stderr], [0], [ignore])
+
+dnl Add meter with invalid rates
+AT_CHECK([ovn-nbctl meter-add meter4 kbps drop 100010111111], [1], [],
+[ovn-nbctl: rate must be in the range 1...4294967295
+])
+
+AT_CHECK([ovn-nbctl meter-add meter4 kbps drop 0], [1], [],
+[ovn-nbctl: rate must be in the range 1...4294967295
+])
+
+dnl Add meter with invalid burst_size
+AT_CHECK([ovn-nbctl meter-add meter4 kbps drop 10 100010111111], [1], [],
+[ovn-nbctl: burst_size must be in the range 0...4294967295
+])
+
+AT_CHECK([ovn-nbctl meter-list], [0], [dnl
+meter1: unit=kbps bands:
+ drop: rate=10
+meter2: unit=kbps bands:
+ drop: rate=3, burst_size=2
+meter3: unit=kbps bands:
+ drop: rate=100, burst_size=200
+])
+
+dnl Delete a single meter.
+AT_CHECK([ovn-nbctl meter-del meter2])
+AT_CHECK([ovn-nbctl meter-list], [0], [dnl
+meter1: unit=kbps bands:
+ drop: rate=10
+meter3: unit=kbps bands:
+ drop: rate=100, burst_size=200
+])
+
+dnl Delete all meters.
+AT_CHECK([ovn-nbctl meter-del])
+AT_CHECK([ovn-nbctl meter-list], [0], [dnl
+])
+
+OVN_NBCTL_TEST_STOP
+AT_CLEANUP
+
+dnl ---------------------------------------------------------------------
+
AT_SETUP([ovn-nbctl - NATs])
OVN_NBCTL_TEST_START
AT_CHECK([ovn-nbctl lr-add lr0])