On 11/29/22 09:14, Han Zhou wrote:
> On Tue, Nov 22, 2022 at 6:15 AM Dumitru Ceara <[email protected]> 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
> 
> Sorry if I missed, but I didn't see any tests that test the form
> "^backends_variable1,^backends_variable2". I only see tests with a single
> backend variable with a single IP in it. Better to test:
> 1. Multiple backends variables

This should be covered.  I have this in the test:

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

> 2. Multiple IPs in a single variable (I saw this in the tutorial in patch
> 5, but better to be covered here, too)
> 

You're right, I added a LB with such a variable instantiation to the
test.

> 
> 
>>
>> 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 <[email protected]>
>> ---
>> V3:
>> - Addressed Mark's comments:
>>   - Added TODO items about potential future template LB improvements.
>>   - Removed n_backends arg from ovn_lb_backends_init_explicit() and
>>     ovn_lb_backends_init_template().
>>   - Fixed ovn_northd_lb_create() to first get the template option before
>>     using its value.
>>   - Fixed up comments and man pages.
>>   - Hardenned setting of address family in ovn-nbctl.
>>
>> 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.
>> ---
>>  TODO.rst                    |    7 +
>>  controller/lflow.c          |  115 +++++++++--
>>  controller/lflow.h          |    7 +
>>  controller/ovn-controller.c |   67 +++++-
>>  lib/lb.c                    |  452
> ++++++++++++++++++++++++++++++++++++++-----
>>  lib/lb.h                    |   40 +++-
>>  lib/ovn-util.c              |    3
>>  northd/northd.c             |   89 ++++----
>>  ovn-nb.xml                  |   62 ++++++
>>  tests/ovn-nbctl.at          |   23 +-
>>  tests/ovn-northd.at         |    7 +
>>  tests/ovn.at                |  131 ++++++++++++
>>  tests/system-ovn.at         |  183 +++++++++++++++++
>>  utilities/ovn-nbctl.c       |  120 ++++++-----
>>  14 files changed, 1078 insertions(+), 228 deletions(-)
>>
>> diff --git a/TODO.rst b/TODO.rst
>> index fe5f9a2f30..53cf2870b2 100644
>> --- a/TODO.rst
>> +++ b/TODO.rst
>> @@ -183,3 +183,10 @@ OVN To-do List
>>  * Chassis_Template_Var
>>
>>    * Support template variables when tracing packets with ovn-trace.
>> +
>> +* Load Balancer templates
>> +
>> +  * Support combining the VIP (or backend) IP and port into a single
>> +    template variable.
> 
> Is it still a TODO for backends? At least the tutorial test (in patch 5) is
> already doing something like this:
> backends0="42.0.0.1:1,42.1.0.1:1,42.2.0.1:1,42.3.0.1:1,42.4.0.1:1"
> 

You're right, we can combine the backends and it works fine.  I removed
the "(or backend)" part.

>> +
>> +  * Support combining all backends into a single template variable.
> 
> What does it mean here? Isn't the tutorial test already combining multiple
> backends into a single variable?
> 

Yes, it is, and it's working fine.  I'm not sure anymore why I had
added this TODO item here.  I removed it now.

>> diff --git a/controller/lflow.c b/controller/lflow.c
>> index 84625fb3f1..f6ac639541 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);
>> @@ -687,7 +696,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;
>> @@ -777,13 +786,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);
>> @@ -792,6 +801,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",
> 
> nit: I think it should be: Failed to find LB ... that refers ...
> 

Yes, fixed.

> 
>> +                     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,
>> @@ -1259,9 +1305,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) {
>> @@ -2001,8 +2047,10 @@ add_lb_ct_snat_hairpin_flows(struct
> ovn_controller_lb *lb,
>>  }
>>
>>  static void
>> -consider_lb_hairpin_flows(const struct sbrec_load_balancer *sbrec_lb,
>> +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)
>> @@ -2039,7 +2087,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")) {
>> @@ -2049,6 +2099,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];
>>
>> @@ -2063,13 +2118,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)
>> @@ -2092,8 +2151,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);
>>      }
>>  }
>>
>> @@ -2229,7 +2288,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,
>> @@ -2280,10 +2341,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);
>> @@ -2308,7 +2369,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;
>>              }
>> @@ -2360,7 +2421,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);
>> @@ -2382,7 +2445,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;
>>      }
>> @@ -2421,7 +2484,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;
>> @@ -2448,7 +2511,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;
>> @@ -2502,7 +2565,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 d95fd41142..9e8f9afd33 100644
>> --- a/controller/lflow.h
>> +++ b/controller/lflow.h
>> @@ -122,9 +122,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;
>>  };
>> @@ -174,4 +175,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 f9ed0e3855..9807ecd8eb 100644
>> --- a/controller/ovn-controller.c
>> +++ b/controller/ovn-controller.c
>> @@ -2791,13 +2791,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. */
>> @@ -2954,8 +2956,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;
>> @@ -2970,8 +2973,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);
>> @@ -2983,7 +2987,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
>> @@ -2994,8 +2998,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);
>> @@ -3030,6 +3035,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) {
>> @@ -3039,6 +3045,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);
>>      }
>>
>> @@ -3172,7 +3179,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;
>>          }
>> @@ -3191,7 +3198,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;
>>              }
>> @@ -3204,7 +3211,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;
>>          }
>> @@ -3239,7 +3246,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;
>>          }
>> @@ -3251,7 +3258,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;
>>          }
>> @@ -3263,7 +3270,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;
>>          }
>> @@ -3297,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;
>>          }
>> @@ -3309,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;
>>          }
>> @@ -3321,7 +3348,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 c08ccceda1..43628bba77 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,293 @@ 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)
>>  {
>> -    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;
>> +    lb_vip->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 (lb_vip->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[lb_vip->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;
>> +        lb_vip->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);
>> +}
>> +
>> +/* 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_)
>> +{
>> +    struct ds errors = DS_EMPTY_INITIALIZER;
>> +    char *value = xstrdup(value_);
>> +    char *save_ptr = NULL;
>> +    size_t n_allocated_backends = 0;
>> +    lb_vip->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 (lb_vip->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[lb_vip->n_backends];
>> +            lb_backend->ip_str = backend_ip;
>> +            lb_backend->port_str = backend_port;
>> +            lb_backend->port = 0;
>> +            lb_vip->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);
>> +}
>> +
>> +/* 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 +399,113 @@ 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 template = smap_get_bool(&nbrec_lb->options, "template", false);
>>      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 +513,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,
> template);
>> +
>>      lb->skip_snat = smap_get_bool(&nbrec_lb->options, "skip_snat",
> false);
>> -    const char *mode =
>> -        smap_get_def(&nbrec_lb->options, "neighbor_responder",
> "reachable");
>> -    lb->neigh_mode = strcmp(mode, "all") ? LB_NEIGH_RESPOND_REACHABLE
>> -                                         : LB_NEIGH_RESPOND_ALL;
>> +    lb->template = template;
>> +
>> +    const char *mode = smap_get(&nbrec_lb->options,
> "neighbor_responder");
>> +    lb->neigh_mode = ovn_lb_get_neigh_mode(nbrec_lb, mode, template);
>> +
>>      uint32_t affinity_timeout =
>>          smap_get_uint(&nbrec_lb->options, "affinity_timeout", 0);
>>      if (affinity_timeout > UINT16_MAX) {
>> @@ -227,13 +542,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,
>> +                                      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, template);
>>          if (IN6_IS_ADDR_V4MAPPED(&lb_vip->vip)) {
>>              sset_add(&lb->ips_v4, lb_vip->vip_str);
>>          } else {
>> @@ -381,9 +702,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);
>> @@ -395,10 +719,26 @@ 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;
>> +        struct lex_str key_s = template
>> +                               ? lexer_parse_template_string(node->key,
>> +
> template_vars,
>> +
> template_vars_ref)
>> +                               : lex_str_use(node->key);
>> +        struct lex_str value_s = template
>> +                               ? lexer_parse_template_string(node->value,
>> +
> template_vars,
>> +
> template_vars_ref)
>> +                               : lex_str_use(node->value);
>> +        char *error = ovn_lb_vip_init_explicit(lb_vip,
>> +                                               lex_str_get(&key_s),
>> +                                               lex_str_get(&value_s));
>> +        if (error) {
>> +            free(error);
>> +        } else {
>> +            n_vips++;
>>          }
>> -        n_vips++;
>> +        lex_str_free(&key_s);
>> +        lex_str_free(&value_s);
>>      }
>>
>>      /* It's possible that parsing VIPs fails.  Update the lb->n_vips to
> the
>> diff --git a/lib/lb.h b/lib/lb.h
>> index 62843e4716..55a41ae0bc 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;
>>      uint16_t affinity_timeout;
>>
>>      struct sset ips_v4;
>> @@ -82,19 +84,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-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. */
>> @@ -174,7 +188,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 597625a291..1f8d0b8add 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 123127e9c1..c590c14818 100644
>> --- a/northd/northd.c
>> +++ b/northd/northd.c
>> @@ -3740,6 +3740,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];
>> @@ -4056,12 +4060,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);
>> @@ -4073,8 +4084,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;
>>
>> @@ -5834,16 +5846,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,
>> @@ -5854,7 +5866,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;
>> @@ -6910,7 +6922,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);
>> @@ -6921,7 +6933,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")) {
>> @@ -6934,14 +6946,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;
>> @@ -7192,24 +7204,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);
>>
>> @@ -7227,8 +7227,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;
>>          }
>>
>> @@ -10231,7 +10232,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 {
>> @@ -10247,14 +10248,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));
>> @@ -10263,7 +10264,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";
>> @@ -10281,9 +10282,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, ") || ");
>>          }
>> @@ -10296,9 +10297,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 =
>> @@ -10571,7 +10572,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);
>> @@ -10581,7 +10582,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 553c0e48c3..8cd2427e8f 100644
>> --- a/ovn-nb.xml
>> +++ b/ovn-nb.xml
>> @@ -1905,8 +1905,66 @@
>>          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.  The load balancer VIPs and backends must be using
>> +          <ref table="Chassis_Template_Var"/> in their definitions.
>> +        </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.
> 
> In this version, the vars are not names but keys of the "variables" column.
> 

True, I fixed it.

>> +        </p>
>> +
>> +        <p>
>> +          Note: The VIP and PORT cannot be combined into a single
> template
>> +          variable. For example, a <ref table="Chassis_Template_Var"/>
>> +          variable expanding to <code>10.0.0.1:8080</code> is not valid
>> +          if used as VIP.
>> +        </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
> 
> I think here each var means a single backend IP, right? So,
> s/BACKENDS/BACKEND/g
> 

Not necessarily.  There's actually no restriction.

>> +        </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
> 
> Same here, and they are keys instead of "names".
> 
> Acked-by: Han Zhou <[email protected]>
> 

Thanks, Han!  I ended up with the following incremental.  Let me
know if it looks ok to you and I can fold it in.

Regards,
Dumitru

---
diff --git a/TODO.rst b/TODO.rst
index 53cf2870b2..15fd131d39 100644
--- a/TODO.rst
+++ b/TODO.rst
@@ -186,7 +186,4 @@ OVN To-do List
  * Load Balancer templates
 -  * Support combining the VIP (or backend) IP and port into a single
-    template variable.
-
-  * Support combining all backends into a single template variable.
+  * Support combining the VIP IP and port into a single template variable.
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 7ecf2047e7..0edc3da96c 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -1958,8 +1958,9 @@
         </pre>
          <p>
-          where <code>VIP_VAR</code> and <code>PORT_VAR</code> are names of
-        <ref table="Chassis_Template_Var"/> records.
+          where <code>VIP_VAR</code> and <code>PORT_VAR</code> are keys of
+          the <ref table="Chassis_Template_Var"/> <ref column="variables"/>
+          records.
         </p>
          <p>
@@ -1982,8 +1983,9 @@ or
         <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.
+          <code>BACKENDS_VAR1</code> and <code>BACKENDS_VAR2</code> are
keys
+          of the <ref table="Chassis_Template_Var"/> <ref
column="variables"/>
+          records.
         </p>
       </column>
 diff --git a/tests/ovn.at b/tests/ovn.at
index bc3a7adfba..f3bd532423 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -33525,9 +33525,11 @@ 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 --template lb-add lb-test4 "^VIP4:^VPORT4"
"^BACKENDS41,^BACKENDS42" 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 ovn-nbctl ls-lb-add sw lb-test4
  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
@@ -33549,7 +33551,10 @@ check ovn-nbctl --wait=hv set
Chassis_Template_Var hv1 \
     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'
+    variables:BPORT1='8503' variables:BPORT2='8503' \
+    variables:VIP4='43.43.43.4' variables:VPORT4='4304' \
+    variables:BACKENDS41='85.85.85.41:8504,85.85.85.42:8504' \
+    variables:BACKENDS42='85.85.85.43:8504,85.85.85.44:8504'
  dnl Ensure the LBs are translated to OpenFlow.
 as hv1
@@ -33568,6 +33573,18 @@ AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=85.85.85.31:8503)' -c], [
 AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=85.85.85.32:8503)' -c], [0], [dnl
 1
 ])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=85.85.85.41:8504)' -c], [0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=85.85.85.42:8504)' -c], [0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=85.85.85.43:8504)' -c], [0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=85.85.85.44:8504)' -c], [0], [dnl
+1
+])
  dnl Ensure hairpin flows are correct.
 as hv1
@@ -33577,6 +33594,10 @@ AT_CHECK([ovs-ofctl dump-flows br-int | grep
table=68 | ofctl_strip_all], [0], [
  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]])
+ table=68,
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2b2b2b04,reg2=0x10d0/0xffff,nw_src=85.85.85.41,nw_dst=85.85.85.41,tp_dst=8504
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.4,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=0x2b2b2b04,reg2=0x10d0/0xffff,nw_src=85.85.85.42,nw_dst=85.85.85.42,tp_dst=8504
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.4,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=0x2b2b2b04,reg2=0x10d0/0xffff,nw_src=85.85.85.43,nw_dst=85.85.85.43,tp_dst=8504
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.4,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=0x2b2b2b04,reg2=0x10d0/0xffff,nw_src=85.85.85.44,nw_dst=85.85.85.44,tp_dst=8504
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.4,nw_proto=6,NXM_OF_TCP_SRC[[]]=NXM_OF_TCP_DST[[]],load:0x1->NXM_NX_REG10[[7]])
 ])
  dnl Change Chassis_Template_Var mappings
@@ -33589,7 +33610,10 @@ check ovn-nbctl --wait=hv set
Chassis_Template_Var hv1 \
     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'
+    variables:BPORT1='8403' variables:BPORT2='8403' \
+    variables:VIP4='42.42.42.4' variables:VPORT4='4204' \
+    variables:BACKENDS41='84.84.84.41:8404,84.84.84.42:8404' \
+    variables:BACKENDS42='84.84.84.43:8404,84.84.84.44:8404'
  dnl Ensure the LBs are translated to OpenFlow.
 as hv1
@@ -33608,6 +33632,18 @@ AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=84.84.84.31:8403)' -c], [
 AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=84.84.84.32:8403)' -c], [0], [dnl
 1
 ])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=84.84.84.41:8404)' -c], [0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=84.84.84.42:8404)' -c], [0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=84.84.84.43:8404)' -c], [0], [dnl
+1
+])
+AT_CHECK([ovs-ofctl dump-groups br-int | grep
'nat(dst=84.84.84.44:8404)' -c], [0], [dnl
+1
+])
  dnl Ensure hairpin flows are correct.
 as hv1
@@ -33617,6 +33653,10 @@ AT_CHECK([ovs-ofctl dump-flows br-int | grep
table=68 | ofctl_strip_all], [0], [
  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]])
+ table=68,
priority=100,ct_mark=0x2/0x2,tcp,reg1=0x2a2a2a04,reg2=0x106c/0xffff,nw_src=84.84.84.41,nw_dst=84.84.84.41,tp_dst=8404
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.4,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=0x2a2a2a04,reg2=0x106c/0xffff,nw_src=84.84.84.42,nw_dst=84.84.84.42,tp_dst=8404
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.4,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=0x2a2a2a04,reg2=0x106c/0xffff,nw_src=84.84.84.43,nw_dst=84.84.84.43,tp_dst=8404
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.4,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=0x2a2a2a04,reg2=0x106c/0xffff,nw_src=84.84.84.44,nw_dst=84.84.84.44,tp_dst=8404
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.4,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
---

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

Reply via email to