diff mbox series

[ovs-dev,6/8] lb: Support using templates.

Message ID 166454653673.202549.12551251078582166589.stgit@dceara.remote.csb
State Changes Requested
Headers show
Series Add OVN component templates. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_ovn-kubernetes success github build: passed
ovsrobot/github-robot-_Build_and_Test fail github build: failed

Commit Message

Dumitru Ceara Sept. 30, 2022, 2:02 p.m. UTC
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 <dceara@redhat.com>
---
 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(-)

Comments

Dumitru Ceara Oct. 6, 2022, 8:57 p.m. UTC | #1
On 9/30/22 16:02, Dumitru Ceara wrote:
> 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 <dceara@redhat.com>
> ---

This needs the following incremental fix without which
ls_in_pre_stateful logical flows are incorrect.  I'll
add this in v2 as well.

diff --git a/northd/northd.c b/northd/northd.c
index 21ce8c9185..7313972a2e 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -6941,8 +6941,8 @@ build_lb_rules_pre_stateful(struct hmap *lflows, struct ovn_northd_lb *lb,
         ds_put_format(action, "%s;", ct_lb_mark ? "ct_lb_mark" : "ct_lb");
 
         ds_put_format(match, "%s.dst == %s", ip_match, lb_vip->vip_str);
-        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", proto, lb_vip->port_str);
         }
 
         struct ovn_lflow *lflow_ref = NULL;


>  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
> dev@openvswitch.org
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
>
Han Zhou Oct. 21, 2022, 5:47 a.m. UTC | #2
On Fri, Sep 30, 2022 at 7:02 AM Dumitru Ceara <dceara@redhat.com> wrote:
>
> 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 <dceara@redhat.com>

Hi Dumitru, sorry for the delayed response. Please see my comments below.

> ---
>  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,

template_vars is added as an input, but I don't see I-P for template_vars
for hairpin flows in this patch, which makes this patch incomplete.
I see that it is taken care by the next patch. So they should be either
combined or switch the patch order, so that each incremental patch is
correct.
In addition, some basic tests may be added to this patch (which should
discover problems of hairpin flows when template vars are updated, due to
the missing I-P).

> +                          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;
> +}

This new limitation of the neighbor_responder mode needs to be documented
in ovn-nb.xml for the options:neighbor_responder.

Also curious why can't we support responders for templated LBs? Can the
relevant lflows be generated with the template vars used for the LB VIPs as
well?

> +
> +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);

Shouldn't we check the options:use_template before parsing?

> +        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.

It would be better to mention what are template variables, with a reference
to the chassis_template_var table. An simple example would be even better.

> +      </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.

It would be better to state the keywords "ipv4" & "ipv6", and point out
that this is *required* if and only if the options:template is "true".

Thanks,
Han

> +      </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 },
>
Dumitru Ceara Oct. 21, 2022, 7:59 a.m. UTC | #3
On 10/21/22 07:47, Han Zhou wrote:
> On Fri, Sep 30, 2022 at 7:02 AM Dumitru Ceara <dceara@redhat.com> wrote:
>>
>> 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 <dceara@redhat.com>
> 
> Hi Dumitru, sorry for the delayed response. Please see my comments below.
> 

Hi Han,

No worries, thanks for the review!

>> ---
>>  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,
> 
> template_vars is added as an input, but I don't see I-P for template_vars
> for hairpin flows in this patch, which makes this patch incomplete.
> I see that it is taken care by the next patch. So they should be either
> combined or switch the patch order, so that each incremental patch is
> correct.
> In addition, some basic tests may be added to this patch (which should
> discover problems of hairpin flows when template vars are updated, due to
> the missing I-P).
> 

I initially wanted to keep things simpler but you're right, now at patch
5/8 we have an incomplete implementation.  I'll squash the two patches
together and add more tests.

>> +                          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;
>> +}
> 
> This new limitation of the neighbor_responder mode needs to be documented
> in ovn-nb.xml for the options:neighbor_responder.
> 

You're right, I'll do that.

> Also curious why can't we support responders for templated LBs? Can the
> relevant lflows be generated with the template vars used for the LB VIPs as
> well?
> 

It seemed quite complex to implement correctly.  That's because in the
"neighbor_responder=reachable" mode we need to reply only to ARPs for
VIPs that are part of router owned subnets.  We currently implement that
in northd, by checking the router port subnets.  But we only have access
to the real VIP value quite late, after template instantiation, in
ovn-controller.

So I decided to not support neighbor_responder=reachable for now.  For
the use case I have in mind (node-port load balancers in ovn-kubernetes)
it doesn't matter because the VIP is the node IP for which we already
have ARP responder flows in place.  For the more generic case, it's up
to the CMS to explicitly request ARP responses (neighbor_responder=all)
or suppress all of them (neighbor_responder=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)
>> +{
>> +    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);
> 
> Shouldn't we check the options:use_template before parsing?
> 

That's better indeed!

>> +        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.
> 
> It would be better to mention what are template variables, with a reference
> to the chassis_template_var table. An simple example would be even better.
> 

Sure, I'll do that.

>> +      </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.
> 
> It would be better to state the keywords "ipv4" & "ipv6", and point out
> that this is *required* if and only if the options:template is "true".
> 

Better indeed, I'll take care of it.

> Thanks,
> Han
> 

Thanks,
Dumitru

>> +      </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 },
>>
>
Han Zhou Oct. 21, 2022, 10:30 p.m. UTC | #4
On Fri, Oct 21, 2022 at 1:00 AM Dumitru Ceara <dceara@redhat.com> wrote:
>
> On 10/21/22 07:47, Han Zhou wrote:
> > On Fri, Sep 30, 2022 at 7:02 AM Dumitru Ceara <dceara@redhat.com> wrote:
> >>
> >> 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 <dceara@redhat.com>
> >
> > Hi Dumitru, sorry for the delayed response. Please see my comments
below.
> >
>
> Hi Han,
>
> No worries, thanks for the review!
>
> >> ---
> >>  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,
> >
> > template_vars is added as an input, but I don't see I-P for
template_vars
> > for hairpin flows in this patch, which makes this patch incomplete.
> > I see that it is taken care by the next patch. So they should be either
> > combined or switch the patch order, so that each incremental patch is
> > correct.
> > In addition, some basic tests may be added to this patch (which should
> > discover problems of hairpin flows when template vars are updated, due
to
> > the missing I-P).
> >
>
> I initially wanted to keep things simpler but you're right, now at patch
> 5/8 we have an incomplete implementation.  I'll squash the two patches
> together and add more tests.
>
> >> +                          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;
> >> +}
> >
> > This new limitation of the neighbor_responder mode needs to be
documented
> > in ovn-nb.xml for the options:neighbor_responder.
> >
>
> You're right, I'll do that.
>
> > Also curious why can't we support responders for templated LBs? Can the
> > relevant lflows be generated with the template vars used for the LB
VIPs as
> > well?
> >
>
> It seemed quite complex to implement correctly.  That's because in the
> "neighbor_responder=reachable" mode we need to reply only to ARPs for
> VIPs that are part of router owned subnets.  We currently implement that
> in northd, by checking the router port subnets.  But we only have access
> to the real VIP value quite late, after template instantiation, in
> ovn-controller.
>
> So I decided to not support neighbor_responder=reachable for now.  For
> the use case I have in mind (node-port load balancers in ovn-kubernetes)
> it doesn't matter because the VIP is the node IP for which we already
> have ARP responder flows in place.  For the more generic case, it's up
> to the CMS to explicitly request ARP responses (neighbor_responder=all)
> or suppress all of them (neighbor_responder=none).
>
Make sense and I agree with you that it isn't worth the complexity.

Thanks,
Han

> >> +
> >> +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);
> >
> > Shouldn't we check the options:use_template before parsing?
> >
>
> That's better indeed!
>
> >> +        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.
> >
> > It would be better to mention what are template variables, with a
reference
> > to the chassis_template_var table. An simple example would be even
better.
> >
>
> Sure, I'll do that.
>
> >> +      </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.
> >
> > It would be better to state the keywords "ipv4" & "ipv6", and point out
> > that this is *required* if and only if the options:template is "true".
> >
>
> Better indeed, I'll take care of it.
>
> > Thanks,
> > Han
> >
>
> Thanks,
> Dumitru
>
> >> +      </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 },
> >>
> >
>
diff mbox series

Patch

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 },