Allow the CMS to configure template LBs.  The following configurations are
supported:
- VIPs of the form: ^vip_variable[:^port_variable|:port]
- Backends of the form:
  
^backendip_variable1[:^port_variable1|:port],^backendip_variable2[:^port_variable2|:port]
  OR
  ^backends_variable1,^backends_variable2

The CMS needs to provide a bit more information than with non-template load
balancers and must explicitly specify the address family to be used.

There is currently no support for template load balancers with
options:add_route=true set.  That is because ovn-northd does not
instantiate template variables.  While this is a limitation in a way, its
impact is not huge.  The load balancer 'add_route' option was added as a
way to make the CMS life easier and to avoid having to explicitly add a
route for the VIP.  The CMS can still achieve the same logical topology by
explicitly adding the VIP route.

Signed-off-by: Dumitru Ceara <[email protected]>
---
 controller/lflow.c    |   18 +-
 lib/lb.c              |  463 ++++++++++++++++++++++++++++++++++++++++++-------
 lib/lb.h              |   39 +++-
 lib/ovn-util.c        |    3 
 northd/northd.c       |   85 +++++----
 ovn-nb.xml            |   12 +
 tests/ovn-nbctl.at    |   23 +-
 tests/ovn-northd.at   |    7 +
 utilities/ovn-nbctl.c |  122 +++++++------
 9 files changed, 576 insertions(+), 196 deletions(-)

diff --git a/controller/lflow.c b/controller/lflow.c
index ad58fcd3c..ffc8fdb6e 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -1993,7 +1993,9 @@ add_lb_ct_snat_hairpin_flows(struct ovn_controller_lb *lb,
 
 static void
 consider_lb_hairpin_flows(const struct sbrec_load_balancer *sbrec_lb,
-                          const struct hmap *local_datapaths, bool use_ct_mark,
+                          const struct hmap *local_datapaths,
+                          const struct smap *template_vars,
+                          bool use_ct_mark,
                           struct ovn_desired_flow_table *flow_table,
                           struct simap *ids)
 {
@@ -2029,7 +2031,8 @@ consider_lb_hairpin_flows(const struct 
sbrec_load_balancer *sbrec_lb,
         return;
     }
 
-    struct ovn_controller_lb *lb = ovn_controller_lb_create(sbrec_lb);
+    struct ovn_controller_lb *lb = ovn_controller_lb_create(sbrec_lb,
+                                                            template_vars);
     uint8_t lb_proto = IPPROTO_TCP;
     if (lb->slb->protocol && lb->slb->protocol[0]) {
         if (!strcmp(lb->slb->protocol, "udp")) {
@@ -2059,7 +2062,9 @@ consider_lb_hairpin_flows(const struct 
sbrec_load_balancer *sbrec_lb,
  * backends to handle the load balanced hairpin traffic. */
 static void
 add_lb_hairpin_flows(const struct sbrec_load_balancer_table *lb_table,
-                     const struct hmap *local_datapaths, bool use_ct_mark,
+                     const struct hmap *local_datapaths,
+                     const struct smap *template_vars,
+                     bool use_ct_mark,
                      struct ovn_desired_flow_table *flow_table,
                      struct simap *ids,
                      struct id_pool *pool)
@@ -2082,8 +2087,8 @@ add_lb_hairpin_flows(const struct 
sbrec_load_balancer_table *lb_table,
             ovs_assert(id_pool_alloc_id(pool, &id));
             simap_put(ids, lb->name, id);
         }
-        consider_lb_hairpin_flows(lb, local_datapaths, use_ct_mark, flow_table,
-                                  ids);
+        consider_lb_hairpin_flows(lb, local_datapaths, template_vars,
+                                  use_ct_mark, flow_table, ids);
     }
 }
 
@@ -2220,6 +2225,7 @@ lflow_run(struct lflow_ctx_in *l_ctx_in, struct 
lflow_ctx_out *l_ctx_out)
                        l_ctx_in->local_datapaths,
                        l_ctx_out->flow_table);
     add_lb_hairpin_flows(l_ctx_in->lb_table, l_ctx_in->local_datapaths,
+                         l_ctx_in->template_vars,
                          l_ctx_in->lb_hairpin_use_ct_mark,
                          l_ctx_out->flow_table,
                          l_ctx_out->hairpin_lb_ids,
@@ -2351,6 +2357,7 @@ lflow_add_flows_for_datapath(const struct 
sbrec_datapath_binding *dp,
      * associated. */
     for (size_t i = 0; i < n_dp_lbs; i++) {
         consider_lb_hairpin_flows(dp_lbs[i], l_ctx_in->local_datapaths,
+                                  l_ctx_in->template_vars,
                                   l_ctx_in->lb_hairpin_use_ct_mark,
                                   l_ctx_out->flow_table,
                                   l_ctx_out->hairpin_lb_ids);
@@ -2493,6 +2500,7 @@ lflow_handle_changed_lbs(struct lflow_ctx_in *l_ctx_in,
         VLOG_DBG("Add load balancer hairpin flows for "UUID_FMT,
                  UUID_ARGS(&lb->header_.uuid));
         consider_lb_hairpin_flows(lb, l_ctx_in->local_datapaths,
+                                  l_ctx_in->template_vars,
                                   l_ctx_in->lb_hairpin_use_ct_mark,
                                   l_ctx_out->flow_table,
                                   l_ctx_out->hairpin_lb_ids);
diff --git a/lib/lb.c b/lib/lb.c
index 77674ea28..15cbf29ee 100644
--- a/lib/lb.c
+++ b/lib/lb.c
@@ -19,6 +19,7 @@
 #include "lib/ovn-nb-idl.h"
 #include "lib/ovn-sb-idl.h"
 #include "lib/ovn-util.h"
+#include "ovn/lex.h"
 
 /* OpenvSwitch lib includes. */
 #include "openvswitch/vlog.h"
@@ -26,6 +27,16 @@
 
 VLOG_DEFINE_THIS_MODULE(lb);
 
+static const char *lb_neighbor_responder_mode_names[] = {
+    [LB_NEIGH_RESPOND_REACHABLE] = "reachable",
+    [LB_NEIGH_RESPOND_ALL] = "all",
+    [LB_NEIGH_RESPOND_NONE] = "none",
+};
+
+static struct nbrec_load_balancer_health_check *
+ovn_lb_get_health_check(const struct nbrec_load_balancer *nbrec_lb,
+                        const char *vip_port_str, bool template);
+
 struct ovn_lb_ip_set *
 ovn_lb_ip_set_create(void)
 {
@@ -71,108 +82,312 @@ ovn_lb_ip_set_clone(struct ovn_lb_ip_set *lb_ip_set)
     return clone;
 }
 
-static
-bool ovn_lb_vip_init(struct ovn_lb_vip *lb_vip, const char *lb_key,
-                     const char *lb_value)
+/* Format for backend ips: "IP1:port1,IP2:port2,...". */
+static char *
+ovn_lb_backends_init_explicit(struct ovn_lb_vip *lb_vip, const char *value,
+                              size_t *n_backends)
 {
-    int addr_family;
-
-    if (!ip_address_and_port_from_lb_key(lb_key, &lb_vip->vip_str,
-                                         &lb_vip->vip_port, &addr_family)) {
-        return false;
-    }
-
-    if (addr_family == AF_INET) {
-        ovs_be32 vip4;
-        ip_parse(lb_vip->vip_str, &vip4);
-        in6_addr_set_mapped_ipv4(&lb_vip->vip, vip4);
-    } else {
-        ipv6_parse(lb_vip->vip_str, &lb_vip->vip);
-    }
-
-    /* Format for backend ips: "IP1:port1,IP2:port2,...". */
-    size_t n_backends = 0;
+    struct ds errors = DS_EMPTY_INITIALIZER;
     size_t n_allocated_backends = 0;
-    char *tokstr = xstrdup(lb_value);
+    char *tokstr = xstrdup(value);
     char *save_ptr = NULL;
+    *n_backends = 0;
+
     for (char *token = strtok_r(tokstr, ",", &save_ptr);
         token != NULL;
         token = strtok_r(NULL, ",", &save_ptr)) {
 
-        if (n_backends == n_allocated_backends) {
+        if (*n_backends == n_allocated_backends) {
             lb_vip->backends = x2nrealloc(lb_vip->backends,
                                           &n_allocated_backends,
                                           sizeof *lb_vip->backends);
         }
 
-        struct ovn_lb_backend *backend = &lb_vip->backends[n_backends];
+        struct ovn_lb_backend *backend = &lb_vip->backends[(*n_backends)];
         int backend_addr_family;
         if (!ip_address_and_port_from_lb_key(token, &backend->ip_str,
                                              &backend->port,
                                              &backend_addr_family)) {
+            if (lb_vip->port_str) {
+                ds_put_format(&errors, "%s: should be an IP address and a "
+                                       "port number with : as a separator, ",
+                              token);
+            } else {
+                ds_put_format(&errors, "%s: should be an IP address, ", token);
+            }
             continue;
         }
 
-        if (addr_family != backend_addr_family) {
+        if (lb_vip->address_family != backend_addr_family) {
             free(backend->ip_str);
+            ds_put_format(&errors, "%s: IP address family is different from "
+                                   "VIP %s, ",
+                          token, lb_vip->vip_str);
             continue;
         }
 
-        if (addr_family == AF_INET) {
+        if (lb_vip->port_str) {
+            if (!backend->port) {
+                free(backend->ip_str);
+                ds_put_format(&errors, "%s: should be an IP address and "
+                                       "a port number with : as a separator, ",
+                              token);
+                continue;
+            }
+        } else {
+            if (backend->port) {
+                free(backend->ip_str);
+                ds_put_format(&errors, "%s: should be an IP address, ", token);
+                continue;
+            }
+        }
+
+        backend->port_str =
+            backend->port ? xasprintf("%"PRIu16, backend->port) : NULL;
+
+        if (lb_vip->address_family == AF_INET) {
             ovs_be32 ip4;
             ip_parse(backend->ip_str, &ip4);
             in6_addr_set_mapped_ipv4(&backend->ip, ip4);
         } else {
             ipv6_parse(backend->ip_str, &backend->ip);
         }
-        n_backends++;
+        (*n_backends)++;
     }
     free(tokstr);
-    lb_vip->n_backends = n_backends;
-    return true;
+
+    if (ds_last(&errors) != EOF) {
+        ds_chomp(&errors, ' ');
+        ds_chomp(&errors, ',');
+        ds_put_char(&errors, '.');
+        return ds_steal_cstr(&errors);
+    }
+    return NULL;
 }
 
 static
-void ovn_lb_vip_destroy(struct ovn_lb_vip *vip)
+char *ovn_lb_vip_init_explicit(struct ovn_lb_vip *lb_vip, const char *lb_key,
+                               const char *lb_value)
+{
+    if (!ip_address_and_port_from_lb_key(lb_key, &lb_vip->vip_str,
+                                         &lb_vip->vip_port,
+                                         &lb_vip->address_family)) {
+        return xasprintf("%s: should be an IP address (or an IP address "
+                         "and a port number with : as a separator).", lb_key);
+    }
+
+    lb_vip->port_str = lb_vip->vip_port
+                       ? xasprintf("%"PRIu16, lb_vip->vip_port)
+                       : NULL;
+
+    if (lb_vip->address_family == AF_INET) {
+        ovs_be32 vip4;
+        ip_parse(lb_vip->vip_str, &vip4);
+        in6_addr_set_mapped_ipv4(&lb_vip->vip, vip4);
+    } else {
+        ipv6_parse(lb_vip->vip_str, &lb_vip->vip);
+    }
+    return ovn_lb_backends_init_explicit(lb_vip, lb_value,
+                                         &lb_vip->n_backends);
+}
+
+/* Parses backends of a templated LB VIP.
+ * For now only the following template forms are supported:
+ * A.
+ *   ^backendip_variable1[:^port_variable1|:port],
+ *   ^backendip_variable2[:^port_variable2|:port]
+ *
+ * B.
+ *   ^backends_variable1,^backends_variable2 is also a thing
+ *      where 'backends_variable1' may expand to IP1_1:PORT1_1 on chassis-1
+ *                                               IP1_2:PORT1_2 on chassis-2
+ *        and 'backends_variable2' may expand to IP2_1:PORT2_1 on chassis-1
+ *                                               IP2_2:PORT2_2 on chassis-2
+ */
+static char *
+ovn_lb_backends_init_template(struct ovn_lb_vip *lb_vip, const char *value_,
+                              size_t *n_backends)
+{
+    struct ds errors = DS_EMPTY_INITIALIZER;
+    char *value = xstrdup(value_);
+    char *save_ptr = NULL;
+    size_t n_allocated_backends = 0;
+    *n_backends = 0;
+
+    for (char *backend = strtok_r(value, ",", &save_ptr); backend;
+         backend = strtok_r(NULL, ",", &save_ptr)) {
+
+        char *atom = xstrdup(backend);
+        char *save_ptr2 = NULL;
+        bool success = false;
+        char *backend_ip = NULL;
+        char *backend_port = NULL;
+
+        for (char *subatom = strtok_r(atom, ":", &save_ptr2); subatom;
+             subatom = strtok_r(NULL, ":", &save_ptr2)) {
+            if (backend_ip && backend_port) {
+                success = false;
+                break;
+            }
+            success = true;
+            if (!backend_ip) {
+                backend_ip = xstrdup(subatom);
+            } else {
+                backend_port = xstrdup(subatom);
+            }
+        }
+
+        if (success) {
+            if (*n_backends == n_allocated_backends) {
+                lb_vip->backends = x2nrealloc(lb_vip->backends,
+                                              &n_allocated_backends,
+                                              sizeof *lb_vip->backends);
+            }
+
+            struct ovn_lb_backend *lb_backend =
+                &lb_vip->backends[(*n_backends)];
+            lb_backend->ip_str = backend_ip;
+            lb_backend->port_str = backend_port;
+            lb_backend->port = 0;
+            (*n_backends)++;
+        } else {
+            ds_put_format(&errors, "%s: should be a template of the form: "
+                          "'^backendip_variable1[:^port_variable1|:port]', ",
+                          atom);
+        }
+        free(atom);
+    }
+
+    free(value);
+    if (ds_last(&errors) != EOF) {
+        ds_chomp(&errors, ' ');
+        ds_chomp(&errors, ',');
+        ds_put_char(&errors, '.');
+        return ds_steal_cstr(&errors);
+    }
+    return NULL;
+}
+
+/* Parses a VIP of a templated LB.
+ * For now only the following template forms are supported:
+ *   ^vip_variable[:^port_variable|:port]
+ */
+static char *
+ovn_lb_vip_init_template(struct ovn_lb_vip *lb_vip, const char *lb_key_,
+                         const char *lb_value, int address_family)
+{
+    char *save_ptr = NULL;
+    char *lb_key = xstrdup(lb_key_);
+    bool success = false;
+
+    for (char *atom = strtok_r(lb_key, ":", &save_ptr); atom;
+         atom = strtok_r(NULL, ":", &save_ptr)) {
+        if (lb_vip->vip_str && lb_vip->port_str) {
+            success = false;
+            break;
+        }
+        success = true;
+        if (!lb_vip->vip_str) {
+            lb_vip->vip_str = xstrdup(atom);
+        } else {
+            lb_vip->port_str = xstrdup(atom);
+        }
+    }
+    free(lb_key);
+
+    if (!success) {
+        return xasprintf("%s: should be a template of the form: "
+                         "'^vip_variable[:^port_variable|:port]'.",
+                         lb_key_);
+    }
+
+    lb_vip->address_family = address_family;
+    return ovn_lb_backends_init_template(lb_vip, lb_value,
+                                         &lb_vip->n_backends);
+}
+
+/* Returns NULL on success, an error string on failure.  The caller is
+ * responsible for destroying 'lb_vip' in all cases.
+ */
+char *
+ovn_lb_vip_init(struct ovn_lb_vip *lb_vip, const char *lb_key,
+                const char *lb_value, bool template, int address_family)
+{
+    memset(lb_vip, 0, sizeof *lb_vip);
+
+    return !template
+           ?  ovn_lb_vip_init_explicit(lb_vip, lb_key, lb_value)
+           :  ovn_lb_vip_init_template(lb_vip, lb_key, lb_value,
+                                       address_family);
+}
+
+void
+ovn_lb_vip_destroy(struct ovn_lb_vip *vip)
 {
     free(vip->vip_str);
+    free(vip->port_str);
     for (size_t i = 0; i < vip->n_backends; i++) {
         free(vip->backends[i].ip_str);
+        free(vip->backends[i].port_str);
     }
     free(vip->backends);
 }
 
+void
+ovn_lb_vip_format(const struct ovn_lb_vip *vip, struct ds *s, bool template)
+{
+    bool needs_brackets = vip->address_family == AF_INET6 && vip->port_str
+                          && !template;
+    if (needs_brackets) {
+        ds_put_char(s, '[');
+    }
+    ds_put_cstr(s, vip->vip_str);
+    if (needs_brackets) {
+        ds_put_char(s, ']');
+    }
+    if (vip->port_str) {
+        ds_put_format(s, ":%s", vip->port_str);
+    }
+}
+
+void
+ovn_lb_vip_backends_format(const struct ovn_lb_vip *vip, struct ds *s,
+                           bool template)
+{
+    bool needs_brackets = vip->address_family == AF_INET6 && vip->port_str
+                          && !template;
+    for (size_t i = 0; i < vip->n_backends; i++) {
+        struct ovn_lb_backend *backend = &vip->backends[i];
+
+        if (needs_brackets) {
+            ds_put_char(s, '[');
+        }
+        ds_put_cstr(s, backend->ip_str);
+        if (needs_brackets) {
+            ds_put_char(s, ']');
+        }
+        if (backend->port_str) {
+            ds_put_format(s, ":%s", backend->port_str);
+        }
+        if (i != vip->n_backends - 1) {
+            ds_put_char(s, ',');
+        }
+    }
+}
+
 static
 void ovn_northd_lb_vip_init(struct ovn_northd_lb_vip *lb_vip_nb,
                             const struct ovn_lb_vip *lb_vip,
                             const struct nbrec_load_balancer *nbrec_lb,
-                            const char *vip_port_str, const char *backend_ips)
+                            const char *vip_port_str, const char *backend_ips,
+                            bool template)
 {
     lb_vip_nb->backend_ips = xstrdup(backend_ips);
     lb_vip_nb->n_backends = lb_vip->n_backends;
     lb_vip_nb->backends_nb = xcalloc(lb_vip_nb->n_backends,
                                      sizeof *lb_vip_nb->backends_nb);
-
-    struct nbrec_load_balancer_health_check *lb_health_check = NULL;
-    if (nbrec_lb->protocol && !strcmp(nbrec_lb->protocol, "sctp")) {
-        if (nbrec_lb->n_health_check > 0) {
-            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
-            VLOG_WARN_RL(&rl,
-                         "SCTP load balancers do not currently support "
-                         "health checks. Not creating health checks for "
-                         "load balancer " UUID_FMT,
-                         UUID_ARGS(&nbrec_lb->header_.uuid));
-        }
-    } else {
-        for (size_t j = 0; j < nbrec_lb->n_health_check; j++) {
-            if (!strcmp(nbrec_lb->health_check[j]->vip, vip_port_str)) {
-                lb_health_check = nbrec_lb->health_check[j];
-                break;
-            }
-        }
-    }
-
-    lb_vip_nb->lb_health_check = lb_health_check;
+    lb_vip_nb->lb_health_check =
+        ovn_lb_get_health_check(nbrec_lb, vip_port_str, template);
 }
 
 static
@@ -203,12 +418,111 @@ ovn_lb_get_hairpin_snat_ip(const struct uuid *lb_uuid,
     }
 }
 
+static bool
+ovn_lb_get_routable_mode(const struct nbrec_load_balancer *nbrec_lb,
+                         bool routable, bool template)
+{
+    if (template && routable) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+        VLOG_WARN_RL(&rl, "Template load balancer "UUID_FMT" does not suport "
+                           "option 'add_route'.  Forcing it to disabled.",
+                     UUID_ARGS(&nbrec_lb->header_.uuid));
+        return false;
+    }
+    return routable;
+}
+
+static bool
+ovn_lb_neigh_mode_is_valid(enum lb_neighbor_responder_mode mode, bool template)
+{
+    if (!template) {
+        return true;
+    }
+
+    switch (mode) {
+    case LB_NEIGH_RESPOND_REACHABLE:
+        return false;
+    case LB_NEIGH_RESPOND_ALL:
+    case LB_NEIGH_RESPOND_NONE:
+        return true;
+    }
+}
+
+static enum lb_neighbor_responder_mode
+ovn_lb_get_neigh_mode(const struct nbrec_load_balancer *nbrec_lb,
+                      const char *mode, bool template)
+{
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+    enum lb_neighbor_responder_mode default_mode =
+        template ? LB_NEIGH_RESPOND_NONE : LB_NEIGH_RESPOND_REACHABLE;
+
+    if (!mode) {
+        mode = lb_neighbor_responder_mode_names[default_mode];
+    }
+
+    for (size_t i = 0; i < ARRAY_SIZE(lb_neighbor_responder_mode_names); i++) {
+        if (!strcmp(mode, lb_neighbor_responder_mode_names[i])) {
+            if (ovn_lb_neigh_mode_is_valid(i, template)) {
+                return i;
+            }
+            break;
+        }
+    }
+
+    VLOG_WARN_RL(&rl, "Invalid neighbor responder mode %s for load balancer "
+                       UUID_FMT", forcing it to %s",
+                 mode, UUID_ARGS(&nbrec_lb->header_.uuid),
+                 lb_neighbor_responder_mode_names[default_mode]);
+    return default_mode;
+}
+
+static struct nbrec_load_balancer_health_check *
+ovn_lb_get_health_check(const struct nbrec_load_balancer *nbrec_lb,
+                        const char *vip_port_str, bool template)
+{
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
+
+    if (!nbrec_lb->n_health_check) {
+        return NULL;
+    }
+
+    if (nbrec_lb->protocol && !strcmp(nbrec_lb->protocol, "sctp")) {
+        VLOG_WARN_RL(&rl,
+                     "SCTP load balancers do not currently support "
+                     "health checks. Not creating health checks for "
+                     "load balancer " UUID_FMT,
+                     UUID_ARGS(&nbrec_lb->header_.uuid));
+        return NULL;
+    }
+
+    if (template) {
+        VLOG_WARN_RL(&rl,
+                     "Template load balancers do not currently support "
+                     "health checks. Not creating health checks for "
+                     "load balancer " UUID_FMT,
+                     UUID_ARGS(&nbrec_lb->header_.uuid));
+        return NULL;
+    }
+
+    for (size_t i = 0; i < nbrec_lb->n_health_check; i++) {
+        if (!strcmp(nbrec_lb->health_check[i]->vip, vip_port_str)) {
+            return nbrec_lb->health_check[i];
+        }
+    }
+    return NULL;
+}
+
 struct ovn_northd_lb *
 ovn_northd_lb_create(const struct nbrec_load_balancer *nbrec_lb)
 {
     bool is_udp = nullable_string_is_equal(nbrec_lb->protocol, "udp");
     bool is_sctp = nullable_string_is_equal(nbrec_lb->protocol, "sctp");
     struct ovn_northd_lb *lb = xzalloc(sizeof *lb);
+    int address_family = !strcmp(smap_get_def(&nbrec_lb->options,
+                                              "address-family", "ipv4"),
+                                 "ipv4")
+                         ? AF_INET
+                         : AF_INET6;
 
     lb->nlb = nbrec_lb;
     lb->proto = is_udp ? "udp" : is_sctp ? "sctp" : "tcp";
@@ -216,12 +530,16 @@ ovn_northd_lb_create(const struct nbrec_load_balancer 
*nbrec_lb)
     lb->vips = xcalloc(lb->n_vips, sizeof *lb->vips);
     lb->vips_nb = xcalloc(lb->n_vips, sizeof *lb->vips_nb);
     lb->controller_event = smap_get_bool(&nbrec_lb->options, "event", false);
-    lb->routable = smap_get_bool(&nbrec_lb->options, "add_route", false);
+
+    bool routable = smap_get_bool(&nbrec_lb->options, "add_route", false);
+    lb->routable = ovn_lb_get_routable_mode(nbrec_lb, routable, lb->template);
+
     lb->skip_snat = smap_get_bool(&nbrec_lb->options, "skip_snat", false);
-    const char *mode =
-        smap_get_def(&nbrec_lb->options, "neighbor_responder", "reachable");
-    lb->neigh_mode = strcmp(mode, "all") ? LB_NEIGH_RESPOND_REACHABLE
-                                         : LB_NEIGH_RESPOND_ALL;
+    lb->template = smap_get_bool(&nbrec_lb->options, "template", false);
+
+    const char *mode = smap_get(&nbrec_lb->options, "neighbor_responder");
+    lb->neigh_mode = ovn_lb_get_neigh_mode(nbrec_lb, mode, lb->template);
+
     sset_init(&lb->ips_v4);
     sset_init(&lb->ips_v6);
     struct smap_node *node;
@@ -231,13 +549,19 @@ ovn_northd_lb_create(const struct nbrec_load_balancer 
*nbrec_lb)
         struct ovn_lb_vip *lb_vip = &lb->vips[n_vips];
         struct ovn_northd_lb_vip *lb_vip_nb = &lb->vips_nb[n_vips];
 
-        lb_vip->empty_backend_rej = smap_get_bool(&nbrec_lb->options,
-                                                  "reject", false);
-        if (!ovn_lb_vip_init(lb_vip, node->key, node->value)) {
+        char *error = ovn_lb_vip_init(lb_vip, node->key, node->value,
+                                      lb->template, address_family);
+        if (error) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+            VLOG_WARN_RL(&rl, "Failed to initialize LB VIP: %s", error);
+            ovn_lb_vip_destroy(lb_vip);
+            free(error);
             continue;
         }
+        lb_vip->empty_backend_rej = smap_get_bool(&nbrec_lb->options,
+                                                  "reject", false);
         ovn_northd_lb_vip_init(lb_vip_nb, lb_vip, nbrec_lb,
-                               node->key, node->value);
+                               node->key, node->value, lb->template);
         if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
             sset_add(&lb->ips_v4, lb_vip->vip_str);
         } else {
@@ -385,7 +709,8 @@ ovn_lb_group_find(const struct hmap *lb_groups, const 
struct uuid *uuid)
 }
 
 struct ovn_controller_lb *
-ovn_controller_lb_create(const struct sbrec_load_balancer *sbrec_lb)
+ovn_controller_lb_create(const struct sbrec_load_balancer *sbrec_lb,
+                         const struct smap *template_vars)
 {
     struct ovn_controller_lb *lb = xzalloc(sizeof *lb);
 
@@ -399,10 +724,20 @@ ovn_controller_lb_create(const struct sbrec_load_balancer 
*sbrec_lb)
     SMAP_FOR_EACH (node, &sbrec_lb->vips) {
         struct ovn_lb_vip *lb_vip = &lb->vips[n_vips];
 
-        if (!ovn_lb_vip_init(lb_vip, node->key, node->value)) {
-            continue;
+        char *key_s = lexer_parse_template_string(xstrdup(node->key),
+                                                  template_vars,
+                                                  NULL);
+        char *value_s = lexer_parse_template_string(xstrdup(node->value),
+                                                    template_vars,
+                                                    NULL);
+        char *error = ovn_lb_vip_init_explicit(lb_vip, key_s, value_s);
+        if (error) {
+            free(error);
+        } else {
+            n_vips++;
         }
-        n_vips++;
+        free(key_s);
+        free(value_s);
     }
 
     /* It's possible that parsing VIPs fails.  Update the lb->n_vips to the
diff --git a/lib/lb.h b/lib/lb.h
index 80ac03399..265a1215a 100644
--- a/lib/lb.h
+++ b/lib/lb.h
@@ -35,6 +35,7 @@ struct uuid;
 enum lb_neighbor_responder_mode {
     LB_NEIGH_RESPOND_REACHABLE,
     LB_NEIGH_RESPOND_ALL,
+    LB_NEIGH_RESPOND_NONE,
 };
 
 /* The "routable" ssets are subsets of the load balancer IPs for which IP
@@ -67,6 +68,7 @@ struct ovn_northd_lb {
     bool controller_event;
     bool routable;
     bool skip_snat;
+    bool template;
 
     struct sset ips_v4;
     struct sset ips_v6;
@@ -81,19 +83,31 @@ struct ovn_northd_lb {
 };
 
 struct ovn_lb_vip {
-    struct in6_addr vip;
-    char *vip_str;
-    uint16_t vip_port;
-
+    struct in6_addr vip; /* Only used in ovn-controller. */
+    char *vip_str;       /* Actual VIP string representation (without port).
+                          * To be used in ovn-northd.
+                          */
+    uint16_t vip_port;   /* Only used in ovn-controller. */
+    char *port_str;      /* Actual port string representation.  To be used
+                          * in ovn-controller.
+                          */
     struct ovn_lb_backend *backends;
     size_t n_backends;
     bool empty_backend_rej;
+    int address_family;
 };
 
 struct ovn_lb_backend {
-    struct in6_addr ip;
-    char *ip_str;
-    uint16_t port;
+    struct in6_addr ip;  /* Only used in ovn-controller. */
+    char *ip_str;        /* Actual IP string representation. To be used in
+                          * ovn-northd.
+                          */
+    uint16_t port;       /* Mostly used in ovn-controller but also for
+                          * healthcheck in ovn-northd.
+                          */
+    char *port_str;      /* Actual port string representation. To be used
+                          * in ovn-northd.
+                          */
 };
 
 /* ovn-northd specific backend information. */
@@ -171,7 +185,16 @@ struct ovn_controller_lb {
 };
 
 struct ovn_controller_lb *ovn_controller_lb_create(
-    const struct sbrec_load_balancer *);
+    const struct sbrec_load_balancer *,
+    const struct smap *template_vars);
 void ovn_controller_lb_destroy(struct ovn_controller_lb *);
 
+char *ovn_lb_vip_init(struct ovn_lb_vip *lb_vip, const char *lb_key,
+                      const char *lb_value, bool template, int address_family);
+void ovn_lb_vip_destroy(struct ovn_lb_vip *vip);
+void ovn_lb_vip_format(const struct ovn_lb_vip *vip, struct ds *s,
+                       bool template);
+void ovn_lb_vip_backends_format(const struct ovn_lb_vip *vip, struct ds *s,
+                                bool template);
+
 #endif /* OVN_LIB_LB_H 1 */
diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index 596e93129..1cc4c98b1 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -799,9 +799,6 @@ ip_address_and_port_from_lb_key(const char *key, char 
**ip_address,
 {
     struct sockaddr_storage ss;
     if (!inet_parse_active(key, 0, &ss, false, NULL)) {
-        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
-        VLOG_WARN_RL(&rl, "bad ip address or port for load balancer key %s",
-                     key);
         *ip_address = NULL;
         *port = 0;
         *addr_family = 0;
diff --git a/northd/northd.c b/northd/northd.c
index 63879ccd0..a93aee37e 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -3708,6 +3708,10 @@ static void
 ovn_lb_svc_create(struct ovsdb_idl_txn *ovnsb_txn, struct ovn_northd_lb *lb,
                   struct hmap *monitor_map, struct hmap *ports)
 {
+    if (lb->template) {
+        return;
+    }
+
     for (size_t i = 0; i < lb->n_vips; i++) {
         struct ovn_lb_vip *lb_vip = &lb->vips[i];
         struct ovn_northd_lb_vip *lb_vip_nb = &lb->vips_nb[i];
@@ -4024,12 +4028,19 @@ static void
 build_lrouter_lb_reachable_ips(struct ovn_datapath *od,
                                const struct ovn_northd_lb *lb)
 {
+    /* If configured to not reply to any neighbor requests for all VIPs
+     * return early.
+     */
+    if (lb->neigh_mode == LB_NEIGH_RESPOND_NONE) {
+        return;
+    }
+
     /* If configured to reply to neighbor requests for all VIPs force them
      * all to be considered "reachable".
      */
     if (lb->neigh_mode == LB_NEIGH_RESPOND_ALL) {
         for (size_t i = 0; i < lb->n_vips; i++) {
-            if (IN6_IS_ADDR_V4MAPPED(&lb->vips[i].vip)) {
+            if (lb->vips[i].address_family == AF_INET) {
                 sset_add(&od->lb_ips->ips_v4_reachable, lb->vips[i].vip_str);
             } else {
                 sset_add(&od->lb_ips->ips_v6_reachable, lb->vips[i].vip_str);
@@ -4041,8 +4052,9 @@ build_lrouter_lb_reachable_ips(struct ovn_datapath *od,
     /* Otherwise, a VIP is reachable if there's at least one router
      * subnet that includes it.
      */
+    ovs_assert(lb->neigh_mode == LB_NEIGH_RESPOND_REACHABLE);
     for (size_t i = 0; i < lb->n_vips; i++) {
-        if (IN6_IS_ADDR_V4MAPPED(&lb->vips[i].vip)) {
+        if (lb->vips[i].address_family == AF_INET) {
             ovs_be32 vip_ip4 = in6_addr_get_mapped_ipv4(&lb->vips[i].vip);
             struct ovn_port *op;
 
@@ -5823,16 +5835,16 @@ build_empty_lb_event_flow(struct ovn_lb_vip *lb_vip,
     ds_clear(action);
     ds_clear(match);
 
-    bool ipv4 = IN6_IS_ADDR_V4MAPPED(&lb_vip->vip);
+    bool ipv4 = lb_vip->address_family == AF_INET;
 
     ds_put_format(match, "ip%s.dst == %s && %s",
                   ipv4 ? "4": "6", lb_vip->vip_str, lb->proto);
 
     char *vip = lb_vip->vip_str;
-    if (lb_vip->vip_port) {
-        ds_put_format(match, " && %s.dst == %u", lb->proto, lb_vip->vip_port);
-        vip = xasprintf("%s%s%s:%u", ipv4 ? "" : "[", lb_vip->vip_str,
-                        ipv4 ? "" : "]", lb_vip->vip_port);
+    if (lb_vip->port_str) {
+        ds_put_format(match, " && %s.dst == %s", lb->proto, lb_vip->port_str);
+        vip = xasprintf("%s%s%s:%s", ipv4 ? "" : "[", lb_vip->vip_str,
+                        ipv4 ? "" : "]", lb_vip->port_str);
     }
 
     ds_put_format(action,
@@ -5843,7 +5855,7 @@ build_empty_lb_event_flow(struct ovn_lb_vip *lb_vip,
                   event_to_string(OVN_EVENT_EMPTY_LB_BACKENDS),
                   vip, lb->proto,
                   UUID_ARGS(&lb->nlb->header_.uuid));
-    if (lb_vip->vip_port) {
+    if (lb_vip->port_str) {
         free(vip);
     }
     return true;
@@ -6899,7 +6911,7 @@ build_lb_rules_pre_stateful(struct hmap *lflows, struct 
ovn_northd_lb *lb,
         /* Store the original destination IP to be used when generating
          * hairpin flows.
          */
-        if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
+        if (lb->vips[i].address_family == AF_INET) {
             ip_match = "ip4";
             ds_put_format(action, REG_ORIG_DIP_IPV4 " = %s; ",
                           lb_vip->vip_str);
@@ -6910,7 +6922,7 @@ build_lb_rules_pre_stateful(struct hmap *lflows, struct 
ovn_northd_lb *lb,
         }
 
         const char *proto = NULL;
-        if (lb_vip->vip_port) {
+        if (lb_vip->port_str) {
             proto = "tcp";
             if (lb->nlb->protocol) {
                 if (!strcmp(lb->nlb->protocol, "udp")) {
@@ -6923,8 +6935,8 @@ build_lb_rules_pre_stateful(struct hmap *lflows, struct 
ovn_northd_lb *lb,
             /* Store the original destination port to be used when generating
              * hairpin flows.
              */
-            ds_put_format(action, REG_ORIG_TP_DPORT " = %"PRIu16"; ",
-                          lb_vip->vip_port);
+            ds_put_format(action, REG_ORIG_TP_DPORT " = %s; ",
+                          lb_vip->port_str);
         }
         ds_put_format(action, "%s;", ct_lb_mark ? "ct_lb_mark" : "ct_lb");
 
@@ -6962,24 +6974,12 @@ build_lb_rules(struct hmap *lflows, struct 
ovn_northd_lb *lb, bool ct_lb_mark,
         struct ovn_lb_vip *lb_vip = &lb->vips[i];
         struct ovn_northd_lb_vip *lb_vip_nb = &lb->vips_nb[i];
         const char *ip_match = NULL;
-        if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
+        if (lb_vip->address_family == AF_INET) {
             ip_match = "ip4";
         } else {
             ip_match = "ip6";
         }
 
-        const char *proto = NULL;
-        if (lb_vip->vip_port) {
-            proto = "tcp";
-            if (lb->nlb->protocol) {
-                if (!strcmp(lb->nlb->protocol, "udp")) {
-                    proto = "udp";
-                } else if (!strcmp(lb->nlb->protocol, "sctp")) {
-                    proto = "sctp";
-                }
-            }
-        }
-
         ds_clear(action);
         ds_clear(match);
 
@@ -6997,8 +6997,9 @@ build_lb_rules(struct hmap *lflows, struct ovn_northd_lb 
*lb, bool ct_lb_mark,
         ds_put_format(match, "ct.new && %s.dst == %s", ip_match,
                       lb_vip->vip_str);
         int priority = 110;
-        if (lb_vip->vip_port) {
-            ds_put_format(match, " && %s.dst == %d", proto, lb_vip->vip_port);
+        if (lb_vip->port_str) {
+            ds_put_format(match, " && %s.dst == %s", lb->proto,
+                          lb_vip->port_str);
             priority = 120;
         }
 
@@ -9998,7 +9999,7 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip *lb_vip,
      * of "ct_lb_mark($targets);". The other flow is for ct.est with
      * an action of "next;".
      */
-    if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
+    if (lb_vip->address_family == AF_INET) {
         ds_put_format(match, "ip4 && "REG_NEXT_HOP_IPV4" == %s",
                       lb_vip->vip_str);
     } else {
@@ -10014,14 +10015,14 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip 
*lb_vip,
     }
 
     int prio = 110;
-    if (lb_vip->vip_port) {
+    if (lb_vip->port_str) {
         prio = 120;
         new_match = xasprintf("ct.new && %s && %s && "
-                              REG_ORIG_TP_DPORT_ROUTER" == %d",
-                              ds_cstr(match), lb->proto, lb_vip->vip_port);
+                              REG_ORIG_TP_DPORT_ROUTER" == %s",
+                              ds_cstr(match), lb->proto, lb_vip->port_str);
         est_match = xasprintf("ct.est && %s && %s && "
-                              REG_ORIG_TP_DPORT_ROUTER" == %d && %s == 1",
-                              ds_cstr(match), lb->proto, lb_vip->vip_port,
+                              REG_ORIG_TP_DPORT_ROUTER" == %s && %s == 1",
+                              ds_cstr(match), lb->proto, lb_vip->port_str,
                               ct_natted);
     } else {
         new_match = xasprintf("ct.new && %s", ds_cstr(match));
@@ -10030,7 +10031,7 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip 
*lb_vip,
     }
 
     const char *ip_match = NULL;
-    if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
+    if (lb_vip->address_family == AF_INET) {
         ip_match = "ip4";
     } else {
         ip_match = "ip6";
@@ -10048,9 +10049,9 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip 
*lb_vip,
         ds_put_format(&undnat_match, "(%s.src == %s", ip_match,
                       backend->ip_str);
 
-        if (backend->port) {
-            ds_put_format(&undnat_match, " && %s.src == %d) || ",
-                          lb->proto, backend->port);
+        if (backend->port_str) {
+            ds_put_format(&undnat_match, " && %s.src == %s) || ",
+                          lb->proto, backend->port_str);
         } else {
             ds_put_cstr(&undnat_match, ") || ");
         }
@@ -10063,9 +10064,9 @@ build_lrouter_nat_flows_for_lb(struct ovn_lb_vip 
*lb_vip,
     struct ds unsnat_match = DS_EMPTY_INITIALIZER;
     ds_put_format(&unsnat_match, "%s && %s.dst == %s && %s",
                   ip_match, ip_match, lb_vip->vip_str, lb->proto);
-    if (lb_vip->vip_port) {
-        ds_put_format(&unsnat_match, " && %s.dst == %d", lb->proto,
-                      lb_vip->vip_port);
+    if (lb_vip->port_str) {
+        ds_put_format(&unsnat_match, " && %s.dst == %s", lb->proto,
+                      lb_vip->port_str);
     }
 
     struct ovn_datapath **gw_router_skip_snat =
@@ -10305,7 +10306,7 @@ build_lrouter_defrag_flows_for_lb(struct ovn_northd_lb 
*lb,
         ds_clear(&defrag_actions);
         ds_clear(match);
 
-        if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
+        if (lb_vip->address_family == AF_INET) {
             ds_put_format(match, "ip && ip4.dst == %s", lb_vip->vip_str);
             ds_put_format(&defrag_actions, REG_NEXT_HOP_IPV4" = %s; ",
                           lb_vip->vip_str);
@@ -10315,7 +10316,7 @@ build_lrouter_defrag_flows_for_lb(struct ovn_northd_lb 
*lb,
                           lb_vip->vip_str);
         }
 
-        if (lb_vip->vip_port) {
+        if (lb_vip->port_str) {
             ds_put_format(match, " && %s", lb->proto);
             prio = 110;
 
diff --git a/ovn-nb.xml b/ovn-nb.xml
index bd28ac372..fb63b212e 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -1908,6 +1908,18 @@
         requests only for VIPs that are part of a router's subnet.  The default
         value of this option, if not specified, is <code>reachable</code>.
       </column>
+
+      <column name="options" key="template">
+        Option to be set to <code>true</code>, if the load balancer is a
+        template.  In this case the template variables used within the
+        load balancer VIPs and backends will be instantiated differently
+        on different chassis.
+      </column>
+
+      <column name="options" key="address-family">
+        Address family used by the load balancer.  This value is used only
+        when the load balancer is a template.
+      </column>
     </group>
   </table>
 
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index 6a5d52a5d..9b766d63e 100644
--- a/tests/ovn-nbctl.at
+++ b/tests/ovn-nbctl.at
@@ -857,23 +857,19 @@ AT_CHECK([ovn-nbctl -vsocket_util:off lb-add lb0 
30.0.0.10 192.168.10.10:a80], [
 [ovn-nbctl: 192.168.10.10:a80: should be an IP address.
 ])
 
-AT_CHECK([ovn-nbctl -vsocket_util:off lb-add lb0 30.0.0.10 192.168.10.10:], 
[1], [],
-[ovn-nbctl: 192.168.10.10:: should be an IP address.
-])
-
 AT_CHECK([ovn-nbctl -vsocket_util:off lb-add lb0 30.0.0.10 192.168.10.1a], 
[1], [],
 [ovn-nbctl: 192.168.10.1a: should be an IP address.
 ])
 
 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: Protocol is unnecessary when no port of vip is given.
+[ovn-nbctl: 192.168.10.10:80: should be an IP address, 192.168.10.20:80: 
should be an IP 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], [],
+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.
 ])
 
@@ -1111,7 +1107,7 @@ AT_CHECK([ovn-nbctl -vsocket_util:off lb-add lb0 
ae0f::10fff [[fd0f::10]]:80,fd0
 
 
 AT_CHECK([ovn-nbctl -vsocket_util:off lb-add lb0 ae0f::10 
[[fd0f::10]]:80,[[fd0f::20]]:80], [1], [],
-[ovn-nbctl: [[fd0f::10]]:80: should be an IP address.
+[ovn-nbctl: [[fd0f::10]]:80: should be an IP address, [[fd0f::20]]:80: should 
be an IP address.
 ])
 
 
@@ -1125,18 +1121,13 @@ AT_CHECK([ovn-nbctl -vsocket_util:off lb-add lb0 
ae0f::10 [[fd0f::10]]:a80], [1]
 ])
 
 
-AT_CHECK([ovn-nbctl -vsocket_util:off lb-add lb0 ae0f::10 [[fd0f::10]]:], [1], 
[],
-[ovn-nbctl: [[fd0f::10]]:: should be an IP address.
-])
-
-
 AT_CHECK([ovn-nbctl -vsocket_util:off lb-add lb0 ae0f::10 fd0f::1001a], [1], 
[],
 [ovn-nbctl: fd0f::1001a: should be an IP address.
 ])
 
 
 AT_CHECK([ovn-nbctl -vsocket_util:off lb-add lb0 [[ae0f::10]]: 
[[fd0f::10]]:80,[[fd0f::20]]:80 tcp], [1], [],
-[ovn-nbctl: Protocol is unnecessary when no port of vip is given.
+[ovn-nbctl: [[fd0f::10]]:80: should be an IP address, [[fd0f::20]]:80: should 
be an IP address.
 ])
 
 
@@ -1146,7 +1137,7 @@ AT_CHECK([ovn-nbctl lb-add lb0 ae0f::10 fd0f::10 tcp], 
[1], [],
 
 
 AT_CHECK([ovn-nbctl lb-add lb0 ae0f::10 [[fd0f::10]]:900 tcp], [1], [],
-[ovn-nbctl: Protocol is unnecessary when no port of vip is given.
+[ovn-nbctl: [[fd0f::10]]:900: should be an IP address.
 ])
 
 AT_CHECK([ovn-nbctl lb-add lb0 ae0f::10 192.168.10.10], [1], [],
@@ -1158,7 +1149,7 @@ AT_CHECK([ovn-nbctl lb-add lb0 ae0f::10 192.168.10.10], 
[1], [],
 ])
 
 AT_CHECK([ovn-nbctl lb-add lb0 [[ae0f::10]]:80 192.168.10.10:80], [1], [],
-[ovn-nbctl: 192.168.10.10:80: IP address family is different from VIP 
[[ae0f::10]]:80.
+[ovn-nbctl: 192.168.10.10:80: IP address family is different from VIP ae0f::10.
 ])
 
 AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10 ae0f::10], [1], [],
@@ -1166,7 +1157,7 @@ AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10 ae0f::10], [1], 
[],
 ])
 
 AT_CHECK([ovn-nbctl lb-add lb0 30.0.0.10:80 [[ae0f::10]]:80], [1], [],
-[ovn-nbctl: [[ae0f::10]]:80: IP address family is different from VIP 
30.0.0.10:80.
+[ovn-nbctl: [[ae0f::10]]:80: IP address family is different from VIP 30.0.0.10.
 ])
 
 AT_CHECK([ovn-nbctl lb-add lb0 ae0f::10 fd0f::10])
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index b6843956c..39032d269 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -1828,6 +1828,11 @@ ovn-nbctl set Load_Balancer lb8 
options:neighbor_responder=all
 ovn-nbctl lb-add lb9 "[[4444::4444]]:8080" "[[10::10]]:8080" udp
 ovn-nbctl set Load_Balancer lb9 options:neighbor_responder=all
 
+ovn-nbctl lb-add lb10 "55.55.55.55:8080" "10.0.0.8:8080" udp
+ovn-nbctl set Load_Balancer lb10 options:neighbor_responder=none
+ovn-nbctl lb-add lb11 "[[5555::5555]]:8080" "[[10::10]]:8080" udp
+ovn-nbctl set Load_Balancer lb11 options:neighbor_responder=none
+
 ovn-nbctl lr-lb-add lr lb1
 ovn-nbctl lr-lb-add lr lb2
 ovn-nbctl lr-lb-add lr lb3
@@ -1837,6 +1842,8 @@ ovn-nbctl lr-lb-add lr lb6
 ovn-nbctl lr-lb-add lr lb7
 ovn-nbctl lr-lb-add lr lb8
 ovn-nbctl lr-lb-add lr lb9
+ovn-nbctl lr-lb-add lr lb10
+ovn-nbctl lr-lb-add lr lb11
 
 ovn-nbctl --wait=sb sync
 lr_key=$(fetch_column sb:datapath_binding tunnel_key external_ids:name=lr)
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index 811468dc6..4feae1db2 100644
--- a/utilities/ovn-nbctl.c
+++ b/utilities/ovn-nbctl.c
@@ -28,6 +28,7 @@
 #include "openvswitch/json.h"
 #include "lib/acl-log.h"
 #include "lib/copp.h"
+#include "lib/lb.h"
 #include "lib/ovn-nb-idl.h"
 #include "lib/ovn-util.h"
 #include "memory.h"
@@ -2837,6 +2838,7 @@ nbctl_lb_add(struct ctl_context *ctx)
     bool empty_backend_rej = shash_find(&ctx->options, "--reject") != NULL;
     bool empty_backend_event = shash_find(&ctx->options, "--event") != NULL;
     bool add_route = shash_find(&ctx->options, "--add-route") != NULL;
+    bool template = shash_find(&ctx->options, "--template") != NULL;
 
     if (empty_backend_event && empty_backend_rej) {
             ctl_error(ctx,
@@ -2844,10 +2846,12 @@ nbctl_lb_add(struct ctl_context *ctx)
             return;
     }
 
+    const char *lb_address_family_str = "ipv4";
+    int lb_address_family = AF_INET;
     const char *lb_proto;
     bool is_update_proto = false;
 
-    if (ctx->argc == 4) {
+    if (ctx->argc <= 4) {
         /* Default protocol. */
         lb_proto = "tcp";
     } else {
@@ -2863,79 +2867,61 @@ nbctl_lb_add(struct ctl_context *ctx)
         }
     }
 
-    struct sockaddr_storage ss_vip;
-    if (!inet_parse_active(lb_vip, 0, &ss_vip, false, NULL)) {
-        ctl_error(ctx, "%s: should be an IP address (or an IP address "
-                  "and a port number with : as a separator).", lb_vip);
-        return;
-    }
-
-    struct ds lb_vip_normalized_ds = DS_EMPTY_INITIALIZER;
-    uint16_t lb_vip_port = ss_get_port(&ss_vip);
-    if (lb_vip_port) {
-        ss_format_address(&ss_vip, &lb_vip_normalized_ds);
-        ds_put_format(&lb_vip_normalized_ds, ":%d", lb_vip_port);
-    } else {
-        ss_format_address_nobracks(&ss_vip, &lb_vip_normalized_ds);
-    }
-    const char *lb_vip_normalized = ds_cstr(&lb_vip_normalized_ds);
+    if (ctx->argc > 5) {
+        lb_address_family_str = ctx->argv[5];
+        lb_address_family = !strcmp(lb_address_family_str, "ipv4")
+                            ? AF_INET : AF_INET6;
 
-    if (!lb_vip_port && is_update_proto) {
-        ds_destroy(&lb_vip_normalized_ds);
-        ctl_error(ctx, "Protocol is unnecessary when no port of vip "
-                  "is given.");
-        return;
     }
 
-    char *token = NULL, *save_ptr = NULL;
+    struct ds lb_vip_normalized = DS_EMPTY_INITIALIZER;
     struct ds lb_ips_new = DS_EMPTY_INITIALIZER;
-    for (token = strtok_r(lb_ips, ",", &save_ptr);
-            token != NULL; token = strtok_r(NULL, ",", &save_ptr)) {
-        struct sockaddr_storage ss_dst;
+    struct ovn_lb_vip lb_vip_parsed;
 
-        if (lb_vip_port) {
-            if (!inet_parse_active(token, -1, &ss_dst, false, NULL)) {
-                ctl_error(ctx, "%s: should be an IP address and a port "
-                          "number with : as a separator.", token);
-                goto out;
-            }
-        } else {
-            if (!inet_parse_address(token, &ss_dst)) {
-                ctl_error(ctx, "%s: should be an IP address.", token);
-                goto out;
-            }
-        }
+    char *error = ovn_lb_vip_init(&lb_vip_parsed, lb_vip, lb_ips, template,
+                                  lb_address_family);
+    if (error) {
+        ctl_error(ctx, "%s", error);
+        ovn_lb_vip_destroy(&lb_vip_parsed);
+        free(error);
+        return;
+    }
 
-        if (ss_vip.ss_family != ss_dst.ss_family) {
-            ctl_error(ctx, "%s: IP address family is different from VIP %s.",
-                      token, lb_vip_normalized);
-            goto out;
-        }
-        ds_put_format(&lb_ips_new, "%s%s",
-                lb_ips_new.length ? "," : "", token);
+    if (is_update_proto && !lb_vip_parsed.port_str) {
+        ctl_error(ctx, "Protocol is unnecessary when no port of vip is "
+                       "given.");
+        ovn_lb_vip_destroy(&lb_vip_parsed);
+        return;
     }
 
+    ovn_lb_vip_format(&lb_vip_parsed, &lb_vip_normalized, template);
+    ovn_lb_vip_backends_format(&lb_vip_parsed, &lb_ips_new, template);
+    ovn_lb_vip_destroy(&lb_vip_parsed);
+
     const struct nbrec_load_balancer *lb = NULL;
     if (!add_duplicate) {
-        char *error = lb_by_name_or_uuid(ctx, lb_name, false, &lb);
+        error = lb_by_name_or_uuid(ctx, lb_name, false, &lb);
         if (error) {
             ctx->error = error;
             goto out;
         }
         if (lb) {
-            if (smap_get(&lb->vips, lb_vip_normalized)) {
+            if (smap_get(&lb->vips, ds_cstr(&lb_vip_normalized))) {
                 if (!may_exist) {
                     ctl_error(ctx, "%s: a load balancer with this vip (%s) "
-                              "already exists", lb_name, lb_vip_normalized);
+                              "already exists", lb_name,
+                              ds_cstr(&lb_vip_normalized));
                     goto out;
                 }
                 /* Update the vips. */
                 smap_replace(CONST_CAST(struct smap *, &lb->vips),
-                        lb_vip_normalized, ds_cstr(&lb_ips_new));
+                             ds_cstr(&lb_vip_normalized),
+                             ds_cstr(&lb_ips_new));
             } else {
                 /* Add the new vips. */
                 smap_add(CONST_CAST(struct smap *, &lb->vips),
-                        lb_vip_normalized, ds_cstr(&lb_ips_new));
+                         ds_cstr(&lb_vip_normalized),
+                         ds_cstr(&lb_ips_new));
             }
 
             /* Update the load balancer. */
@@ -2954,7 +2940,7 @@ nbctl_lb_add(struct ctl_context *ctx)
     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_normalized, ds_cstr(&lb_ips_new));
+             ds_cstr(&lb_vip_normalized), ds_cstr(&lb_ips_new));
     nbrec_load_balancer_set_vips(lb, &lb->vips);
     struct smap options = SMAP_INITIALIZER(&options);
     if (empty_backend_rej) {
@@ -2966,12 +2952,16 @@ nbctl_lb_add(struct ctl_context *ctx)
     if (add_route) {
         smap_add(&options, "add_route", "true");
     }
+    if (template) {
+        smap_add(&options, "template", "true");
+        smap_add(&options, "address-family", lb_address_family_str);
+    }
     nbrec_load_balancer_set_options(lb, &options);
     smap_destroy(&options);
 out:
     ds_destroy(&lb_ips_new);
 
-    ds_destroy(&lb_vip_normalized_ds);
+    ds_destroy(&lb_vip_normalized);
 }
 
 static void
@@ -3025,6 +3015,7 @@ static void
 nbctl_pre_lb_list(struct ctl_context *ctx)
 {
     ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_name);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_options);
     ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_protocol);
     ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_vips);
 }
@@ -3033,6 +3024,7 @@ static void
 lb_info_add_smap(const struct nbrec_load_balancer *lb,
                  struct smap *lbs, int vip_width)
 {
+    bool template = smap_get_bool(&lb->options, "template", false);
     const struct smap_node **nodes = smap_sort(&lb->vips);
     if (!nodes) {
         return;
@@ -3041,13 +3033,24 @@ lb_info_add_smap(const struct nbrec_load_balancer *lb,
     struct ds val = DS_EMPTY_INITIALIZER;
     for (size_t i = 0; i < smap_count(&lb->vips); i++) {
         const struct smap_node *node = nodes[i];
+        const char *protocol = lb->protocol;
 
-        struct sockaddr_storage ss;
-        if (!inet_parse_active(node->key, 0, &ss, false, NULL)) {
-            continue;
+        if (!template) {
+            struct sockaddr_storage ss;
+            if (!inet_parse_active(node->key, 0, &ss, false, NULL)) {
+                continue;
+            }
+            protocol = ss_get_port(&ss) ? lb->protocol : "";
+        } else {
+            if (!lb->protocol) {
+                VLOG_WARN("Load Balancer "UUID_FMT" (%s) is a template and "
+                          "misses protocol", UUID_ARGS(&lb->header_.uuid),
+                          lb->name);
+                continue;
+            }
+            protocol = lb->protocol;
         }
 
-        char *protocol = ss_get_port(&ss) ? lb->protocol : "";
         if (i == 0) {
             ds_put_format(&val, UUID_FMT "    %-20.16s%-11.7s%-*.*s%s",
                           UUID_ARGS(&lb->header_.uuid),
@@ -3239,6 +3242,7 @@ nbctl_pre_lr_lb_list(struct ctl_context *ctx)
                          &nbrec_logical_router_col_load_balancer_group);
 
     ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_name);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_options);
     ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_protocol);
     ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_vips);
 
@@ -3402,6 +3406,7 @@ nbctl_pre_ls_lb_list(struct ctl_context *ctx)
                          &nbrec_logical_switch_col_load_balancer_group);
 
     ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_name);
+    ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_options);
     ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_protocol);
     ovsdb_idl_add_column(ctx->idl, &nbrec_load_balancer_col_vips);
 
@@ -7469,9 +7474,10 @@ static const struct ctl_command_syntax nbctl_commands[] 
= {
       nbctl_pre_lr_nat_set_ext_ips, nbctl_lr_nat_set_ext_ips,
       NULL, "--is-exempted", RW},
     /* load balancer commands. */
-    { "lb-add", 3, 4, "LB VIP[:PORT] IP[:PORT]... [PROTOCOL]",
+    { "lb-add", 3, 5, "LB VIP[:PORT] IP[:PORT]... [PROTOCOL] [ADDRESS_FAMILY]",
       nbctl_pre_lb_add, nbctl_lb_add, NULL,
-      "--may-exist,--add-duplicate,--reject,--event,--add-route", RW },
+      "--may-exist,--add-duplicate,--reject,--event,--add-route,--template",
+      RW },
     { "lb-del", 1, 2, "LB [VIP]", nbctl_pre_lb_del, nbctl_lb_del, NULL,
         "--if-exists", RW },
     { "lb-list", 0, 1, "[LB]", nbctl_pre_lb_list, nbctl_lb_list, NULL, "", RO 
},

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to