Hi Dumitru, I have some findings down below.

In general, I understand why the use of template variables is so restrictive, but I think there are some adjustments that could be made in future versions of OVN to make this more user-friendly and more generally applicable.

1) It would be nice if the IP address and port could be combined into a single template variable. 2) It would be nice if backends could all be combined into a single template variable. 3) It would be nice if templates and explicit addresses could be combined. For instance, it could be useful for an explicit VIP to be used but with templated backends.

The issue with the current setup is that for templated load balancers to be useful, the load balancers have to have the same number of VIPs and backends. If there is any variability across load balancers, then it requires creating separate load balancers. This is great for the ovn-kubernetes NodePort scenario.

To be perfectly clear, the items I suggest above are NOT blocking this changeset from being accepted. However, I think it's worth noting this now so that we have a path forward with how to improve.

On 11/4/22 18:11, 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.

Template load balancers don't support the "reachable" neighbor-responder
mode.  Instead the CMS can explicitly configure the responder mode to
either "all" or "none".

To properly handle template updates in ovn-controller we also add a
Chassis_Template_Var <- LB reference in ovn-controller.  This way, when
a Chassis_Template_Var changes value all load balancers that refer to
it will also get updated.

Signed-off-by: Dumitru Ceara <dce...@redhat.com>
---
V2:
- Fix GCC build due to missing explicit return.
- Fix ls_in_pre_stateful flows due to using wrong lb field.
- Use new lexer_parse_template_string().
- Changed lb_handle_changed_ref() signature to return bool.
- Update documentation with info about responder mode=none, LB template
   supported formats, lb explicit address family requirements.
- Squashed the template LB patches into a single one
- Added more tests.
- Squashed the system tests patch into this one.
---
  controller/lflow.c          |  118 +++++++++--
  controller/lflow.h          |    7 +
  controller/ovn-controller.c |   67 +++++-
  lib/lb.c                    |  457 ++++++++++++++++++++++++++++++++++++++-----
  lib/lb.h                    |   40 +++-
  lib/ovn-util.c              |    3
  northd/northd.c             |   89 ++++----
  ovn-nb.xml                  |   53 +++++
  tests/ovn-nbctl.at          |   23 +-
  tests/ovn-northd.at         |    7 +
  tests/ovn.at                |  131 ++++++++++++
  tests/system-ovn.at         |  183 +++++++++++++++++
  utilities/ovn-nbctl.c       |  122 ++++++-----
  13 files changed, 1071 insertions(+), 229 deletions(-)

diff --git a/controller/lflow.c b/controller/lflow.c
index fc4371d0df..7f880bd62b 100644
--- a/controller/lflow.c
+++ b/controller/lflow.c
@@ -97,6 +97,15 @@ consider_logical_flow(const struct sbrec_logical_flow *lflow,
                        struct lflow_ctx_in *l_ctx_in,
                        struct lflow_ctx_out *l_ctx_out);
+static void
+consider_lb_hairpin_flows(struct objdep_mgr *mgr,
+                          const struct sbrec_load_balancer *sbrec_lb,
+                          const struct hmap *local_datapaths,
+                          const struct smap *template_vars,
+                          bool use_ct_mark,
+                          struct ovn_desired_flow_table *flow_table,
+                          struct simap *ids);
+
  static void add_port_sec_flows(const struct shash *binding_lports,
                                 const struct sbrec_chassis *,
                                 struct ovn_desired_flow_table *);
@@ -223,7 +232,7 @@ lflow_handle_changed_flows(struct lflow_ctx_in *l_ctx_in,
          UUIDSET_INITIALIZER(&flood_remove_nodes);
      SBREC_LOGICAL_FLOW_TABLE_FOR_EACH_TRACKED (lflow,
                                                 l_ctx_in->logical_flow_table) {
-        if (uuidset_find(l_ctx_out->lflows_processed, &lflow->header_.uuid)) {
+        if (uuidset_find(l_ctx_out->objs_processed, &lflow->header_.uuid)) {
              VLOG_DBG("lflow "UUID_FMT"has been processed, skip.",
                       UUID_ARGS(&lflow->header_.uuid));
              continue;
@@ -253,14 +262,14 @@ lflow_handle_changed_flows(struct lflow_ctx_in *l_ctx_in,
                       UUID_ARGS(&lflow->header_.uuid));
/* For the extra lflows that need to be reprocessed because of the
-             * flood remove, remove it from lflows_processed. */
+             * flood remove, remove it from objs_processed. */
              struct uuidset_node *unode =
-                uuidset_find(l_ctx_out->lflows_processed,
+                uuidset_find(l_ctx_out->objs_processed,
                               &lflow->header_.uuid);
              if (unode) {
                  VLOG_DBG("lflow "UUID_FMT"has been processed, now reprocess.",
                           UUID_ARGS(&lflow->header_.uuid));
-                uuidset_delete(l_ctx_out->lflows_processed, unode);
+                uuidset_delete(l_ctx_out->objs_processed, unode);
              }
consider_logical_flow(lflow, false, l_ctx_in, l_ctx_out);
@@ -677,7 +686,7 @@ lflow_handle_addr_set_update(const char *as_name,
      struct object_to_resources_list_node *resource_list_node;
      RESOURCE_FOR_EACH_OBJ (resource_list_node, resource_node) {
          const struct uuid *obj_uuid = &resource_list_node->obj_uuid;
-        if (uuidset_find(l_ctx_out->lflows_processed, obj_uuid)) {
+        if (uuidset_find(l_ctx_out->objs_processed, obj_uuid)) {
              VLOG_DBG("lflow "UUID_FMT"has been processed, skip.",
                       UUID_ARGS(obj_uuid));
              continue;
@@ -767,13 +776,13 @@ lflow_handle_changed_ref(enum objdep_type type, const 
char *res_name,
          }
/* For the extra lflows that need to be reprocessed because of the
-         * flood remove, remove it from lflows_processed. */
+         * flood remove, remove it from objs_processed. */
          struct uuidset_node *unode =
-            uuidset_find(l_ctx_out->lflows_processed, &lflow->header_.uuid);
+            uuidset_find(l_ctx_out->objs_processed, &lflow->header_.uuid);
          if (unode) {
              VLOG_DBG("lflow "UUID_FMT"has been processed, now reprocess.",
                       UUID_ARGS(&lflow->header_.uuid));
-            uuidset_delete(l_ctx_out->lflows_processed, unode);
+            uuidset_delete(l_ctx_out->objs_processed, unode);
          }
consider_logical_flow(lflow, false, l_ctx_in, l_ctx_out);
@@ -782,6 +791,43 @@ lflow_handle_changed_ref(enum objdep_type type, const char 
*res_name,
      return true;
  }
+bool
+lb_handle_changed_ref(enum objdep_type type, const char *res_name,
+                      struct ovs_list *objs_todo,
+                      const void *in_arg, void *out_arg)
+{
+    struct lflow_ctx_in *l_ctx_in = CONST_CAST(struct lflow_ctx_in *, in_arg);
+    struct lflow_ctx_out *l_ctx_out = out_arg;
+
+    struct object_to_resources_list_node *resource_lb_uuid;
+    LIST_FOR_EACH_POP (resource_lb_uuid, list_node, objs_todo) {
+        VLOG_DBG("Reprocess LB "UUID_FMT" for resource type: %s, name: %s",
+                 UUID_ARGS(&resource_lb_uuid->obj_uuid),
+                 objdep_type_name(type), res_name);
+
+        const struct sbrec_load_balancer *lb =
+            sbrec_load_balancer_table_get_for_uuid(
+                l_ctx_in->lb_table, &resource_lb_uuid->obj_uuid);
+        if (!lb) {
+            VLOG_DBG("Failed to find LB "UUID_FMT" referred by: %s",
+                     UUID_ARGS(&resource_lb_uuid->obj_uuid), res_name);
+        } else {
+            ofctrl_remove_flows(l_ctx_out->flow_table,
+                                &resource_lb_uuid->obj_uuid);
+
+            consider_lb_hairpin_flows(l_ctx_out->lb_deps_mgr, 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);
+        }
+
+        free(resource_lb_uuid);
+    }
+    return true;
+}
+
  static void
  lflow_parse_ctrl_meter(const struct sbrec_logical_flow *lflow,
                         struct ovn_extend_table *meter_table,
@@ -1263,9 +1309,9 @@ consider_logical_flow(const struct sbrec_logical_flow 
*lflow,
COVERAGE_INC(consider_logical_flow);
      if (!is_recompute) {
-        ovs_assert(!uuidset_find(l_ctx_out->lflows_processed,
+        ovs_assert(!uuidset_find(l_ctx_out->objs_processed,
                                   &lflow->header_.uuid));
-        uuidset_insert(l_ctx_out->lflows_processed, &lflow->header_.uuid);
+        uuidset_insert(l_ctx_out->objs_processed, &lflow->header_.uuid);
      }
if (dp) {
@@ -2005,8 +2051,11 @@ 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,
+consider_lb_hairpin_flows(struct objdep_mgr *mgr,
+                          const struct sbrec_load_balancer *sbrec_lb,
+                          const struct hmap *local_datapaths,
+                          const struct smap *template_vars,
+                          bool use_ct_mark,
                            struct ovn_desired_flow_table *flow_table,
                            struct simap *ids)
  {
@@ -2042,7 +2091,9 @@ consider_lb_hairpin_flows(const struct 
sbrec_load_balancer *sbrec_lb,
          return;
      }
- struct ovn_controller_lb *lb = ovn_controller_lb_create(sbrec_lb);
+    struct sset template_vars_ref = SSET_INITIALIZER(&template_vars_ref);
+    struct ovn_controller_lb *lb =
+        ovn_controller_lb_create(sbrec_lb, template_vars, &template_vars_ref);
      uint8_t lb_proto = IPPROTO_TCP;
      if (lb->slb->protocol && lb->slb->protocol[0]) {
          if (!strcmp(lb->slb->protocol, "udp")) {
@@ -2052,6 +2103,11 @@ consider_lb_hairpin_flows(const struct 
sbrec_load_balancer *sbrec_lb,
          }
      }
+ const char *tv_name;
+    SSET_FOR_EACH (tv_name, &template_vars_ref) {
+        objdep_mgr_add(mgr, OBJDEP_TYPE_TEMPLATE, tv_name,
+                       &sbrec_lb->header_.uuid);
+    }
      for (i = 0; i < lb->n_vips; i++) {
          struct ovn_lb_vip *lb_vip = &lb->vips[i];
@@ -2066,13 +2122,17 @@ consider_lb_hairpin_flows(const struct sbrec_load_balancer *sbrec_lb,
      add_lb_ct_snat_hairpin_flows(lb, id, lb_proto, flow_table);
ovn_controller_lb_destroy(lb);
+    sset_destroy(&template_vars_ref);
  }
/* Adds OpenFlow flows to flow tables for each Load balancer VIPs and
   * 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,
+add_lb_hairpin_flows(struct objdep_mgr *mgr,
+                     const struct sbrec_load_balancer_table *lb_table,
+                     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)
@@ -2095,8 +2155,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(mgr, lb, local_datapaths, template_vars,
+                                  use_ct_mark, flow_table, ids);
      }
  }
@@ -2232,7 +2292,9 @@ lflow_run(struct lflow_ctx_in *l_ctx_in, struct lflow_ctx_out *l_ctx_out)
                         l_ctx_in->static_mac_binding_table,
                         l_ctx_in->local_datapaths,
                         l_ctx_out->flow_table);
-    add_lb_hairpin_flows(l_ctx_in->lb_table, l_ctx_in->local_datapaths,
+    add_lb_hairpin_flows(l_ctx_out->lb_deps_mgr, 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,
@@ -2283,10 +2345,10 @@ lflow_add_flows_for_datapath(const struct 
sbrec_datapath_binding *dp,
      const struct sbrec_logical_flow *lflow;
      SBREC_LOGICAL_FLOW_FOR_EACH_EQUAL (
          lflow, lf_row, l_ctx_in->sbrec_logical_flow_by_logical_datapath) {
-        if (uuidset_find(l_ctx_out->lflows_processed, &lflow->header_.uuid)) {
+        if (uuidset_find(l_ctx_out->objs_processed, &lflow->header_.uuid)) {
              continue;
          }
-        uuidset_insert(l_ctx_out->lflows_processed, &lflow->header_.uuid);
+        uuidset_insert(l_ctx_out->objs_processed, &lflow->header_.uuid);
          consider_logical_flow__(lflow, dp, l_ctx_in, l_ctx_out);
      }
      sbrec_logical_flow_index_destroy_row(lf_row);
@@ -2311,7 +2373,7 @@ lflow_add_flows_for_datapath(const struct 
sbrec_datapath_binding *dp,
          sbrec_logical_flow_index_set_logical_dp_group(lf_row, ldpg);
          SBREC_LOGICAL_FLOW_FOR_EACH_EQUAL (
              lflow, lf_row, l_ctx_in->sbrec_logical_flow_by_logical_dp_group) {
-            if (uuidset_find(l_ctx_out->lflows_processed,
+            if (uuidset_find(l_ctx_out->objs_processed,
                               &lflow->header_.uuid)) {
                  continue;
              }
@@ -2363,7 +2425,9 @@ lflow_add_flows_for_datapath(const struct 
sbrec_datapath_binding *dp,
      /* Add load balancer hairpin flows if the datapath has any load balancers
       * associated. */
      for (size_t i = 0; i < n_dp_lbs; i++) {
-        consider_lb_hairpin_flows(dp_lbs[i], l_ctx_in->local_datapaths,
+        consider_lb_hairpin_flows(l_ctx_out->lb_deps_mgr, 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);
@@ -2385,7 +2449,7 @@ lflow_handle_flows_for_lport(const struct 
sbrec_port_binding *pb,
                                    OBJDEP_TYPE_PORTBINDING,
                                    pb->logical_port,
                                    lflow_handle_changed_ref,
-                                  l_ctx_out->lflows_processed,
+                                  l_ctx_out->objs_processed,
                                    l_ctx_in, l_ctx_out, &changed)) {
          return false;
      }
@@ -2424,7 +2488,7 @@ lflow_handle_changed_port_bindings(struct lflow_ctx_in 
*l_ctx_in,
                                        OBJDEP_TYPE_PORTBINDING,
                                        pb->logical_port,
                                        lflow_handle_changed_ref,
-                                      l_ctx_out->lflows_processed,
+                                      l_ctx_out->objs_processed,
                                        l_ctx_in, l_ctx_out, &changed)) {
              ret = false;
              break;
@@ -2451,7 +2515,7 @@ lflow_handle_changed_mc_groups(struct lflow_ctx_in 
*l_ctx_in,
          if (!objdep_mgr_handle_change(l_ctx_out->lflow_deps_mgr,
                                        OBJDEP_TYPE_MC_GROUP, ds_cstr(&mg_key),
                                        lflow_handle_changed_ref,
-                                      l_ctx_out->lflows_processed,
+                                      l_ctx_out->objs_processed,
                                        l_ctx_in, l_ctx_out, &changed)) {
              ret = false;
              break;
@@ -2505,7 +2569,9 @@ 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,
+        consider_lb_hairpin_flows(l_ctx_out->lb_deps_mgr, 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/controller/lflow.h b/controller/lflow.h
index 9a7079f99e..069b5d795f 100644
--- a/controller/lflow.h
+++ b/controller/lflow.h
@@ -121,9 +121,10 @@ struct lflow_ctx_out {
      struct ovn_extend_table *group_table;
      struct ovn_extend_table *meter_table;
      struct objdep_mgr *lflow_deps_mgr;
+    struct objdep_mgr *lb_deps_mgr;
      struct lflow_cache *lflow_cache;
      struct conj_ids *conj_ids;
-    struct uuidset *lflows_processed;
+    struct uuidset *objs_processed;
      struct simap *hairpin_lb_ids;
      struct id_pool *hairpin_id_pool;
  };
@@ -173,4 +174,8 @@ bool lflow_handle_changed_mc_groups(struct lflow_ctx_in *,
                                      struct lflow_ctx_out *);
  bool lflow_handle_changed_port_bindings(struct lflow_ctx_in *,
                                          struct lflow_ctx_out *);
+
+bool lb_handle_changed_ref(enum objdep_type type, const char *res_name,
+                           struct ovs_list *objs_todo,
+                           const void *in_arg, void *out_arg);
  #endif /* controller/lflow.h */
diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
index c374bd0f33..f0be783ee6 100644
--- a/controller/ovn-controller.c
+++ b/controller/ovn-controller.c
@@ -2769,13 +2769,15 @@ struct ed_type_lflow_output {
      struct ovn_extend_table meter_table;
      /* lflow <-> resource cross reference */
      struct objdep_mgr lflow_deps_mgr;;
+    /* load balancer <-> resource cross reference */
+    struct objdep_mgr lb_deps_mgr;
      /* conjunciton ID usage information of lflows */
      struct conj_ids conj_ids;
- /* lflows processed in the current engine execution.
+    /* objects (lflows and lbs) processed in the current engine execution.
       * Cleared by en_lflow_output_clear_tracked_data before each engine
       * execution. */
-    struct uuidset lflows_processed;
+    struct uuidset objs_processed;
/* Data which is persistent and not cleared during
       * full recompute. */
@@ -2932,8 +2934,9 @@ init_lflow_ctx(struct engine_node *node,
      l_ctx_out->group_table = &fo->group_table;
      l_ctx_out->meter_table = &fo->meter_table;
      l_ctx_out->lflow_deps_mgr = &fo->lflow_deps_mgr;
+    l_ctx_out->lb_deps_mgr = &fo->lb_deps_mgr;
      l_ctx_out->conj_ids = &fo->conj_ids;
-    l_ctx_out->lflows_processed = &fo->lflows_processed;
+    l_ctx_out->objs_processed = &fo->objs_processed;
      l_ctx_out->lflow_cache = fo->pd.lflow_cache;
      l_ctx_out->hairpin_id_pool = fo->hd.pool;
      l_ctx_out->hairpin_lb_ids = &fo->hd.ids;
@@ -2948,8 +2951,9 @@ en_lflow_output_init(struct engine_node *node OVS_UNUSED,
      ovn_extend_table_init(&data->group_table);
      ovn_extend_table_init(&data->meter_table);
      objdep_mgr_init(&data->lflow_deps_mgr);
+    objdep_mgr_init(&data->lb_deps_mgr);
      lflow_conj_ids_init(&data->conj_ids);
-    uuidset_init(&data->lflows_processed);
+    uuidset_init(&data->objs_processed);
      simap_init(&data->hd.ids);
      data->hd.pool = id_pool_create(1, UINT32_MAX - 1);
      nd_ra_opts_init(&data->nd_ra_opts);
@@ -2961,7 +2965,7 @@ static void
  en_lflow_output_clear_tracked_data(void *data)
  {
      struct ed_type_lflow_output *flow_output_data = data;
-    uuidset_clear(&flow_output_data->lflows_processed);
+    uuidset_clear(&flow_output_data->objs_processed);
  }
static void
@@ -2972,8 +2976,9 @@ en_lflow_output_cleanup(void *data)
      ovn_extend_table_destroy(&flow_output_data->group_table);
      ovn_extend_table_destroy(&flow_output_data->meter_table);
      objdep_mgr_destroy(&flow_output_data->lflow_deps_mgr);
+    objdep_mgr_destroy(&flow_output_data->lb_deps_mgr);
      lflow_conj_ids_destroy(&flow_output_data->conj_ids);
-    uuidset_destroy(&flow_output_data->lflows_processed);
+    uuidset_destroy(&flow_output_data->objs_processed);
      lflow_cache_destroy(flow_output_data->pd.lflow_cache);
      simap_destroy(&flow_output_data->hd.ids);
      id_pool_destroy(flow_output_data->hd.pool);
@@ -3008,6 +3013,7 @@ en_lflow_output_run(struct engine_node *node, void *data)
      struct ovn_extend_table *group_table = &fo->group_table;
      struct ovn_extend_table *meter_table = &fo->meter_table;
      struct objdep_mgr *lflow_deps_mgr = &fo->lflow_deps_mgr;
+    struct objdep_mgr *lb_deps_mgr = &fo->lb_deps_mgr;
static bool first_run = true;
      if (first_run) {
@@ -3017,6 +3023,7 @@ en_lflow_output_run(struct engine_node *node, void *data)
          ovn_extend_table_clear(group_table, false /* desired */);
          ovn_extend_table_clear(meter_table, false /* desired */);
          objdep_mgr_clear(lflow_deps_mgr);
+        objdep_mgr_clear(lb_deps_mgr);
          lflow_conj_ids_clear(&fo->conj_ids);
      }
@@ -3150,7 +3157,7 @@ lflow_output_addr_sets_handler(struct engine_node *node, void *data)
          if (!objdep_mgr_handle_change(l_ctx_out.lflow_deps_mgr,
                                        OBJDEP_TYPE_ADDRSET, ref_name,
                                        lflow_handle_changed_ref,
-                                      l_ctx_out.lflows_processed,
+                                      l_ctx_out.objs_processed,
                                        &l_ctx_in, &l_ctx_out, &changed)) {
              return false;
          }
@@ -3169,7 +3176,7 @@ lflow_output_addr_sets_handler(struct engine_node *node, 
void *data)
                                            OBJDEP_TYPE_ADDRSET,
                                            shash_node->name,
                                            lflow_handle_changed_ref,
-                                          l_ctx_out.lflows_processed,
+                                          l_ctx_out.objs_processed,
                                            &l_ctx_in, &l_ctx_out, &changed)) {
                  return false;
              }
@@ -3182,7 +3189,7 @@ lflow_output_addr_sets_handler(struct engine_node *node, 
void *data)
          if (!objdep_mgr_handle_change(l_ctx_out.lflow_deps_mgr,
                                        OBJDEP_TYPE_ADDRSET, ref_name,
                                        lflow_handle_changed_ref,
-                                      l_ctx_out.lflows_processed,
+                                      l_ctx_out.objs_processed,
                                        &l_ctx_in, &l_ctx_out, &changed)) {
              return false;
          }
@@ -3217,7 +3224,7 @@ lflow_output_port_groups_handler(struct engine_node 
*node, void *data)
          if (!objdep_mgr_handle_change(l_ctx_out.lflow_deps_mgr,
                                        OBJDEP_TYPE_PORTGROUP, ref_name,
                                        lflow_handle_changed_ref,
-                                      l_ctx_out.lflows_processed,
+                                      l_ctx_out.objs_processed,
                                        &l_ctx_in, &l_ctx_out, &changed)) {
              return false;
          }
@@ -3229,7 +3236,7 @@ lflow_output_port_groups_handler(struct engine_node 
*node, void *data)
          if (!objdep_mgr_handle_change(l_ctx_out.lflow_deps_mgr,
                                        OBJDEP_TYPE_PORTGROUP, ref_name,
                                        lflow_handle_changed_ref,
-                                      l_ctx_out.lflows_processed,
+                                      l_ctx_out.objs_processed,
                                        &l_ctx_in, &l_ctx_out, &changed)) {
              return false;
          }
@@ -3241,7 +3248,7 @@ lflow_output_port_groups_handler(struct engine_node 
*node, void *data)
          if (!objdep_mgr_handle_change(l_ctx_out.lflow_deps_mgr,
                                        OBJDEP_TYPE_PORTGROUP, ref_name,
                                        lflow_handle_changed_ref,
-                                      l_ctx_out.lflows_processed,
+                                      l_ctx_out.objs_processed,
                                        &l_ctx_in, &l_ctx_out, &changed)) {
              return false;
          }
@@ -3275,7 +3282,17 @@ lflow_output_template_vars_handler(struct engine_node 
*node, void *data)
          if (!objdep_mgr_handle_change(l_ctx_out.lflow_deps_mgr,
                                        OBJDEP_TYPE_TEMPLATE,
                                        res_name, lflow_handle_changed_ref,
-                                      l_ctx_out.lflows_processed,
+                                      l_ctx_out.objs_processed,
+                                      &l_ctx_in, &l_ctx_out, &changed)) {
+            return false;
+        }
+        if (changed) {
+            engine_set_node_state(node, EN_UPDATED);
+        }
+        if (!objdep_mgr_handle_change(l_ctx_out.lb_deps_mgr,
+                                      OBJDEP_TYPE_TEMPLATE,
+                                      res_name, lb_handle_changed_ref,
+                                      l_ctx_out.objs_processed,
                                        &l_ctx_in, &l_ctx_out, &changed)) {
              return false;
          }
@@ -3287,7 +3304,17 @@ lflow_output_template_vars_handler(struct engine_node 
*node, void *data)
          if (!objdep_mgr_handle_change(l_ctx_out.lflow_deps_mgr,
                                        OBJDEP_TYPE_TEMPLATE,
                                        res_name, lflow_handle_changed_ref,
-                                      l_ctx_out.lflows_processed,
+                                      l_ctx_out.objs_processed,
+                                      &l_ctx_in, &l_ctx_out, &changed)) {
+            return false;
+        }
+        if (changed) {
+            engine_set_node_state(node, EN_UPDATED);
+        }
+        if (!objdep_mgr_handle_change(l_ctx_out.lb_deps_mgr,
+                                      OBJDEP_TYPE_TEMPLATE,
+                                      res_name, lb_handle_changed_ref,
+                                      l_ctx_out.objs_processed,
                                        &l_ctx_in, &l_ctx_out, &changed)) {
              return false;
          }
@@ -3299,7 +3326,17 @@ lflow_output_template_vars_handler(struct engine_node 
*node, void *data)
          if (!objdep_mgr_handle_change(l_ctx_out.lflow_deps_mgr,
                                        OBJDEP_TYPE_TEMPLATE,
                                        res_name, lflow_handle_changed_ref,
-                                      l_ctx_out.lflows_processed,
+                                      l_ctx_out.objs_processed,
+                                      &l_ctx_in, &l_ctx_out, &changed)) {
+            return false;
+        }
+        if (changed) {
+            engine_set_node_state(node, EN_UPDATED);
+        }
+        if (!objdep_mgr_handle_change(l_ctx_out.lb_deps_mgr,
+                                      OBJDEP_TYPE_TEMPLATE,
+                                      res_name, lb_handle_changed_ref,
+                                      l_ctx_out.objs_processed,
                                        &l_ctx_in, &l_ctx_out, &changed)) {
              return false;
          }
diff --git a/lib/lb.c b/lib/lb.c
index ab5de38a89..caeb9a8be7 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,94 +82,297 @@ 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)

The n_backends parameter doesn't make much sense to me. The only value that ever makes sense to pass in is &lb_vip->n_backends . If you passed a different pointer in, then it would result in backends being placed in arbitrary places in the lb_vip->backends array, and lb_vip->n_backends would not have the proper value set.

Instead, just leave this parameter off and update lb_vip->n_backends directly in this function.

  {
-    int addr_family;
-
-    if (!ip_address_and_port_from_lb_key(lb_key, &lb_vip->vip_str,
-                                         &lb_vip->vip, &lb_vip->vip_port,
-                                         &addr_family)) {
-        return false;
-    }
-
-    /* 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->ip, &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;
          }
- n_backends++;
+        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;
+        (*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, &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;
+
+    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)

The same critique I had for obn_lb_backends_init_explicit() holds here. Leave off the n_backends parameter and update lb_vip->n_backends in this function instead.

+{
+    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
@@ -189,12 +403,112 @@ 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;
+    }
+    return false;
+}
+
+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";
@@ -202,12 +516,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);

This function call is referencing lb->template, but lb->template is not set until the next line. This means you're passing "false" unconditionally currently.

+
      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;
@@ -217,13 +535,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 {
@@ -371,9 +695,12 @@ 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 sset *template_vars_ref)
  {
      struct ovn_controller_lb *lb = xzalloc(sizeof *lb);
+    bool template = smap_get_bool(&sbrec_lb->options, "template", false);
lb->slb = sbrec_lb;
      lb->n_vips = smap_count(&sbrec_lb->vips);
@@ -385,10 +712,28 @@ 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_expanded_s = NULL;
+        const char *key_s = template
+                            ? lexer_parse_template_string(node->key,
+                                                          template_vars,
+                                                          template_vars_ref,
+                                                          &key_expanded_s)
+                            : node->key;
+        char *value_expanded_s = NULL;
+        const char *value_s = template
+                              ? lexer_parse_template_string(node->value,
+                                                            template_vars,
+                                                            template_vars_ref,
+                                                            &value_expanded_s)
+                              : node->value;
+        char *error = ovn_lb_vip_init_explicit(lb_vip, key_s, value_s);
+        if (error) {
+            free(error);
+        } else {
+            n_vips++;
          }
-        n_vips++;
+        free(key_expanded_s);
+        free(value_expanded_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 c1aadd6dd5..42bc5afecb 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.

The comment says "to be used in ovn-controller" but I'm pretty sure it's supposed to be "to be used in ovn-northd."

+                          */
      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. */
@@ -173,7 +187,17 @@ 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,
+    struct sset *template_vars_ref);
  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 5dca727146..91b572feb3 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -793,9 +793,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;
          memset(ip, 0, sizeof(*ip));
          *port = 0;
diff --git a/northd/northd.c b/northd/northd.c
index 170b4f95c8..e12b91a3a6 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -3720,6 +3720,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];
@@ -4036,12 +4040,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);
@@ -4053,8 +4064,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;
@@ -5814,16 +5826,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,
@@ -5834,7 +5846,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;
@@ -6890,7 +6902,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);
@@ -6901,7 +6913,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")) {
@@ -6914,14 +6926,14 @@ 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");
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;
@@ -6953,24 +6965,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);
@@ -6988,8 +6988,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;
          }
@@ -9989,7 +9990,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 {
@@ -10005,14 +10006,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));
@@ -10021,7 +10022,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";
@@ -10039,9 +10040,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, ") || ");
          }
@@ -10054,9 +10055,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 =
@@ -10296,7 +10297,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);
@@ -10306,7 +10307,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 45b75e66df..af3ac261bc 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -1905,8 +1905,57 @@
          is applied reply to ARP/neighbor discovery requests for all VIPs
          of the load balancer.  If set to <code>reachable</code>, then routers
          on which the load balancer is applied reply to ARP/neighbor discovery
-        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>.
+        requests only for VIPs that are part of a router's subnet.  If set to
+        <code>none</code>, then routers on which the load balancer is applied
+        never reply to ARP/neighbor discovery requests for any of the load
+        balancer VIPs. Load balancers with <code>options:template=true</code>
+        do not support <code>reachable</code> as a valid mode.  The default
+        value of this option, if not specified, is <code>reachable</code> for
+        regular load balancers and <code>none</code> for template load
+        balancers.
+      </column>
+
+      <column name="options" key="template">
+        <p>
+          Option to be set to <code>true</code>, if the load balancer is a
+          template.  In this the load balancer VIPs and/or backends may be
+          using <ref table="Chassis_Template_Var"/> in their definition.

This sentence reads a bit odd to me. I think that it would read better if:

* remove "In this" from the beginning of the sentence.
* Change "and/or" to just "and"
* Change "may" to "must"
* Change "definition" to "definitions"

I think with those changes,  it fixes the grammar and is more accurate.

+        </p>
+
+        <p>
+          Load balancer template VIP supported formats are:
+        </p>
+        <pre>
+^VIP_VAR[:^PORT_VAR|:port]
+        </pre>
+
+        <p>
+          where <code>VIP_VAR</code> and <code>PORT_VAR</code> are names of
+        <ref table="Chassis_Template_Var"/> records.

Just to drive the point home, I would make a note that VIP and PORT cannot be combined into a single template variable. In other words, you can't have a load balancer VIP that is "^MYVIP" and define "^MYVIP" in the Chassis_Template_Var table as expanding to "10.0.0.1:8080".

+        </p>
+
+        <p>
+          Load balancer template backend supported formats are:
+        </p>
+        <pre>
+^BACKEND_VAR1[:^PORT_VAR1|:port],^BACKEND_VAR2[:^PORT_VAR2|:port]
+
+or
+
+^BACKENDS_VAR1,^BACKENDS_VAR2
+        </pre>
+        <p>
+          where <code>BACKEND_VAR1</code>, <code>PORT_VAR1</code>,
+          <code>BACKEND_VAR2</code>, <code>PORT_VAR2</code>,
+          <code>BACKENDS_VAR1</code> and <code>BACKENDS_VAR2</code> are names
+          of <ref table="Chassis_Template_Var"/> records.
+        </p>
+      </column>
+
+      <column name="options" key="address-family">
+        Address family used by the load balancer.  Supported values are
+        <code>ipv4</code> and <code>ipv6</code>.  This value is used and is
+        mandatory for load balancer with <code>options:template=true</code>.

I think it needs to be made more clear that the address-family is *only* used for templated load balancers. For explicit load balancers, setting the address-family has no effect.

        </column>
      </group>
    </table>
diff --git a/tests/ovn-nbctl.at b/tests/ovn-nbctl.at
index 4d480e3573..9da7c26b31 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 c7112b805d..d088961819 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/tests/ovn.at b/tests/ovn.at
index 2a59235c59..45241d7e20 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -33053,3 +33053,134 @@ AT_CHECK([ovs-ofctl dump-flows br-int | grep 
'42\.42\.42\.42'], [1], [])
  OVN_CLEANUP([hv1])
  AT_CLEANUP
  ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([Load balancers with Chassis_Template_Var references])
+AT_KEYWORDS([templates])
+ovn_start
+net_add n1
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+
+check ovn-nbctl ls-add sw
+
+dnl Use --wait=sb to ensure lsp1 getting a tunnel_key before lsp2.
+check ovn-nbctl --wait=sb lsp-add sw lsp1
+check ovn-nbctl --wait=sb lsp-add sw lsp2
+
+AT_CHECK([ovn-nbctl create Chassis_Template_Var chassis=hv1], [0], [ignore])
+
+dnl Create a few LBs that use "uninstantiated" templates.
+check ovn-nbctl --template lb-add lb-test1 "^VIP1:^VPORT1" "^BACKENDS1" tcp
+check ovn-nbctl --template lb-add lb-test2 "^VIP2:^VPORT2" 
"^BACKENDS21,^BACKENDS22" tcp
+check ovn-nbctl --template lb-add lb-test3 "^VIP3:^VPORT3" 
"^BACKENDS31:^BPORT1,^BACKENDS32:^BPORT2" tcp
+check ovn-nbctl ls-lb-add sw lb-test1
+check ovn-nbctl ls-lb-add sw lb-test2
+check ovn-nbctl ls-lb-add sw lb-test3
+
+check ovs-vsctl add-port br-int p1 -- set interface p1 
external_ids:iface-id=lsp1
+check ovs-vsctl add-port br-int p2 -- set interface p2 
external_ids:iface-id=lsp2
+
+wait_for_ports_up
+ovn-nbctl --wait=hv sync
+
+dnl Ensure the LBs are not translated to OpenFlow.
+as hv1
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat'], [1], [])
+
+dnl Create Chassis_Template_Var mappings.
+check ovn-nbctl --wait=hv set Chassis_Template_Var hv1 \
+    variables:VIP1='43.43.43.1' variables:VPORT1='4301' \
+    variables:BACKENDS1='85.85.85.1:8501' \
+    variables:VIP2='43.43.43.2' variables:VPORT2='4302' \
+    variables:BACKENDS21='85.85.85.21:8502' \
+    variables:BACKENDS22='85.85.85.22:8502' \
+    variables:VIP3='43.43.43.3' variables:VPORT3='4303' \
+    variables:BACKENDS31='85.85.85.31' \
+    variables:BACKENDS32='85.85.85.32' \
+    variables:BPORT1='8503' variables:BPORT2='8503'
+
+dnl Ensure the LBs are translated to OpenFlow.
+as hv1
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=85.85.85.1:8501)' -c], 
[0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=85.85.85.21:8502)' -c], 
[0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=85.85.85.22:8502)' -c], 
[0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=85.85.85.31:8503)' -c], 
[0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=85.85.85.32:8503)' -c], 
[0], [dnl
+1
+])
+
+dnl Ensure hairpin flows are correct.
+as hv1
+AT_CHECK([ovs-ofctl dump-flows br-int | grep table=68 | ofctl_strip_all], [0], 
[dnl
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2b2b2b01,reg2=0x10cd/0xffff,nw_src=85.85.85.1,nw_dst=85.85.85.1,tp_dst=8501
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=43.43.43.1,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2b2b2b02,reg2=0x10ce/0xffff,nw_src=85.85.85.21,nw_dst=85.85.85.21,tp_dst=8502
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=43.43.43.2,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2b2b2b02,reg2=0x10ce/0xffff,nw_src=85.85.85.22,nw_dst=85.85.85.22,tp_dst=8502
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=43.43.43.2,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2b2b2b03,reg2=0x10cf/0xffff,nw_src=85.85.85.31,nw_dst=85.85.85.31,tp_dst=8503
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=43.43.43.3,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2b2b2b03,reg2=0x10cf/0xffff,nw_src=85.85.85.32,nw_dst=85.85.85.32,tp_dst=8503
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=43.43.43.3,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+])
+
+dnl Change Chassis_Template_Var mappings
+check ovn-nbctl --wait=hv set Chassis_Template_Var hv1 \
+    variables:VIP1='42.42.42.1' variables:VPORT1='4201' \
+    variables:BACKENDS1='84.84.84.1:8401' \
+    variables:VIP2='42.42.42.2' variables:VPORT2='4202' \
+    variables:BACKENDS21='84.84.84.21:8402' \
+    variables:BACKENDS22='84.84.84.22:8402' \
+    variables:VIP3='42.42.42.3' variables:VPORT3='4203' \
+    variables:BACKENDS31='84.84.84.31' \
+    variables:BACKENDS32='84.84.84.32' \
+    variables:BPORT1='8403' variables:BPORT2='8403'
+
+dnl Ensure the LBs are translated to OpenFlow.
+as hv1
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=84.84.84.1:8401)' -c], 
[0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=84.84.84.21:8402)' -c], 
[0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=84.84.84.22:8402)' -c], 
[0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=84.84.84.31:8403)' -c], 
[0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat(dst=84.84.84.32:8403)' -c], 
[0], [dnl
+1
+])
+
+dnl Ensure hairpin flows are correct.
+as hv1
+AT_CHECK([ovs-ofctl dump-flows br-int | grep table=68 | ofctl_strip_all], [0], 
[dnl
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2a2a2a01,reg2=0x1069/0xffff,nw_src=84.84.84.1,nw_dst=84.84.84.1,tp_dst=8401
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=42.42.42.1,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2a2a2a02,reg2=0x106a/0xffff,nw_src=84.84.84.21,nw_dst=84.84.84.21,tp_dst=8402
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=42.42.42.2,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2a2a2a02,reg2=0x106a/0xffff,nw_src=84.84.84.22,nw_dst=84.84.84.22,tp_dst=8402
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=42.42.42.2,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2a2a2a03,reg2=0x106b/0xffff,nw_src=84.84.84.31,nw_dst=84.84.84.31,tp_dst=8403
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=42.42.42.3,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+ table=68, 
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2a2a2a03,reg2=0x106b/0xffff,nw_src=84.84.84.32,nw_dst=84.84.84.32,tp_dst=8403
 
actions=load:0x1->NXM_NX_REG10[[7]],learn(table=69,delete_learned,OXM_OF_METADATA[[]],eth_type=0x800,NXM_OF_IP_SRC[[]],ip_dst=42.42.42.3,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
+])
+
+dnl Remove Chassis_Template_Variables and check that everything is
+dnl removed from OpenFlow.
+check ovn-nbctl --wait=hv clear Chassis_Template_Var hv1 variables
+
+as hv1
+AT_CHECK([ovs-ofctl dump-groups br-int | grep 'nat'], [1], [])
+
+as hv1
+AT_CHECK([ovs-ofctl dump-flows br-int | grep table=68 | ofctl_strip_all], [0], 
[])
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+])
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index 20c0584151..8d0ec68479 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -8597,3 +8597,186 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port 
patch-.*/d
  /connection dropped.*/d"])
  AT_CLEANUP
  ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([load-balancer template IPv4])
+AT_SKIP_IF([test $HAVE_NC = no])
+AT_KEYWORDS([ovnlb templates])
+
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+OVS_CHECK_CT_ZERO_SNAT()
+ADD_BR([br-int])
+
+# Set external-ids in br-int needed for ovn-controller
+ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . 
external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+# Start ovn-controller
+start_daemon ovn-controller
+
+# Logical network:
+# VM1 -- LS1 -- GW-Router -- LS2 -- VM3
+#         |
+# VM2 ----+
+#
+# A templated load balancer applied on LS1 and GW-Router with
+# VM1 as backend.  The VIP should be accessible from both VM2 and VM3.
+
+check ovn-nbctl                                                   \
+    -- lr-add rtr                                                 \
+    -- set Logical_Router rtr options:chassis=hv1                 \
+    -- lrp-add rtr rtr-ls1 00:00:00:00:01:00 42.42.42.1/24        \
+    -- lrp-add rtr rtr-ls2 00:00:00:00:02:00 43.43.43.1/24        \
+    -- ls-add ls1                                                 \
+    -- lsp-add ls1 ls1-rtr                                        \
+    -- lsp-set-addresses ls1-rtr 00:00:00:00:01:00                \
+    -- lsp-set-type ls1-rtr router                                \
+    -- lsp-set-options ls1-rtr router-port=rtr-ls1                \
+    -- lsp-add ls1 vm1 -- lsp-set-addresses vm1 00:00:00:00:00:01 \
+    -- lsp-add ls1 vm2 -- lsp-set-addresses vm2 00:00:00:00:00:02 \
+    -- ls-add ls2                                                 \
+    -- lsp-add ls2 ls2-rtr                                        \
+    -- lsp-set-addresses ls2-rtr 00:00:00:00:02:00                \
+    -- lsp-set-type ls2-rtr router                                \
+    -- lsp-set-options ls2-rtr router-port=rtr-ls2                \
+    -- lsp-add ls2 vm3 -- lsp-set-addresses vm3 00:00:00:00:00:03
+
+# Add a template LB that eventually expands to:
+# VIP=66.66.66.66:666 backends=42.42.42.2:4242 proto=tcp
+
+AT_CHECK([ovn-nbctl -- create chassis_template_var chassis="hv1" 
variables="{vip=66.66.66.66,vport=666,backends=\"42.42.42.2:4242\"}"],
+         [0], [ignore])
+
+check ovn-nbctl --template lb-add lb-test "^vip:^vport" "^backends" tcp \
+    -- ls-lb-add ls1 lb-test                                            \
+    -- lr-lb-add rtr lb-test
+
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "42.42.42.2/24", "00:00:00:00:00:01", "42.42.42.1")
+
+ADD_NAMESPACES(vm2)
+ADD_VETH(vm2, vm2, br-int, "42.42.42.3/24", "00:00:00:00:00:02", "42.42.42.1")
+
+ADD_NAMESPACES(vm3)
+ADD_VETH(vm3, vm3, br-int, "43.43.43.2/24", "00:00:00:00:00:03", "43.43.43.1")
+
+# Wait for ovn-controller to catch up.
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+AT_CHECK([ovn-appctl -t ovn-controller debug/dump-local-template-vars | sort], 
[0], [dnl
+Local template vars:
+name: 'backends' value: '42.42.42.2:4242'
+name: 'vip' value: '66.66.66.66'
+name: 'vport' value: '666'
+])
+
+# Start IPv4 TCP server on vm1.
+NETNS_DAEMONIZE([vm1], [nc -k -l 42.42.42.2 4242], [nc-vm1.pid])
+
+# Make sure connecting to the VIP works.
+NS_CHECK_EXEC([vm2], [nc 66.66.66.66 666 -z], [0], [ignore], [ignore])
+NS_CHECK_EXEC([vm3], [nc 66.66.66.66 666 -z], [0], [ignore], [ignore])
+
+AT_CLEANUP
+])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([load-balancer template IPv6])
+AT_SKIP_IF([test $HAVE_NC = no])
+AT_KEYWORDS([ovnlb templates])
+
+CHECK_CONNTRACK()
+CHECK_CONNTRACK_NAT()
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+OVS_CHECK_CT_ZERO_SNAT()
+ADD_BR([br-int])
+
+# Set external-ids in br-int needed for ovn-controller
+ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . 
external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+# Start ovn-controller
+start_daemon ovn-controller
+
+# Logical network:
+# VM1 -- LS1 -- GW-Router -- LS2 -- VM3
+#         |
+# VM2 ----+
+#
+# A templated load balancer applied on LS1 and GW-Router with
+# VM1 as backend.  The VIP should be accessible from both VM2 and VM3.
+
+check ovn-nbctl                                                   \
+    -- lr-add rtr                                                 \
+    -- set Logical_Router rtr options:chassis=hv1                 \
+    -- lrp-add rtr rtr-ls1 00:00:00:00:01:00 4242::1/64           \
+    -- lrp-add rtr rtr-ls2 00:00:00:00:02:00 4343::1/64           \
+    -- ls-add ls1                                                 \
+    -- lsp-add ls1 ls1-rtr                                        \
+    -- lsp-set-addresses ls1-rtr 00:00:00:00:01:00                \
+    -- lsp-set-type ls1-rtr router                                \
+    -- lsp-set-options ls1-rtr router-port=rtr-ls1                \
+    -- lsp-add ls1 vm1 -- lsp-set-addresses vm1 00:00:00:00:00:01 \
+    -- lsp-add ls1 vm2 -- lsp-set-addresses vm2 00:00:00:00:00:02 \
+    -- ls-add ls2                                                 \
+    -- lsp-add ls2 ls2-rtr                                        \
+    -- lsp-set-addresses ls2-rtr 00:00:00:00:02:00                \
+    -- lsp-set-type ls2-rtr router                                \
+    -- lsp-set-options ls2-rtr router-port=rtr-ls2                \
+    -- lsp-add ls2 vm3 -- lsp-set-addresses vm3 00:00:00:00:00:03
+
+# Add a template LB that eventually expands to:
+# VIP=6666::1 backends=[4242::2]:4242 proto=tcp
+
+AT_CHECK([ovn-nbctl -- create chassis_template_var chassis="hv1" 
variables="{vip=\"6666::1\",vport=666,backends=\"[[4242::2]]:4242\"}"],
+         [0], [ignore])
+
+check ovn-nbctl --template lb-add lb-test "^vip:^vport" "^backends" tcp ipv6 \
+    -- ls-lb-add ls1 lb-test                                                 \
+    -- lr-lb-add rtr lb-test
+
+ADD_NAMESPACES(vm1)
+ADD_VETH(vm1, vm1, br-int, "4242::2/64", "00:00:00:00:00:01", "4242::1")
+OVS_WAIT_UNTIL([test "$(ip netns exec vm1 ip a | grep 4242::2 | grep tentative)" = 
""])
+
+ADD_NAMESPACES(vm2)
+ADD_VETH(vm2, vm2, br-int, "4242::3/64", "00:00:00:00:00:02", "4242::1")
+OVS_WAIT_UNTIL([test "$(ip netns exec vm2 ip a | grep 4242::3 | grep tentative)" = 
""])
+
+ADD_NAMESPACES(vm3)
+ADD_VETH(vm3, vm3, br-int, "4343::2/64", "00:00:00:00:00:03", "4343::1")
+OVS_WAIT_UNTIL([test "$(ip netns exec vm3 ip a | grep 4343::2 | grep tentative)" = 
""])
+
+# Wait for ovn-controller to catch up.
+wait_for_ports_up
+check ovn-nbctl --wait=hv sync
+
+AT_CHECK([ovn-appctl -t ovn-controller debug/dump-local-template-vars | sort], 
[0], [dnl
+Local template vars:
+name: 'backends' value: '[[4242::2]]:4242'
+name: 'vip' value: '6666::1'
+name: 'vport' value: '666'
+])
+
+# Start IPv6 TCP server on vm1.
+NETNS_DAEMONIZE([vm1], [nc -k -l 4242::2 4242], [nc-vm1.pid])
+
+# Make sure connecting to the VIP works.
+NS_CHECK_EXEC([vm2], [nc 6666::1 666 -z], [0], [ignore], [ignore])
+NS_CHECK_EXEC([vm3], [nc 6666::1 666 -z], [0], [ignore], [ignore])
+
+AT_CLEANUP
+])
diff --git a/utilities/ovn-nbctl.c b/utilities/ovn-nbctl.c
index d2dee6b31c..dea7218f21 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);

Hm, this is a case where anything that isn't "ipv4" gets treated as if it were "ipv6". If I were to pass something like "pepperoni_pizza" it would consistently be treated like "ipv6" and work just fine. However, it would be really weird to have that in the DB and I could totally see it failing due to a future well-intentioned update from a developer.

Therefore, when setting "address-family", it probably would be better to normalize to either "ipv4" or "ipv6" instead of whatever the user passed in.

+    }
      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);
@@ -7472,9 +7477,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
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to