[ovs-dev] [PATCH v9] ovn-nbctl: Add LB commands.

Flavio Fernandes flavio at flaviof.com
Thu Sep 29 21:30:31 UTC 2016


> On Sep 29, 2016, at 5:43 AM, nickcooper-zhangtonghao <nickcooper-zhangtonghao at opencloud.tech> wrote:
> 
> This patch provides the command line to create a load balancer.
> You can create a load balancer independently and add it to multiple
> switches or routers. A single load balancer can have multiple vips.
> Add a name column for the load balancer. With --add-duplicate,
> the command really creates a new load balancer with a duplicate name.
> This name has no special meaning or purpose other than to provide
> convenience for human interaction with the ovn-nb database.
> This patch also provides the unit tests and the documentation.
> 
> Signed-off-by: nickcooper-zhangtonghao <nickcooper-zhangtonghao at opencloud.tech>


comments/suggestions inline.

Also, I'd think it would be good to run with valgrind [1] to see if there is any error codepath
that may be causing leaks.

-- flaviof

[1]: cd ~/ovs.git && make check-valgrind TESTSUITEFLAGS='-k ovn'


> ---
> lib/packets.c                 |  18 ++
> lib/packets.h                 |  10 +
> ovn/ovn-nb.ovsschema          |   5 +-
> ovn/ovn-nb.xml                |   6 +
> ovn/utilities/ovn-nbctl.8.xml | 105 +++++++++
> ovn/utilities/ovn-nbctl.c     | 480 +++++++++++++++++++++++++++++++++++++++++-
> tests/ovn-nbctl.at            | 218 +++++++++++++++++++
> 7 files changed, 839 insertions(+), 3 deletions(-)
> 
> diff --git a/lib/packets.c b/lib/packets.c
> index e4c29d5..11bb587 100644
> --- a/lib/packets.c
> +++ b/lib/packets.c
> @@ -427,6 +427,24 @@ ip_parse(const char *s, ovs_be32 *ip)
>     return inet_pton(AF_INET, s, ip) == 1;
> }
> 
> +/* Parses string 's', which must be an IP address with a port number
> + * with ":" as a separator (e.g.: 192.168.1.2:80).
> + * Stores the IP address into '*ip' and port number to '*port'. */
> +char * OVS_WARN_UNUSED_RESULT
> +ip_parse_port(const char *s, ovs_be32 *ip, ovs_be16 *port)
> +{
> +    int n = 0;
> +    if (!ovs_scan_len(s, &n, IP_PORT_SCAN_FMT,
> +                IP_PORT_SCAN_ARGS(ip, port))) {
> +        return xasprintf("%s: invalid IP address or port number", s);
> +    }
> +
> +    if (s[n]) {
> +        return xasprintf("%s: invalid IP address or port number", s);
> +    }
> +    return NULL;
> +}
> +
> /* Parses string 's', which must be an IP address with an optional netmask or
>  * CIDR prefix length.  Stores the IP address into '*ip', netmask into '*mask',
>  * (255.255.255.255, if 's' lacks a netmask), and number of scanned characters
> diff --git a/lib/packets.h b/lib/packets.h
> index dcfcd04..21bd35c 100644
> --- a/lib/packets.h
> +++ b/lib/packets.h
> @@ -537,6 +537,14 @@ mpls_lse_to_bos(ovs_be32 mpls_lse)
>         &((uint8_t *) ip)[2],                               \
>         &((uint8_t *) ip)[3]
> 
> +#define IP_PORT_SCAN_FMT "%"SCNu8".%"SCNu8".%"SCNu8".%"SCNu8":%"SCNu16
> +#define IP_PORT_SCAN_ARGS(ip, port)                                    \
> +        ((void) (ovs_be32) *(ip), &((uint8_t *) ip)[0]),    \
> +        &((uint8_t *) ip)[1],                               \
> +        &((uint8_t *) ip)[2],                               \
> +        &((uint8_t *) ip)[3],                               \
> +        ((void) (ovs_be16) *(port), (uint16_t *) port)
> +
> /* Returns true if 'netmask' is a CIDR netmask, that is, if it consists of N
>  * high-order 1-bits and 32-N low-order 0-bits. */
> static inline bool
> @@ -558,6 +566,8 @@ ip_is_local_multicast(ovs_be32 ip)
> int ip_count_cidr_bits(ovs_be32 netmask);
> void ip_format_masked(ovs_be32 ip, ovs_be32 mask, struct ds *);
> bool ip_parse(const char *s, ovs_be32 *ip);
> +char *ip_parse_port(const char *s, ovs_be32 *ip, ovs_be16 *port)
> +    OVS_WARN_UNUSED_RESULT;
> char *ip_parse_masked(const char *s, ovs_be32 *ip, ovs_be32 *mask)
>     OVS_WARN_UNUSED_RESULT;
> char *ip_parse_cidr(const char *s, ovs_be32 *ip, unsigned int *plen)
> diff --git a/ovn/ovn-nb.ovsschema b/ovn/ovn-nb.ovsschema
> index b7e70aa..5f2f2bf 100644
> --- a/ovn/ovn-nb.ovsschema
> +++ b/ovn/ovn-nb.ovsschema
> @@ -1,7 +1,7 @@
> {
>     "name": "OVN_Northbound",
> -    "version": "5.3.3",
> -    "cksum": "2442952958 9945",
> +    "version": "5.3.4",
> +    "cksum": "1155817817 9975",
>     "tables": {
>         "NB_Global": {
>             "columns": {
> @@ -97,6 +97,7 @@
>             "isRoot": true},
>         "Load_Balancer": {
>             "columns": {
> +		"name": {"type": "string"},
>                 "vips": {
>                     "type": {"key": "string", "value": "string",
>                              "min": 0, "max": "unlimited"}},
> diff --git a/ovn/ovn-nb.xml b/ovn/ovn-nb.xml
> index c45a444..b7690d0 100644
> --- a/ovn/ovn-nb.xml
> +++ b/ovn/ovn-nb.xml
> @@ -676,6 +676,12 @@
>       Each row represents one load balancer.
>     </p>
> 
> +    <column name="name">
> +      A name for the load balancer.  This name has no special meaning or
> +      purpose other than to provide convenience for human interaction with
> +      the ovn-nb database.
> +    </column>
> +
>     <column name="vips">
>       <p>
>         A map of virtual IPv4 addresses (and an optional port number with
> diff --git a/ovn/utilities/ovn-nbctl.8.xml b/ovn/utilities/ovn-nbctl.8.xml
> index 76cf97e..7cd515f 100644
> --- a/ovn/utilities/ovn-nbctl.8.xml
> +++ b/ovn/utilities/ovn-nbctl.8.xml
> @@ -400,6 +400,111 @@
>       </dd>
>     </dl>
> 
> +    <h1>Load Balancer Commands</h1>
> +    <dl>
> +      <dt>[<code>--may-exist</code> | <code>--add-duplicate</code>] <code>lb-add</code> <var>lb</var> <var>vip</var> <var>ips</var> [<var>protocol</var>]</dt>
> +      <dd>
> +        <p>
> +         Creates a new load balancer named <var>lb</var> with the provided
> +         <var>vip</var> and <var>ips</var> or adds the <var>vip</var> to
> +         an existing <var>lb</var>.  <var>vip</var> should be a
> +         virtual IPv4 address (or an IPv4 address and a port number with
> +         <code>:</code> as a separator).  Examples for <var>vip</var> are
> +         <code>192.168.1.4</code> and <code>192.168.1.5:8080</code>.
> +         <var>ips</var> should be comma separated IPv4 endpoints (or comma
> +         separated IPv4 addresses and port numbers with <code>:</code> as a
> +         separator).  Examples for <var>ips</var> are <code>10.0.0.1,10.0.0.2
> +         </code>or <code>20.0.0.10:8800,20.0.0.11:8800</code>.
> +        </p>
> +
> +        <p>
> +         The optional argument <var>protocol</var> must be either
> +         <code>tcp</code> or <code>udp</code>.  This argument is useful when
> +         a port number is provided as part of the <var>vip</var>.  If the
> +         <var>protocol</var> is unspecified and a port number is provided as
> +         part of the <var>vip</var>, OVN assumes the <var>protocol</var> to
> +         be <code>tcp</code>.
> +        </p>
> +
> +        <p>
> +         It is an error if the <var>vip</var> already exists in the load
> +         balancer named <var>lb</var>, unless <code>--may-exist</code> is
> +         specified.  With <code>--add-duplicate</code>, the command really
> +         creates a new load balancer with a duplicate name.
> +        </p>
> +
> +        <p>
> +         The following example adds a load balancer.
> +        </p>
> +
> +        <p>
> +         <code>lb-add lb0 30.0.0.10:80
> +         192.168.10.10:80,192.168.10.20:80,192.168.10.30:80 udp</code>
> +        </p>
> +      </dd>
> +
> +      <dt>[<code>--if-exists</code>] <code>lb-del</code> <var>lb</var> [<var>vip</var>]</dt>
> +      <dd>
> +        Deletes <var>lb</var> or the <var>vip</var> from <var>lb</var>.
> +        If <var>vip</var> is supplied, only the <var>vip</var> will be
> +        deleted from the <var>lb</var>.  If only the <var>lb</var> is supplied,
> +        the <var>lb</var> will be deleted.  It is an error if <var>vip</var>
> +        does not already exist in <var>lb</var>, unless
> +        <code>--if-exists</code> is specified.
> +      </dd>
> +
> +      <dt><code>lb-list</code> [<var>lb</var>]</dt>
> +      <dd>
> +        Lists the LBs.  If <var>lb</var> is also specified, then only the
> +        specified <var>lb</var> will be listed.
> +      </dd>
> +
> +      <dt>[<code>--may-exist</code>] <code>ls-lb-add</code> <var>switch</var> <var>lb</var></dt>
> +      <dd>
> +        Adds the specified <var>lb</var> to <var>switch</var>.
> +        It is an error if a load balancer named <var>lb</var> already exists
> +        in the <var>switch</var>, unless <code>--may-exist</code> is specified.
> +      </dd>
> +
> +      <dt>[<code>--if-exists</code>] <code>ls-lb-del</code> <var>switch</var> [<var>lb</var>]</dt>
> +      <dd>
> +        Removes <var>lb</var> from <var>switch</var>.  If only
> +        <var>switch</var> is supplied, all the LBs from the logical switch are
> +        removed.  If <var>lb</var> is also specified, then only the
> +        <var>lb</var> will be removed from the logical switch.
> +        It is an error if <var>lb</var> does not exist in the
> +        <var>switch</var>, unless <code>--if-exists</code> is specified.
> +      </dd>
> +
> +      <dt><code>ls-lb-list</code> <var>switch</var></dt>
> +      <dd>
> +        Lists the LBs for the given <var>switch</var>.
> +      </dd>
> +
> +      <dt>[<code>--may-exist</code>] <code>lr-lb-add</code> <var>router</var> <var>lb</var></dt>
> +      <dd>
> +        Adds the specified <var>lb</var> to <var>router</var>.
> +        It is an error if a load balancer named <var>lb</var> already exists
> +        in the <var>router</var>, unless <code>--may-exist</code> is specified.
> +      </dd>
> +
> +      <dt>[<code>--if-exists</code>] <code>lr-lb-del</code> <var>router</var> [<var>lb</var>]</dt>
> +      <dd>
> +        Removes <var>lb</var> from <var>router</var>.  If only
> +        <var>router</var> is supplied, all the LBs from the logical router are
> +        removed.  If <var>lb</var> is also specified, then only the
> +        <var>lb</var> will be removed from the logical router.
> +        It is an error if <var>lb</var> does not exist in the
> +        <var>router</var>, unless <code>--if-exists</code> is specified.
> +      </dd>
> +
> +      <dt><code>lr-lb-list</code> <var>router</var></dt>
> +      <dd>
> +        Lists the LBs for the given <var>router</var>.
> +      </dd>
> +    </dl>
> +
> +
>     <h1>DHCP Options commands</h1>
> 
>     <dl>
> diff --git a/ovn/utilities/ovn-nbctl.c b/ovn/utilities/ovn-nbctl.c
> index 2148665..994aaaf 100644
> --- a/ovn/utilities/ovn-nbctl.c
> +++ b/ovn/utilities/ovn-nbctl.c
> @@ -384,6 +384,19 @@ Route commands:\n\
>                             remove routes from ROUTER\n\
>   lr-route-list ROUTER      print routes for ROUTER\n\
> \n\
> +LB commands:\n\
> +  lb-add LB VIP[:PORT] IP[:PORT]... [PROTOCOL]\n\
> +                            create a load-balancer or add a VIP to an\n\
> +                            existing load balancer\n\
> +  lb-del LB [VIP]           remove a load-balancer or just the VIP from\n\
> +                            the load balancer\n\
> +  lb-list [LB]              print load-balancers\n\
> +  lr-lb-add ROUTER LB       add a load-balancer to ROUTER\n\
> +  lr-lb-del ROUTER [LB]     remove load-balancers from ROUTER\n\
> +  lr-lb-list ROUTER         print load-balancers\n\
> +  ls-lb-add SWITCH LB       add a load-balancer to SWITCH\n\
> +  ls-lb-del SWITCH [LB]     remove load-balancers from SWITCH\n\
> +  ls-lb-list SWITCH         print load-balancers\n\
> \n\
> DHCP Options commands:\n\
>   dhcp-options-create CIDR [EXTERNAL_IDS]\n\
> @@ -493,6 +506,40 @@ ls_by_name_or_uuid(struct ctl_context *ctx, const char *id, bool must_exist)
>     return ls;
> }
> 
> +static const struct nbrec_load_balancer *
> +lb_by_name_or_uuid(struct ctl_context *ctx, const char *id, bool must_exist)
> +{
> +    const struct nbrec_load_balancer *lb = NULL;
> +
> +    struct uuid lb_uuid;
> +    bool is_uuid = uuid_from_string(&lb_uuid, id);

nitpick: make bool is_uuid const

> +    if (is_uuid) {
> +        lb = nbrec_load_balancer_get_for_uuid(ctx->idl, &lb_uuid);
> +    }
> +
> +    if (!lb) {
> +        const struct nbrec_load_balancer *iter;
> +
> +        NBREC_LOAD_BALANCER_FOR_EACH(iter, ctx->idl) {
> +            if (strcmp(iter->name, id)) {
> +                continue;
> +            }
> +            if (lb) {
> +                ctl_fatal("Multiple load balancers named '%s'.  "
> +                          "Use a UUID.", id);
> +            }
> +            lb = iter;
> +        }
> +    }
> +
> +    if (!lb && must_exist) {
> +        ctl_fatal("%s: load balancer %s not found", id,
> +                is_uuid ? "UUID" : "name");
> +    }
> +
> +    return lb;
> +}
> +
> /* Given pointer to logical router, this routine prints the router
>  * information.  */
> static void
> @@ -1316,7 +1363,419 @@ nbctl_acl_del(struct ctl_context *ctx)
>         }
>     }
> }
> -
> +
> +static void
> +nbctl_lb_add(struct ctl_context *ctx)
> +{
> +    const char *lb_name = ctx->argv[1];
> +    const char *lb_vip = ctx->argv[2];
> +    const char *lb_ips = ctx->argv[3];
> +
> +    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
> +    bool add_duplicate = shash_find(&ctx->options, "--add-duplicate") != NULL;

nitpick: make may_exist and add_duplicate const

> +
> +    const char *lb_proto;
> +    bool is_update_proto = false;
> +    bool is_vip_with_port = true;
> +
> +    if (ctx->argc == 4) {
> +        /* Default protocol. */
> +        lb_proto = "tcp";
> +    } else {
> +        /* Validate protocol. */
> +        lb_proto = ctx->argv[4];
> +        is_update_proto = true;
> +        if (strcmp(lb_proto, "tcp") && strcmp(lb_proto, "udp")) {
> +            ctl_fatal("%s: protocol must be one of \"tcp\", \"udp\".",
> +                    lb_proto);
> +        }
> +    }
> +
> +    ovs_be32 ipv4 = 0;
> +    ovs_be16 port = 0;
> +    char *error = ip_parse_port(lb_vip, &ipv4, &port);
> +    if (error) {
> +        free(error);
> +        if (!ip_parse(lb_vip, &ipv4)) {
> +            ctl_fatal("%s: should be an IPv4 address (or an IPv4 address "
> +                    "and a port number with : as a separator).", lb_vip);
> +        }
> +
> +        if (is_update_proto) {
> +            ctl_fatal("Protocol is unnecessary when no port of vip "
> +                    "is given.");
> +        }
> +        is_vip_with_port = false;
> +    }
> +
> +    char *token = NULL, *save_ptr = NULL;
> +    char *ips = xstrdup(lb_ips);
> +    for (token = strtok_r(ips, ",", &save_ptr);
> +            token != NULL; token = strtok_r(NULL, ",", &save_ptr)) {
> +        if (is_vip_with_port) {
> +            error = ip_parse_port(token, &ipv4, &port);
> +            if (error) {
> +                free(error);

also need free(ips);  ?

> +                ctl_fatal("%s: should be an IPv4 address and a port "
> +                        "number with : as a separator.", token);
> +            }
> +        } else {
> +            if (!ip_parse(token, &ipv4)) {
> +                ctl_fatal("%s: should be an IPv4 address.", token);

also need free(ips);  ?

> +            }
> +        }
> +    }
> +
> +    free(ips);
> +
> +    const struct nbrec_load_balancer *lb = NULL;
> +    if (!add_duplicate) {
> +        lb = lb_by_name_or_uuid(ctx, lb_name, false);
> +        if (lb) {
> +            if (smap_get(&lb->vips, lb_vip)) {
> +                if (!may_exist) {
> +                    ctl_fatal("%s: a load balancer with this vip (%s) "
> +                            "already exists", lb_name, lb_vip);
> +                }
> +                /* Update the vips. */
> +                smap_replace(CONST_CAST(struct smap *, &lb->vips),
> +                        lb_vip, lb_ips);
> +            } else {
> +                /* Add the new vips. */
> +                smap_add(CONST_CAST(struct smap *, &lb->vips),
> +                        lb_vip, lb_ips);
> +            }
> +
> +            /* Update the load balancer. */
> +            if (is_update_proto) {
> +                nbrec_load_balancer_verify_protocol(lb);
> +                nbrec_load_balancer_set_protocol(lb, lb_proto);
> +            }
> +            nbrec_load_balancer_verify_vips(lb);
> +            nbrec_load_balancer_set_vips(lb, &lb->vips);
> +            return;
> +        }
> +    }
> +
> +    /* Create the load balancer. */
> +    lb = nbrec_load_balancer_insert(ctx->txn);
> +    nbrec_load_balancer_set_name(lb, lb_name);
> +    nbrec_load_balancer_set_protocol(lb, lb_proto);
> +    smap_add(CONST_CAST(struct smap *, &lb->vips), lb_vip, lb_ips);
> +    nbrec_load_balancer_set_vips(lb, &lb->vips);
> +}
> +
> +static void
> +nbctl_lb_del(struct ctl_context *ctx)
> +{
> +    const char *id = ctx->argv[1];
> +    const struct nbrec_load_balancer *lb = NULL;
> +    bool must_exist = !shash_find(&ctx->options, "--if-exists");
> +
> +    lb = lb_by_name_or_uuid(ctx, id, false);
> +    if (!lb) {
> +        return;
> +    }
> +
> +    if (ctx->argc == 3) {
> +        const char *lb_vip = ctx->argv[2];
> +        if (smap_get(&lb->vips, lb_vip)) {
> +            smap_remove(CONST_CAST(struct smap *, &lb->vips), lb_vip);
> +            if (smap_is_empty(&lb->vips)) {
> +                nbrec_load_balancer_delete(lb);
> +                return;
> +            }
> +
> +            /* Delete the vip of the load balancer. */
> +            nbrec_load_balancer_verify_vips(lb);
> +            nbrec_load_balancer_set_vips(lb, &lb->vips);
> +            return;
> +        }
> +        if (must_exist) {
> +            ctl_fatal("vip %s is not part of the load balancer.",
> +                    lb_vip);
> +        }
> +        return;
> +    }
> +    nbrec_load_balancer_delete(lb);
> +}
> +
> +static void
> +lb_info_add_smap(const struct nbrec_load_balancer *lb,
> +                 struct smap *lbs)
> +{
> +    struct ds key = DS_EMPTY_INITIALIZER;
> +    struct ds val = DS_EMPTY_INITIALIZER;
> +    char *error, *protocol;
> +    ovs_be32 ipv4 = 0;
> +    ovs_be16 port = 0;
> +
> +    const struct smap_node **nodes = smap_sort(&lb->vips);
> +    if (nodes) {
> +        for (int i = 0; i < smap_count(&lb->vips); i++) {
> +            const struct smap_node *node = nodes[i];
> +            protocol = lb->protocol;
> +            error = ip_parse_port(node->key, &ipv4, &port);
> +            if (error) {
> +                free(error);
> +                protocol = "tcp/udp";
> +            }
> +
> +            i == 0 ? ds_put_format(&val,
> +                        UUID_FMT "    %-20.16s%-11.7s%-25.21s%s",
> +                        UUID_ARGS(&lb->header_.uuid),
> +                        lb->name, protocol,
> +                        node->key, node->value)
> +                   : ds_put_format(&val, "\n%60s%-11.7s%-25.21s%s",
> +                        "", protocol,
> +                        node->key, node->value);
> +        }
> +
> +        ds_put_format(&key, "%-20.16s", lb->name);
> +        smap_add(lbs, ds_cstr(&key), ds_cstr(&val));
> +
> +        ds_destroy(&key);
> +        ds_destroy(&val);
> +        free(nodes);
> +    }
> +}
> +
> +static void
> +lb_info_print(struct ctl_context *ctx, struct smap *lbs)
> +{
> +    const struct smap_node **nodes = smap_sort(lbs);
> +    if (nodes) {
> +        ds_put_format(&ctx->output, "%-40.36s%-20.16s%-11.7s%-25.21s%s\n",
> +                "UUID", "LB", "PROTO", "VIP", "IPs");
> +        for (size_t i = 0; i < smap_count(lbs); i++) {
> +            const struct smap_node *node = nodes[i];
> +            ds_put_format(&ctx->output, "%s\n", node->value);
> +        }
> +
> +        free(nodes);
> +    }
> +}
> +
> +static void
> +lb_info_list_all(struct ctl_context *ctx,
> +                 const char *lb_name, bool lb_check)
> +{
> +    const struct nbrec_load_balancer *lb;
> +    struct smap lbs = SMAP_INITIALIZER(&lbs);
> +
> +    NBREC_LOAD_BALANCER_FOR_EACH(lb, ctx->idl) {
> +        if (lb_check && strcmp(lb->name, lb_name)) {
> +            continue;
> +        }
> +        lb_info_add_smap(lb, &lbs);
> +    }
> +
> +    lb_info_print(ctx, &lbs);
> +    smap_destroy(&lbs);
> +}
> +
> +static void
> +nbctl_lb_list(struct ctl_context *ctx)
> +{
> +    if (ctx->argc == 1) {
> +        lb_info_list_all(ctx, NULL, false);
> +    } else if (ctx->argc == 2) {
> +        lb_info_list_all(ctx, ctx->argv[1], true);
> +    }
> +}
> +
> +static void
> +nbctl_lr_lb_add(struct ctl_context *ctx)
> +{
> +    const struct nbrec_logical_router *lr;
> +    const struct nbrec_load_balancer *new_lb;
> +
> +    lr = lr_by_name_or_uuid(ctx, ctx->argv[1], true);
> +    new_lb = lb_by_name_or_uuid(ctx, ctx->argv[2], true);
> +
> +    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;

nitpick: const bool may_exist

> +    for (int i = 0; i < lr->n_load_balancer; i++) {
> +        const struct nbrec_load_balancer *lb
> +            = lr->load_balancer[i];
> +
> +        if (uuid_equals(&new_lb->header_.uuid, &lb->header_.uuid)) {
> +            if (may_exist) {
> +                return;
> +            }
> +            ctl_fatal(UUID_FMT " : a load balancer with this UUID already "
> +                    "exists", UUID_ARGS(&lb->header_.uuid));
> +        }
> +    }
> +
> +    /* Insert the load balancer into the logical router. */
> +    nbrec_logical_router_verify_load_balancer(lr);
> +    struct nbrec_load_balancer **new_lbs
> +        = xmalloc(sizeof *new_lbs * (lr->n_load_balancer + 1));
> +
> +    memcpy(new_lbs, lr->load_balancer, sizeof *new_lbs * lr->n_load_balancer);
> +    new_lbs[lr->n_load_balancer] = CONST_CAST(struct nbrec_load_balancer *,
> +            new_lb);
> +    nbrec_logical_router_set_load_balancer(lr, new_lbs,
> +            lr->n_load_balancer + 1);
> +    free(new_lbs);
> +}
> +
> +static void
> +nbctl_lr_lb_del(struct ctl_context *ctx)
> +{
> +    const struct nbrec_logical_router *lr;
> +    const struct nbrec_load_balancer *del_lb;
> +    lr = lr_by_name_or_uuid(ctx, ctx->argv[1], true);
> +
> +    if (ctx->argc == 2) {
> +        /* If load-balancer is not specified, remove
> +         * all load-balancers from the logical router. */
> +        nbrec_logical_router_verify_load_balancer(lr);
> +        nbrec_logical_router_set_load_balancer(lr, NULL, 0);
> +        return;
> +    }
> +
> +    del_lb = lb_by_name_or_uuid(ctx, ctx->argv[2], true);
> +    for (size_t i = 0; i < lr->n_load_balancer; i++) {
> +        const struct nbrec_load_balancer *lb
> +            = lr->load_balancer[i];
> +
> +        if (uuid_equals(&del_lb->header_.uuid, &lb->header_.uuid)) {
> +            /* Remove the matching rule. */
> +            nbrec_logical_router_verify_load_balancer(lr);
> +
> +            struct nbrec_load_balancer **new_lbs
> +                = xmemdup(lr->load_balancer,
> +                    sizeof *new_lbs * lr->n_load_balancer);
> +            new_lbs[i] = lr->load_balancer[lr->n_load_balancer - 1];
> +            nbrec_logical_router_set_load_balancer(lr, new_lbs,
> +                                          lr->n_load_balancer - 1);
> +            free(new_lbs);
> +            return;
> +        }
> +    }
> +
> +    bool must_exist = !shash_find(&ctx->options, "--if-exists");

nitpick: const bool must_exist

> +    if (must_exist) {
> +        ctl_fatal("load balancer %s is not part of any logical router.",
> +                del_lb->name);
> +    }
> +}
> +
> +static void
> +nbctl_lr_lb_list(struct ctl_context *ctx)
> +{
> +    const char *lr_name = ctx->argv[1];
> +    const struct nbrec_logical_router *lr;
> +    struct smap lbs = SMAP_INITIALIZER(&lbs);
> +
> +    lr = lr_by_name_or_uuid(ctx, lr_name, true);
> +    for (int i = 0; i < lr->n_load_balancer; i++) {
> +        const struct nbrec_load_balancer *lb
> +            = lr->load_balancer[i];
> +        lb_info_add_smap(lb, &lbs);
> +    }
> +
> +    lb_info_print(ctx, &lbs);
> +    smap_destroy(&lbs);
> +}
> +
> +static void
> +nbctl_ls_lb_add(struct ctl_context *ctx)
> +{
> +    const struct nbrec_logical_switch *ls;
> +    const struct nbrec_load_balancer *new_lb;
> +
> +    ls = ls_by_name_or_uuid(ctx, ctx->argv[1], true);
> +    new_lb = lb_by_name_or_uuid(ctx, ctx->argv[2], true);
> +
> +    bool may_exist = shash_find(&ctx->options, "--may-exist") != NULL;
> +    for (int i = 0; i < ls->n_load_balancer; i++) {
> +        const struct nbrec_load_balancer *lb
> +            = ls->load_balancer[i];
> +
> +        if (uuid_equals(&new_lb->header_.uuid, &lb->header_.uuid)) {
> +            if (may_exist) {
> +                return;
> +            }
> +            ctl_fatal(UUID_FMT " : a load balancer with this UUID already "
> +                    "exists", UUID_ARGS(&lb->header_.uuid));
> +        }
> +    }
> +
> +    /* Insert the load balancer into the logical switch. */
> +    nbrec_logical_switch_verify_load_balancer(ls);
> +    struct nbrec_load_balancer **new_lbs
> +        = xmalloc(sizeof *new_lbs * (ls->n_load_balancer + 1));
> +
> +    memcpy(new_lbs, ls->load_balancer, sizeof *new_lbs * ls->n_load_balancer);
> +    new_lbs[ls->n_load_balancer] = CONST_CAST(struct nbrec_load_balancer *,
> +            new_lb);
> +    nbrec_logical_switch_set_load_balancer(ls, new_lbs,
> +            ls->n_load_balancer + 1);
> +    free(new_lbs);
> +}
> +
> +static void
> +nbctl_ls_lb_del(struct ctl_context *ctx)
> +{
> +    const struct nbrec_logical_switch *ls;
> +    const struct nbrec_load_balancer *del_lb;
> +    ls = ls_by_name_or_uuid(ctx, ctx->argv[1], true);
> +
> +    if (ctx->argc == 2) {
> +        /* If load-balancer is not specified, remove
> +         * all load-balancers from the logical switch. */
> +        nbrec_logical_switch_verify_load_balancer(ls);
> +        nbrec_logical_switch_set_load_balancer(ls, NULL, 0);
> +        return;
> +    }
> +
> +    del_lb = lb_by_name_or_uuid(ctx, ctx->argv[2], true);
> +    for (size_t i = 0; i < ls->n_load_balancer; i++) {
> +        const struct nbrec_load_balancer *lb
> +            = ls->load_balancer[i];
> +
> +        if (uuid_equals(&del_lb->header_.uuid, &lb->header_.uuid)) {
> +            /* Remove the matching rule. */
> +            nbrec_logical_switch_verify_load_balancer(ls);
> +
> +            struct nbrec_load_balancer **new_lbs
> +                = xmemdup(ls->load_balancer,
> +                        sizeof *new_lbs * ls->n_load_balancer);
> +            new_lbs[i] = ls->load_balancer[ls->n_load_balancer - 1];
> +            nbrec_logical_switch_set_load_balancer(ls, new_lbs,
> +                                          ls->n_load_balancer - 1);
> +            free(new_lbs);
> +            return;
> +        }
> +    }
> +
> +    bool must_exist = !shash_find(&ctx->options, "--if-exists");

nitpick: const ...

> +    if (must_exist) {
> +        ctl_fatal("load balancer %s is not part of any logical switch.",
> +                del_lb->name);
> +    }
> +}
> +
> +static void
> +nbctl_ls_lb_list(struct ctl_context *ctx)
> +{
> +    const char *ls_name = ctx->argv[1];
> +    const struct nbrec_logical_switch *ls;
> +    struct smap lbs = SMAP_INITIALIZER(&lbs);
> +
> +    ls = ls_by_name_or_uuid(ctx, ls_name, true);
> +    for (int i = 0; i < ls->n_load_balancer; i++) {
> +        const struct nbrec_load_balancer *lb
> +            = ls->load_balancer[i];
> +        lb_info_add_smap(lb, &lbs);
> +    }
> +
> +    lb_info_print(ctx, &lbs);
> +    smap_destroy(&lbs);
> +}
> +
> static void
> nbctl_lr_add(struct ctl_context *ctx)
> {
> @@ -2480,6 +2939,25 @@ static const struct ctl_command_syntax nbctl_commands[] = {
>     { "lr-route-list", 1, 1, "ROUTER", NULL, nbctl_lr_route_list, NULL,
>       "", RO },
> 
> +    /* load balancer commands. */
> +    { "lb-add", 3, 4, "LB VIP[:PORT] IP[:PORT]... [PROTOCOL]", NULL,
> +      nbctl_lb_add, NULL, "--may-exist,--add-duplicate", RW },
> +    { "lb-del", 1, 2, "LB [VIP]", NULL, nbctl_lb_del, NULL,
> +        "--if-exists", RW },
> +    { "lb-list", 0, 1, "[LB]", NULL, nbctl_lb_list, NULL, "", RO },
> +    { "lr-lb-add", 2, 2, "ROUTER LB", NULL, nbctl_lr_lb_add, NULL,
> +        "--may-exist", RW },
> +    { "lr-lb-del", 1, 2, "ROUTER [LB]", NULL, nbctl_lr_lb_del, NULL,
> +        "--if-exists", RW },
> +    { "lr-lb-list", 1, 1, "ROUTER", NULL, nbctl_lr_lb_list, NULL,
> +        "", RO },
> +    { "ls-lb-add", 2, 2, "SWITCH LB", NULL, nbctl_ls_lb_add, NULL,
> +        "--may-exist", RW },
> +    { "ls-lb-del", 1, 2, "SWITCH [LB]", NULL, nbctl_ls_lb_del, NULL,
> +        "--if-exists", RW },
> +    { "ls-lb-list", 1, 1, "SWITCH", NULL, nbctl_ls_lb_list, NULL,
> +        "", RO },
> +
>     /* DHCP_Options commands */
>     {"dhcp-options-create", 1, INT_MAX, "CIDR [EXTERNAL:IDS]", NULL,
>      nbctl_dhcp_options_create, NULL, "", RW },
> diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
> index 241e6d3..1e8290a 100644
> --- a/tests/ovn-nbctl.at
> +++ b/tests/ovn-nbctl.at
> @@ -239,6 +239,224 @@ AT_CLEANUP
> 
> dnl ---------------------------------------------------------------------
> 
> +AT_SETUP([ovn-nbctl - LBs])
> +OVN_NBCTL_TEST_START
> +
> +dnl Add two LBs.
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10:80a 192.168.10.10:80,192.168.10.20:80 tcp], [1], [],
> +[ovn-nbctl: 30.0.0.10:80a: should be an IPv4 address (or an IPv4 address and a port number with : as a separator).
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10:a80 192.168.10.10:80,192.168.10.20:80 tcp], [1], [],
> +[ovn-nbctl: 30.0.0.10:a80: should be an IPv4 address (or an IPv4 address and a port number with : as a separator).
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10: 192.168.10.10:80,192.168.10.20:80 tcp], [1], [],
> +[ovn-nbctl: 30.0.0.10:: should be an IPv4 address (or an IPv4 address and a port number with : as a separator).
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10:80 192.168.10.10:80,192.168.10.20 tcp], [1], [],
> +[ovn-nbctl: 192.168.10.20: should be an IPv4 address and a port number with : as a separator.
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.1a 192.168.10.10:80,192.168.10.20:80], [1], [],
> +[ovn-nbctl: 30.0.0.1a: should be an IPv4 address (or an IPv4 address and a port number with : as a separator).
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0 192.168.10.10:80,192.168.10.20:80], [1], [],
> +[ovn-nbctl: 30.0.0: should be an IPv4 address (or an IPv4 address and a port number with : as a separator).
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10 192.168.10.10,192.168.10.20:80], [1], [],
> +[ovn-nbctl: 192.168.10.20:80: should be an IPv4 address.
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10 192.168.10.10:a80], [1], [],
> +[ovn-nbctl: 192.168.10.10:a80: should be an IPv4 address.
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10 192.168.10.10:], [1], [],
> +[ovn-nbctl: 192.168.10.10:: should be an IPv4 address.
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10 192.168.10.1a], [1], [],
> +[ovn-nbctl: 192.168.10.1a: should be an IPv4 address.
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10 192.168.10], [1], [],
> +[ovn-nbctl: 192.168.10: should be an IPv4 address.
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10 192.168.10.10 tcp], [1], [],
> +[ovn-nbctl: Protocol is unnecessary when no port of vip is given.
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10 192.168.10.10:900 tcp], [1], [],
> +[ovn-nbctl: Protocol is unnecessary when no port of vip is given.
> +])
> +
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10:80 192.168.10.10:80,192.168.10.20:80])
> +AT_CHECK([ovn-nbctl lb-add lb1 30.0.0.10:80 192.168.10.10:80,192.168.10.20:80 tcp])
> +AT_CHECK([ovn-nbctl lb-list | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb0                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb1                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +])
> +
> +dnl Update the VIP of the lb1.
> +AT_CHECK([ovn-nbctl --may-exist lb-add lb1 30.0.0.10:80 192.168.10.10:80,192.168.10.20:8080])
> +AT_CHECK([ovn-nbctl lb-list | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb0                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb1                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:8080
> +])
> +
> +AT_CHECK([ovn-nbctl --may-exist lb-add lb1 30.0.0.10:80 192.168.10.10:80,192.168.10.20:8080 udp])
> +AT_CHECK([ovn-nbctl lb-list | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb0                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb1                 udp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:8080
> +])
> +
> +dnl Config lb1 with another VIP.
> +AT_CHECK([ovn-nbctl lb-add lb1 30.0.0.20:80 192.168.10.10:80 udp])
> +AT_CHECK([ovn-nbctl lb-list | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb0                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb1                 udp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:8080
> +                                                            udp        30.0.0.20:80             192.168.10.10:80
> +])
> +
> +AT_CHECK([ovn-nbctl lb-del lb1 30.0.0.20:80])
> +AT_CHECK([ovn-nbctl lb-list | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb0                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb1                 udp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:8080
> +])
> +
> +dnl Add LBs whose vip is just an IP address.
> +AT_CHECK([ovn-nbctl lb-add lb2 30.0.0.30 192.168.10.10])
> +AT_CHECK([ovn-nbctl lb-add lb3 30.0.0.30 192.168.10.10])
> +AT_CHECK([ovn-nbctl lb-list | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb0                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb1                 udp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:8080
> +<2>    lb2                 tcp/udp    30.0.0.30                192.168.10.10
> +<3>    lb3                 tcp/udp    30.0.0.30                192.168.10.10
> +])
> +AT_CHECK([ovn-nbctl lb-del lb2 30.0.0.30])
> +AT_CHECK([ovn-nbctl lb-del lb3 30.0.0.30])
> +
> +AT_CHECK([ovn-nbctl lb-add lb2 30.0.0.10:8080 192.168.10.10:80,192.168.10.20:80 tcp])
> +AT_CHECK([ovn-nbctl --add-duplicate lb-add lb2 30.0.0.10:8080 192.168.10.10:80,192.168.10.20:80 tcp])
> +AT_CHECK([ovn-nbctl lb-list | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb0                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb1                 udp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:8080
> +<2>    lb2                 tcp        30.0.0.10:8080           192.168.10.10:80,192.168.10.20:80
> +<3>    lb2                 tcp        30.0.0.10:8080           192.168.10.10:80,192.168.10.20:80
> +])
> +
> +dnl If there are multiple load balancers with the same name, use a UUID to update/delete.
> +AT_CHECK([ovn-nbctl lb-add lb2 30.0.0.10:8080 192.168.10.10:80,192.168.10.20:80 tcp], [1], [],
> +[ovn-nbctl: Multiple load balancers named 'lb2'.  Use a UUID.
> +])
> +
> +AT_CHECK([ovn-nbctl lb-del lb2], [1], [],
> +[ovn-nbctl: Multiple load balancers named 'lb2'.  Use a UUID.
> +])
> +
> +AT_CHECK([ovn-nbctl --may-exist lb-add lb1 30.0.0.10:80 192.168.10.10:8080,192.168.10.20:8080 udp])
> +AT_CHECK([ovn-nbctl --may-exist lb-add lb1 30.0.0.10:8080 192.168.10.10:8080,192.168.10.20:8080 udp])
> +AT_CHECK([ovn-nbctl --may-exist lb-add lb1 30.0.0.10:9090 192.168.10.10:8080,192.168.10.20:8080 udp])
> +AT_CHECK([ovn-nbctl lb-del lb0 30.0.0.10:80])
> +AT_CHECK([ovn-nbctl lb-del lb1])
> +AT_CHECK([ovn-nbctl lb-list | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb2                 tcp        30.0.0.10:8080           192.168.10.10:80,192.168.10.20:80
> +<1>    lb2                 tcp        30.0.0.10:8080           192.168.10.10:80,192.168.10.20:80
> +])
> +
> +dnl Add load balancer to logical switch.
> +AT_CHECK([ovn-nbctl ls-add ls0])
> +AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10:80 192.168.10.10:80,192.168.10.20:80])
> +AT_CHECK([ovn-nbctl lb-add lb1 30.0.0.10:80 192.168.10.10:80,192.168.10.20:80 udp])
> +AT_CHECK([ovn-nbctl lb-add lb3 30.0.0.10 192.168.10.10,192.168.10.20])
> +AT_CHECK([ovn-nbctl ls-lb-add ls0 lb0])
> +AT_CHECK([ovn-nbctl ls-lb-add ls0 lb1])
> +AT_CHECK([ovn-nbctl --may-exist ls-lb-add ls0 lb1])
> +AT_CHECK([ovn-nbctl ls-lb-add ls0 lb2], [1], [],
> +[ovn-nbctl: Multiple load balancers named 'lb2'.  Use a UUID.
> +])
> +AT_CHECK([ovn-nbctl ls-lb-add ls0 lb3])
> +
> +AT_CHECK([ovn-nbctl ls-lb-list ls0 | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb0                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb1                 udp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<2>    lb3                 tcp/udp    30.0.0.10                192.168.10.10,192.168.10.20
> +])
> +
> +AT_CHECK([ovn-nbctl ls-lb-del ls0 lb0])
> +AT_CHECK([ovn-nbctl ls-lb-list ls0 | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb1                 udp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb3                 tcp/udp    30.0.0.10                192.168.10.10,192.168.10.20
> +])
> +
> +AT_CHECK([ovn-nbctl ls-lb-del ls0 lb1])
> +AT_CHECK([ovn-nbctl ls-lb-del ls0 lb3])
> +AT_CHECK([ovn-nbctl ls-lb-list ls0 | ${PERL} $srcdir/uuidfilt.pl], [0], [])
> +AT_CHECK([ovn-nbctl --if-exists ls-lb-del ls0 lb1])
> +
> +dnl Remove all load balancers from logical switch.
> +AT_CHECK([ovn-nbctl ls-lb-add ls0 lb0])
> +AT_CHECK([ovn-nbctl ls-lb-add ls0 lb1])
> +AT_CHECK([ovn-nbctl ls-lb-add ls0 lb3])
> +AT_CHECK([ovn-nbctl ls-lb-del ls0])
> +AT_CHECK([ovn-nbctl ls-lb-list ls0 | ${PERL} $srcdir/uuidfilt.pl], [0], [])
> +
> +dnl Add load balancer to logical router.
> +AT_CHECK([ovn-nbctl lr-add lr0])
> +AT_CHECK([ovn-nbctl lr-lb-add lr0 lb0])
> +AT_CHECK([ovn-nbctl lr-lb-add lr0 lb1])
> +AT_CHECK([ovn-nbctl --may-exist lr-lb-add lr0 lb1])
> +AT_CHECK([ovn-nbctl lr-lb-add lr0 lb2], [1], [],
> +[ovn-nbctl: Multiple load balancers named 'lb2'.  Use a UUID.
> +])
> +AT_CHECK([ovn-nbctl lr-lb-add lr0 lb3])
> +
> +AT_CHECK([ovn-nbctl lr-lb-list lr0 | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb0                 tcp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb1                 udp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<2>    lb3                 tcp/udp    30.0.0.10                192.168.10.10,192.168.10.20
> +])
> +
> +AT_CHECK([ovn-nbctl lr-lb-del lr0 lb0])
> +AT_CHECK([ovn-nbctl lr-lb-list lr0 | ${PERL} $srcdir/uuidfilt.pl], [0], [dnl
> +UUID                                    LB                  PROTO      VIP                      IPs
> +<0>    lb1                 udp        30.0.0.10:80             192.168.10.10:80,192.168.10.20:80
> +<1>    lb3                 tcp/udp    30.0.0.10                192.168.10.10,192.168.10.20
> +])
> +
> +AT_CHECK([ovn-nbctl lr-lb-del lr0 lb1])
> +AT_CHECK([ovn-nbctl lr-lb-del lr0 lb3])
> +AT_CHECK([ovn-nbctl lr-lb-list lr0 | ${PERL} $srcdir/uuidfilt.pl], [0], [])
> +AT_CHECK([ovn-nbctl --if-exists lr-lb-del lr0 lb1])
> +
> +dnl Remove all load balancers from logical router.
> +AT_CHECK([ovn-nbctl lr-lb-add lr0 lb0])
> +AT_CHECK([ovn-nbctl lr-lb-add lr0 lb1])
> +AT_CHECK([ovn-nbctl lr-lb-add lr0 lb3])
> +AT_CHECK([ovn-nbctl lr-lb-del lr0])
> +AT_CHECK([ovn-nbctl lr-lb-list lr0 | ${PERL} $srcdir/uuidfilt.pl], [0], [])
> +
> +OVN_NBCTL_TEST_STOP
> +AT_CLEANUP
> +
> +dnl ---------------------------------------------------------------------
> +
> AT_SETUP([ovn-nbctl - basic logical router commands])
> OVN_NBCTL_TEST_START
> 
> --
> 1.8.3.1
> 
> 
> 
> _______________________________________________
> dev mailing list
> dev at openvswitch.org
> http://openvswitch.org/mailman/listinfo/dev




More information about the dev mailing list