On 11/16/22 22:33, Mark Michelson wrote:
> Hi Dumitru, I have a few comments down below
> 
> On 11/4/22 18:11, Dumitru Ceara wrote:
>> Expand SB.Template_Var records in two stages:
>> 1. first expand them to local values in match/action strings
>> 2. then reparse the expanded strings
>>
>> For the case when a lflow references a Chassis_Template_Var
>> also track references (similar to the ones maintained for
>> multicast groups, address sets, port_groups, port bindings).
>>
>> Signed-off-by: Dumitru Ceara <[email protected]>
>> ---
>> V2:
>> - Fix GCC build due to missing newline.
>> - Handle SB table rename Template_Var -> Chassis_Template_Var.
>> - Address Han's comments:
>>    - Add new function to parse lflow actions.
>>    - Move xstrdup inside lexer_parse_template_string() and execute it
>> only
>>      if needed.
>>    - Match template vars only by chassis name.
>>    - Change local_templates table to a plain smap.
>>    - Some indentation updates.
>>    - Use NULL template variable change handlers for chassis/Open_vSwitch
>>      changes.
>>    - Added more tests.
>> - Fix tracking of template references in lflow actions.
>> ---
>>   controller/lflow.c          |  136 ++++++++++++++------
>>   controller/lflow.h          |    1
>>   controller/ofctrl.c         |   11 +-
>>   controller/ofctrl.h         |    3
>>   controller/ovn-controller.c |  289
>> +++++++++++++++++++++++++++++++++++++++++++
>>   include/ovn/expr.h          |    4 -
>>   include/ovn/lex.h           |   15 ++
>>   lib/actions.c               |    9 +
>>   lib/expr.c                  |   18 ++-
>>   lib/lex.c                   |   57 ++++++++
>>   lib/objdep.c                |    1
>>   lib/objdep.h                |    1
>>   tests/ovn-controller.at     |   50 +++++++
>>   tests/ovn.at                |   80 ++++++++++++
>>   tests/test-ovn.c            |   17 ++-
>>   utilities/ovn-trace.c       |   33 ++++-
>>   16 files changed, 658 insertions(+), 67 deletions(-)
>>
>> diff --git a/controller/lflow.c b/controller/lflow.c
>> index d4434bdee8..fc4371d0df 100644
>> --- a/controller/lflow.c
>> +++ b/controller/lflow.c
>> @@ -81,6 +81,8 @@ convert_match_to_expr(const struct
>> sbrec_logical_flow *,
>>                         const struct local_datapath *ldp,
>>                         struct expr **prereqs, const struct shash
>> *addr_sets,
>>                         const struct shash *port_groups,
>> +                      const struct smap *template_vars,
>> +                      struct sset *template_vars_ref,
>>                         struct objdep_mgr *, bool *pg_addr_set_ref);
>>   static void
>>   add_matches_to_flow_table(const struct sbrec_logical_flow *,
>> @@ -297,6 +299,43 @@ as_info_from_expr_const(const char *as_name,
>> const union expr_constant *c,
>>       return true;
>>   }
>>   +static bool
>> +lflow_parse_actions(const struct sbrec_logical_flow *lflow,
>> +                    const struct lflow_ctx_in *l_ctx_in,
>> +                    struct sset *template_vars_ref,
>> +                    struct ofpbuf *ovnacts_out,
>> +                    struct expr **prereqs_out)
>> +{
>> +    bool ingress = !strcmp(lflow->pipeline, "ingress");
>> +    struct ovnact_parse_params pp = {
>> +        .symtab = &symtab,
>> +        .dhcp_opts = l_ctx_in->dhcp_opts,
>> +        .dhcpv6_opts = l_ctx_in->dhcpv6_opts,
>> +        .nd_ra_opts = l_ctx_in->nd_ra_opts,
>> +        .controller_event_opts = l_ctx_in->controller_event_opts,
>> +
>> +        .pipeline = ingress ? OVNACT_P_INGRESS : OVNACT_P_EGRESS,
>> +        .n_tables = LOG_PIPELINE_LEN,
>> +        .cur_ltable = lflow->table_id,
>> +    };
>> +
>> +    char *actions_expanded_s = NULL;
>> +    const char *actions_s =
>> +        lexer_parse_template_string(lflow->actions,
>> l_ctx_in->template_vars,
>> +                                    template_vars_ref,
>> &actions_expanded_s);
>> +    char *error = ovnacts_parse_string(actions_s, &pp,
>> +                                       ovnacts_out, prereqs_out);
>> +    free(actions_expanded_s);
>> +    if (error) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
>> +        VLOG_WARN_RL(&rl, "error parsing actions \"%s\": %s",
>> +                     lflow->actions, error);
>> +        free(error);
>> +        return false;
>> +    }
>> +    return true;
>> +}
>> +
>>   /* Parses the lflow regarding the changed address set 'as_name', and
>> generates
>>    * ovs flows for the newly added addresses in 'as_diff_added' only.
>> It is
>>    * similar to consider_logical_flow__, with the below differences:
>> @@ -347,27 +386,14 @@ consider_lflow_for_added_as_ips__(
>>         uint64_t ovnacts_stub[1024 / 8];
>>       struct ofpbuf ovnacts = OFPBUF_STUB_INITIALIZER(ovnacts_stub);
>> -    struct ovnact_parse_params pp = {
>> -        .symtab = &symtab,
>> -        .dhcp_opts = l_ctx_in->dhcp_opts,
>> -        .dhcpv6_opts = l_ctx_in->dhcpv6_opts,
>> -        .nd_ra_opts = l_ctx_in->nd_ra_opts,
>> -        .controller_event_opts = l_ctx_in->controller_event_opts,
>> -        .pipeline = ingress ? OVNACT_P_INGRESS : OVNACT_P_EGRESS,
>> -        .n_tables = LOG_PIPELINE_LEN,
>> -        .cur_ltable = lflow->table_id,
>> -    };
>> +    struct sset template_vars_ref =
>> SSET_INITIALIZER(&template_vars_ref);
>>       struct expr *prereqs = NULL;
>> -    char *error;
>>   -    error = ovnacts_parse_string(lflow->actions, &pp, &ovnacts,
>> &prereqs);
>> -    if (error) {
>> -        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
>> -        VLOG_WARN_RL(&rl, "error parsing actions \"%s\": %s",
>> -                     lflow->actions, error);
>> -        free(error);
>> +    if (!lflow_parse_actions(lflow, l_ctx_in, &template_vars_ref,
>> +                             &ovnacts, &prereqs)) {
>>           ovnacts_free(ovnacts.data, ovnacts.size);
>>           ofpbuf_uninit(&ovnacts);
>> +        sset_destroy(&template_vars_ref);
>>           return true;
>>       }
>>   @@ -431,6 +457,8 @@ consider_lflow_for_added_as_ips__(
>>       struct expr *expr = convert_match_to_expr(lflow, ldp, &prereqs,
>>                                                 l_ctx_in->addr_sets,
>>                                                 l_ctx_in->port_groups,
>> +                                              l_ctx_in->template_vars,
>> +                                              &template_vars_ref,
>>                                                
>> l_ctx_out->lflow_deps_mgr, NULL);
>>       shash_replace((struct shash *)l_ctx_in->addr_sets, as_name,
>> real_as);
>>       if (new_fake_as) {
>> @@ -502,6 +530,14 @@ done:
>>       ofpbuf_uninit(&ovnacts);
>>       expr_destroy(expr);
>>       expr_matches_destroy(&matches);
>> +
>> +    const char *tv_name;
>> +    SSET_FOR_EACH (tv_name, &template_vars_ref) {
>> +        objdep_mgr_add(l_ctx_out->lflow_deps_mgr, OBJDEP_TYPE_TEMPLATE,
>> +                       tv_name, &lflow->header_.uuid);
>> +    }
> 
> The loop above seems to be an inlined implementation of
> store_lflow_template_refs(). I think you should just call that function
> instead.
> 

You're right.  I factored out the function at some point and I failed to
use it in all applicable places.  Fixed in v3.

>> +    sset_destroy(&template_vars_ref);
>> +
>>       return handled;
>>   }
>>   @@ -913,6 +949,8 @@ convert_match_to_expr(const struct
>> sbrec_logical_flow *lflow,
>>                         struct expr **prereqs,
>>                         const struct shash *addr_sets,
>>                         const struct shash *port_groups,
>> +                      const struct smap *template_vars,
>> +                      struct sset *template_vars_ref,
>>                         struct objdep_mgr *mgr,
>>                         bool *pg_addr_set_ref)
>>   {
>> @@ -920,11 +958,18 @@ convert_match_to_expr(const struct
>> sbrec_logical_flow *lflow,
>>       struct sset port_groups_ref = SSET_INITIALIZER(&port_groups_ref);
>>       char *error = NULL;
>>   -    struct expr *e = expr_parse_string(lflow->match, &symtab,
>> addr_sets,
>> +    char *match_expanded_s = NULL;
>> +    const char *match_s = lexer_parse_template_string(lflow->match,
>> +                                                      template_vars,
>> +                                                      template_vars_ref,
>> +                                                     
>> &match_expanded_s);
>> +    struct expr *e = expr_parse_string(match_s, &symtab, addr_sets,
>>                                          port_groups, &addr_sets_ref,
>>                                          &port_groups_ref,
>>                                          ldp->datapath->tunnel_key,
>>                                          &error);
>> +    free(match_expanded_s);
>> +
>>       struct shash_node *addr_sets_ref_node;
>>       SHASH_FOR_EACH (addr_sets_ref_node, &addr_sets_ref) {
>>           objdep_mgr_add_with_refcount(mgr, OBJDEP_TYPE_ADDRSET,
>> @@ -963,6 +1008,18 @@ convert_match_to_expr(const struct
>> sbrec_logical_flow *lflow,
>>       return expr_simplify(e);
>>   }
>>   +static void
>> +store_lflow_template_refs(struct objdep_mgr *lflow_deps_mgr,
>> +                          const struct sset *template_vars_ref,
>> +                          const struct sbrec_logical_flow *lflow)
>> +{
>> +    const char *tv_name;
>> +    SSET_FOR_EACH (tv_name, template_vars_ref) {
>> +        objdep_mgr_add(lflow_deps_mgr, OBJDEP_TYPE_TEMPLATE, tv_name,
>> +                       &lflow->header_.uuid);
>> +    }
>> +}
>> +
>>   static void
>>   consider_logical_flow__(const struct sbrec_logical_flow *lflow,
>>                           const struct sbrec_datapath_binding *dp,
>> @@ -1015,28 +1072,16 @@ consider_logical_flow__(const struct
>> sbrec_logical_flow *lflow,
>>        * XXX Deny changes to 'outport' in egress pipeline. */
>>       uint64_t ovnacts_stub[1024 / 8];
>>       struct ofpbuf ovnacts = OFPBUF_STUB_INITIALIZER(ovnacts_stub);
>> -    struct ovnact_parse_params pp = {
>> -        .symtab = &symtab,
>> -        .dhcp_opts = l_ctx_in->dhcp_opts,
>> -        .dhcpv6_opts = l_ctx_in->dhcpv6_opts,
>> -        .nd_ra_opts = l_ctx_in->nd_ra_opts,
>> -        .controller_event_opts = l_ctx_in->controller_event_opts,
>> -
>> -        .pipeline = ingress ? OVNACT_P_INGRESS : OVNACT_P_EGRESS,
>> -        .n_tables = LOG_PIPELINE_LEN,
>> -        .cur_ltable = lflow->table_id,
>> -    };
>> +    struct sset template_vars_ref =
>> SSET_INITIALIZER(&template_vars_ref);
>>       struct expr *prereqs = NULL;
>> -    char *error;
>>   -    error = ovnacts_parse_string(lflow->actions, &pp, &ovnacts,
>> &prereqs);
>> -    if (error) {
>> -        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
>> -        VLOG_WARN_RL(&rl, "error parsing actions \"%s\": %s",
>> -                     lflow->actions, error);
>> -        free(error);
>> +    if (!lflow_parse_actions(lflow, l_ctx_in, &template_vars_ref,
>> +                             &ovnacts, &prereqs)) {
>>           ovnacts_free(ovnacts.data, ovnacts.size);
>>           ofpbuf_uninit(&ovnacts);
>> +        store_lflow_template_refs(l_ctx_out->lflow_deps_mgr,
>> +                                  &template_vars_ref, lflow);
> 
> Can you explain why we call store_lflow_template_refs() in the case
> where lflow_parse_actions() fails? This is different from the behavior
> in consider_lflow_for_added_as_ips__(), where
> store_lflow_template_refs() is only called when lflow_parse_actions()
> succeeds.
> 

It should be called in case of failures too.  It's for the case when an
address set change is processed incrementally and the added value is a
template.  If that template is not yet defined we still need to record
the relation between the flow using the address set and the template.

I added the missing call in v3.

>> +        sset_destroy(&template_vars_ref);
>>           return;
>>       }
>>   @@ -1087,6 +1132,8 @@ consider_logical_flow__(const struct
>> sbrec_logical_flow *lflow,
>>       case LCACHE_T_NONE:
>>           expr = convert_match_to_expr(lflow, ldp, &prereqs,
>> l_ctx_in->addr_sets,
>>                                        l_ctx_in->port_groups,
>> +                                     l_ctx_in->template_vars,
>> +                                     &template_vars_ref,
>>                                        l_ctx_out->lflow_deps_mgr,
>>                                        &pg_addr_set_ref);
>>           if (!expr) {
>> @@ -1101,11 +1148,13 @@ consider_logical_flow__(const struct
>> sbrec_logical_flow *lflow,
>>       }
>>         /* If caching is enabled and this is a not cached expr that
>> doesn't refer
>> -     * to address sets or port groups, save it to potentially cache
>> it later.
>> +     * to address sets, port groups, or template variables, save it to
>> +     * potentially cache it later.
>>        */
>>       if (lcv_type == LCACHE_T_NONE
>>               && lflow_cache_is_enabled(l_ctx_out->lflow_cache)
>> -            && !pg_addr_set_ref) {
>> +            && !pg_addr_set_ref
>> +            && sset_is_empty(&template_vars_ref)) {
>>           cached_expr = expr_clone(expr);
>>       }
>>   @@ -1190,6 +1239,10 @@ done:
>>       expr_destroy(cached_expr);
>>       expr_matches_destroy(matches);
>>       free(matches);
>> +
>> +    store_lflow_template_refs(l_ctx_out->lflow_deps_mgr,
>> +                              &template_vars_ref, lflow);
>> +    sset_destroy(&template_vars_ref);
>>   }
>>     static void
>> @@ -1953,8 +2006,7 @@ add_lb_ct_snat_hairpin_flows(struct
>> ovn_controller_lb *lb,
>>     static void
>>   consider_lb_hairpin_flows(const struct sbrec_load_balancer *sbrec_lb,
>> -                          const struct hmap *local_datapaths,
>> -                          bool use_ct_mark,
>> +                          const struct hmap *local_datapaths, bool
>> use_ct_mark,
> 
> Nit: this doesn't have anything to do with chassis template variables
> and should probably be left out of this patch. If you want to change
> this, it probably should be done as a follow-up patch of its own.
> 

Ack.

>>                             struct ovn_desired_flow_table *flow_table,
>>                             struct simap *ids)
>>   {
>> @@ -2043,8 +2095,8 @@ add_lb_hairpin_flows(const struct
>> sbrec_load_balancer_table *lb_table,
>>               ovs_assert(id_pool_alloc_id(pool, &id));
>>               simap_put(ids, lb->name, id);
>>           }
>> -        consider_lb_hairpin_flows(lb, local_datapaths, use_ct_mark,
>> -                                  flow_table, ids);
>> +        consider_lb_hairpin_flows(lb, local_datapaths, use_ct_mark,
>> flow_table,
>> +                                  ids);
> 
> Nit: Same as my previous comment.
> 

Ack.

>>       }
>>   }
>>   diff --git a/controller/lflow.h b/controller/lflow.h
>> index 3f369ebfb6..9a7079f99e 100644
>> --- a/controller/lflow.h
>> +++ b/controller/lflow.h
>> @@ -112,6 +112,7 @@ struct lflow_ctx_in {
>>       const struct hmap *dhcp_opts;
>>       const struct hmap *dhcpv6_opts;
>>       const struct controller_event_options *controller_event_opts;
>> +    const struct smap *template_vars;
>>       bool lb_hairpin_use_ct_mark;
>>   };
>>   diff --git a/controller/ofctrl.c b/controller/ofctrl.c
>> index c779912583..f92e2017ce 100644
>> --- a/controller/ofctrl.c
>> +++ b/controller/ofctrl.c
>> @@ -2873,7 +2873,8 @@ ofctrl_lookup_port(const void *br_int_, const
>> char *port_name,
>>   char *
>>   ofctrl_inject_pkt(const struct ovsrec_bridge *br_int, const char
>> *flow_s,
>>                     const struct shash *addr_sets,
>> -                  const struct shash *port_groups)
>> +                  const struct shash *port_groups,
>> +                  const struct smap *template_vars)
>>   {
>>       int version = rconn_get_version(swconn);
>>       if (version < 0) {
>> @@ -2881,9 +2882,15 @@ ofctrl_inject_pkt(const struct ovsrec_bridge
>> *br_int, const char *flow_s,
>>       }
>>         struct flow uflow;
>> -    char *error = expr_parse_microflow(flow_s, &symtab, addr_sets,
>> +    char *flow_expanded_s = NULL;
>> +    const char *flow_exp_s = lexer_parse_template_string(flow_s,
>> +                                                         template_vars,
>> +                                                         NULL,
>> +                                                        
>> &flow_expanded_s);
>> +    char *error = expr_parse_microflow(flow_exp_s, &symtab, addr_sets,
>>                                          port_groups, ofctrl_lookup_port,
>>                                          br_int, &uflow);
>> +    free(flow_expanded_s);
>>       if (error) {
>>           return error;
>>       }
>> diff --git a/controller/ofctrl.h b/controller/ofctrl.h
>> index 71d3f5838a..3968245126 100644
>> --- a/controller/ofctrl.h
>> +++ b/controller/ofctrl.h
>> @@ -71,7 +71,8 @@ void ofctrl_ct_flush_zone(uint16_t zone_id);
>>     char *ofctrl_inject_pkt(const struct ovsrec_bridge *br_int,
>>                           const char *flow_s, const struct shash
>> *addr_sets,
>> -                        const struct shash *port_groups);
>> +                        const struct shash *port_groups,
>> +                        const struct smap *template_vars);
>>     /* Flow table interfaces to the rest of ovn-controller. */
>>   diff --git a/controller/ovn-controller.c b/controller/ovn-controller.c
>> index f62303e3cb..c374bd0f33 100644
>> --- a/controller/ovn-controller.c
>> +++ b/controller/ovn-controller.c
>> @@ -91,6 +91,7 @@ static unixctl_cb_func debug_pause_execution;
>>   static unixctl_cb_func debug_resume_execution;
>>   static unixctl_cb_func debug_status_execution;
>>   static unixctl_cb_func debug_dump_local_bindings;
>> +static unixctl_cb_func debug_dump_local_template_vars;
>>   static unixctl_cb_func debug_dump_lflow_conj_ids;
>>   static unixctl_cb_func lflow_cache_flush_cmd;
>>   static unixctl_cb_func lflow_cache_show_stats_cmd;
>> @@ -170,6 +171,8 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
>>        *
>>        * Monitor IGMP_Groups for local chassis.
>>        *
>> +     * Monitor Template_Var for local chassis.
>> +     *
>>        * We always monitor patch ports because they allow us to see
>> the linkages
>>        * between related logical datapaths.  That way, when we know
>> that we have
>>        * a VIF on a particular logical switch, we immediately know to
>> monitor all
>> @@ -184,6 +187,7 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
>>       struct ovsdb_idl_condition ip_mcast =
>> OVSDB_IDL_CONDITION_INIT(&ip_mcast);
>>       struct ovsdb_idl_condition igmp = OVSDB_IDL_CONDITION_INIT(&igmp);
>>       struct ovsdb_idl_condition chprv =
>> OVSDB_IDL_CONDITION_INIT(&chprv);
>> +    struct ovsdb_idl_condition tv = OVSDB_IDL_CONDITION_INIT(&tv);
>>         /* Always monitor all logical datapath groups. Otherwise, DPG
>> updates may
>>        * be received *after* the lflows using it are seen by
>> ovn-controller.
>> @@ -201,6 +205,7 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
>>           ovsdb_idl_condition_add_clause_true(&ip_mcast);
>>           ovsdb_idl_condition_add_clause_true(&igmp);
>>           ovsdb_idl_condition_add_clause_true(&chprv);
>> +        ovsdb_idl_condition_add_clause_true(&tv);
>>           goto out;
>>       }
>>   @@ -239,6 +244,9 @@ update_sb_monitors(struct ovsdb_idl *ovnsb_idl,
>>           /* Monitors Chassis_Private record for current chassis only. */
>>           sbrec_chassis_private_add_clause_name(&chprv, OVSDB_F_EQ,
>>                                                 chassis->name);
>> +
>> +        sbrec_chassis_template_var_add_clause_chassis(&tv, OVSDB_F_EQ,
>> +                                                      chassis->name);
>>       } else {
>>           /* During initialization, we monitor all records in
>> Chassis_Private so
>>            * that we don't try to recreate existing ones. */
>> @@ -290,6 +298,7 @@ out:;
>>           sbrec_ip_multicast_set_condition(ovnsb_idl, &ip_mcast),
>>           sbrec_igmp_group_set_condition(ovnsb_idl, &igmp),
>>           sbrec_chassis_private_set_condition(ovnsb_idl, &chprv),
>> +        sbrec_chassis_template_var_set_condition(ovnsb_idl, &tv),
>>       };
>>         unsigned int expected_cond_seqno = 0;
>> @@ -307,6 +316,7 @@ out:;
>>       ovsdb_idl_condition_destroy(&ip_mcast);
>>       ovsdb_idl_condition_destroy(&igmp);
>>       ovsdb_idl_condition_destroy(&chprv);
>> +    ovsdb_idl_condition_destroy(&tv);
>>       return expected_cond_seqno;
>>   }
>>   @@ -986,7 +996,8 @@ ctrl_register_ovs_idl(struct ovsdb_idl *ovs_idl)
>>       SB_NODE(load_balancer, "load_balancer") \
>>       SB_NODE(fdb, "fdb") \
>>       SB_NODE(meter, "meter") \
>> -    SB_NODE(static_mac_binding, "static_mac_binding")
>> +    SB_NODE(static_mac_binding, "static_mac_binding") \
>> +    SB_NODE(chassis_template_var, "chassis_template_var")
>>     enum sb_engine_node {
>>   #define SB_NODE(NAME, NAME_STR) SB_##NAME,
>> @@ -1648,6 +1659,184 @@
>> runtime_data_sb_datapath_binding_handler(struct engine_node *node
>> OVS_UNUSED,
>>       return true;
>>   }
>>   +static void
>> +local_templates_to_string(const struct smap *local_templates,
>> +                          struct ds *out_data)
>> +{
>> +    const struct smap_node **nodes = smap_sort(local_templates);
>> +
>> +    for (size_t i = 0; i < smap_count(local_templates); i++) {
>> +        const struct smap_node *node = nodes[i];
>> +
>> +        ds_put_format(out_data, "name: '%s' value: '%s'\n",
>> +                      node->key, node->value);
>> +    }
>> +    free(nodes);
>> +}
>> +
>> +struct ed_type_template_vars {
>> +    struct smap local_templates;
>> +
>> +    bool change_tracked;
>> +    struct sset new;
>> +    struct sset deleted;
>> +    struct sset updated;
>> +};
>> +
>> +static void
>> +template_vars_init(struct ovsdb_idl_index *tv_index_by_chassis,
>> +                   const struct sbrec_chassis *chassis,
>> +                   struct smap *local_templates)
>> +{
>> +    const struct sbrec_chassis_template_var *tv;
>> +    struct sbrec_chassis_template_var *tv_key =
>> +        sbrec_chassis_template_var_index_init_row(tv_index_by_chassis);
>> +    sbrec_chassis_template_var_index_set_chassis(tv_key, chassis->name);
>> +
>> +    tv = sbrec_chassis_template_var_index_find(tv_index_by_chassis,
>> tv_key);
>> +    if (tv) {
>> +        smap_destroy(local_templates);
>> +        smap_clone(local_templates, &tv->variables);
>> +    }
>> +
>> +    sbrec_chassis_template_var_index_destroy_row(tv_key);
>> +}
>> +
>> +static void
>> +template_vars_update(const struct sbrec_chassis_template_var_table
>> *tv_table,
>> +                     const struct sbrec_chassis *chassis,
>> +                     struct smap *local_templates, struct sset *new,
>> +                     struct sset *deleted, struct sset *updated)
>> +{
>> +    const struct sbrec_chassis_template_var *tv;
>> +    struct smap_node *node;
>> +    SBREC_CHASSIS_TEMPLATE_VAR_TABLE_FOR_EACH_TRACKED (tv, tv_table) {
>> +        if (strcmp(tv->chassis, chassis->name)) {
>> +            continue;
>> +        }
>> +
>> +        if (sbrec_chassis_template_var_is_deleted(tv)) {
>> +            SMAP_FOR_EACH (node, local_templates) {
>> +                sset_add(deleted, node->key);
>> +            }
>> +        } else if (sbrec_chassis_template_var_is_new(tv)) {
>> +            ovs_assert(smap_count(local_templates) == 0);
>> +            SMAP_FOR_EACH (node, &tv->variables) {
>> +                sset_add(new, node->key);
>> +            }
>> +        } else {
>> +            SMAP_FOR_EACH (node, &tv->variables) {
>> +                struct smap_node *old = smap_get_node(local_templates,
>> +                                                      node->key);
>> +                if (old) {
>> +                    if (strcmp(old->value, node->value)) {
>> +                        sset_add(updated, node->key);
>> +                    }
>> +                } else {
>> +                    sset_add(new, node->key);
>> +                }
>> +            }
>> +            SMAP_FOR_EACH (node, local_templates) {
>> +                sset_add (deleted, node->key);
>> +            }
>> +        }
>> +
>> +        smap_destroy(local_templates);
>> +        smap_clone(local_templates, &tv->variables);
>> +        return;
>> +    }
>> +}
>> +
>> +static void *
>> +en_template_vars_init(struct engine_node *node OVS_UNUSED,
>> +                      struct engine_arg *arg OVS_UNUSED)
>> +{
>> +    struct ed_type_template_vars *tv_data = xzalloc(sizeof *tv_data);
>> +    smap_init(&tv_data->local_templates);
>> +    tv_data->change_tracked = false;
>> +    sset_init(&tv_data->new);
>> +    sset_init(&tv_data->deleted);
>> +    sset_init(&tv_data->updated);
>> +    return tv_data;
>> +}
>> +
>> +static void
>> +en_template_vars_run(struct engine_node *node, void *data)
>> +{
>> +    struct ed_type_template_vars *tv_data = data;
>> +
>> +    const struct ovsrec_open_vswitch_table *ovs_table =
>> +        EN_OVSDB_GET(engine_get_input("OVS_open_vswitch", node));
>> +    struct ovsdb_idl_index *sbrec_chassis_by_name =
>> +        engine_ovsdb_node_get_index(engine_get_input("SB_chassis",
>> node),
>> +                                    "name");
>> +    const struct sbrec_chassis *chassis =
>> +        chassis_lookup_by_name(sbrec_chassis_by_name,
>> +                               get_ovs_chassis_id(ovs_table));
>> +    struct ovsdb_idl_index
>> *sbrec_chassis_template_var_index_by_chassis =
>> +        engine_ovsdb_node_get_index(
>> +            engine_get_input("SB_chassis_template_var", node),
>> +            "chassis");
>> +
>> +    smap_clear(&tv_data->local_templates);
>> +    template_vars_init(sbrec_chassis_template_var_index_by_chassis,
>> +                       chassis, &tv_data->local_templates);
>> +    engine_set_node_state(node, EN_UPDATED);
>> +}
>> +
>> +static bool
>> +template_vars_sb_chassis_template_var_handler(struct engine_node *node,
>> +                                              void *data)
>> +{
>> +    struct ed_type_template_vars *tv_data = data;
>> +
>> +    const struct sbrec_chassis_template_var_table *tv_table =
>> +        EN_OVSDB_GET(engine_get_input("SB_chassis_template_var", node));
>> +    const struct ovsrec_open_vswitch_table *ovs_table =
>> +        EN_OVSDB_GET(engine_get_input("OVS_open_vswitch", node));
>> +    struct ovsdb_idl_index *sbrec_chassis_by_name =
>> +        engine_ovsdb_node_get_index(engine_get_input("SB_chassis",
>> node),
>> +                                    "name");
>> +    const struct sbrec_chassis *chassis =
>> +        chassis_lookup_by_name(sbrec_chassis_by_name,
>> +                               get_ovs_chassis_id(ovs_table));
>> +
>> +    template_vars_update(tv_table, chassis, &tv_data->local_templates,
>> +                         &tv_data->new, &tv_data->deleted,
>> &tv_data->updated);
>> +
>> +    if (!sset_is_empty(&tv_data->new) ||
>> !sset_is_empty(&tv_data->deleted) ||
>> +            !sset_is_empty(&tv_data->updated)) {
>> +        engine_set_node_state(node, EN_UPDATED);
>> +    } else {
>> +        engine_set_node_state(node, EN_UNCHANGED);
>> +    }
>> +
>> +    tv_data->change_tracked = true;
>> +    return true;
>> +}
>> +
>> +static void
>> +en_template_vars_clear_tracked_data(void *data)
>> +{
>> +    struct ed_type_template_vars *tv_data = data;
>> +
>> +    sset_clear(&tv_data->new);
>> +    sset_clear(&tv_data->deleted);
>> +    sset_clear(&tv_data->updated);
>> +    tv_data->change_tracked = false;
>> +}
>> +
>> +static void
>> +en_template_vars_cleanup(void *data)
>> +{
>> +    struct ed_type_template_vars *tv_data = data;
>> +
>> +    smap_destroy(&tv_data->local_templates);
>> +    sset_destroy(&tv_data->new);
>> +    sset_destroy(&tv_data->deleted);
>> +    sset_destroy(&tv_data->updated);
>> +}
>> +
>>   struct ed_type_addr_sets {
>>       struct shash addr_sets;
>>       bool change_tracked;
>> @@ -2702,6 +2891,9 @@ init_lflow_ctx(struct engine_node *node,
>>       struct ed_type_dhcp_options *dhcp_opts =
>>           engine_get_input_data("dhcp_options", node);
>>   +    struct ed_type_template_vars *template_vars =
>> +        engine_get_input_data("template_vars", node);
>> +
>>       l_ctx_in->sbrec_multicast_group_by_name_datapath =
>>           sbrec_mc_group_by_name_dp;
>>       l_ctx_in->sbrec_logical_flow_by_logical_datapath =
>> @@ -2734,6 +2926,7 @@ init_lflow_ctx(struct engine_node *node,
>>       l_ctx_in->dhcp_opts = &dhcp_opts->v4_opts;
>>       l_ctx_in->dhcpv6_opts = &dhcp_opts->v6_opts;
>>       l_ctx_in->controller_event_opts = &fo->controller_event_opts;
>> +    l_ctx_in->template_vars = &template_vars->local_templates;
>>         l_ctx_out->flow_table = &fo->flow_table;
>>       l_ctx_out->group_table = &fo->group_table;
>> @@ -3060,6 +3253,64 @@ lflow_output_port_groups_handler(struct
>> engine_node *node, void *data)
>>       return true;
>>   }
>>   +static bool
>> +lflow_output_template_vars_handler(struct engine_node *node, void *data)
>> +{
>> +    struct ed_type_template_vars *tv_data =
>> +        engine_get_input_data("template_vars", node);
>> +
>> +    struct ed_type_lflow_output *fo = data;
>> +    struct lflow_ctx_out l_ctx_out;
>> +    struct lflow_ctx_in l_ctx_in;
>> +    init_lflow_ctx(node, fo, &l_ctx_in, &l_ctx_out);
>> +
>> +    const char *res_name;
>> +    bool changed;
>> +
>> +    if (!tv_data->change_tracked) {
>> +        return false;
>> +    }
>> +
>> +    SSET_FOR_EACH (res_name, &tv_data->deleted) {
>> +        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_in, &l_ctx_out,
>> &changed)) {
>> +            return false;
>> +        }
>> +        if (changed) {
>> +            engine_set_node_state(node, EN_UPDATED);
>> +        }
>> +    }
>> +    SSET_FOR_EACH (res_name, &tv_data->updated) {
>> +        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_in, &l_ctx_out,
>> &changed)) {
>> +            return false;
>> +        }
>> +        if (changed) {
>> +            engine_set_node_state(node, EN_UPDATED);
>> +        }
>> +    }
>> +    SSET_FOR_EACH (res_name, &tv_data->new) {
>> +        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_in, &l_ctx_out,
>> &changed)) {
>> +            return false;
>> +        }
>> +        if (changed) {
>> +            engine_set_node_state(node, EN_UPDATED);
>> +        }
>> +    }
>> +
>> +    return true;
>> +}
>> +
>>   static bool
>>   lflow_output_runtime_data_handler(struct engine_node *node,
>>                                     void *data OVS_UNUSED)
>> @@ -3646,6 +3897,9 @@ main(int argc, char *argv[])
>>       struct ovsdb_idl_index *sbrec_static_mac_binding_by_datapath
>>           = ovsdb_idl_index_create1(ovnsb_idl_loop.idl,
>>                                    
>> &sbrec_static_mac_binding_col_datapath);
>> +    struct ovsdb_idl_index *sbrec_chassis_template_var_index_by_chassis
>> +        = ovsdb_idl_index_create1(ovnsb_idl_loop.idl,
>> +                                 
>> &sbrec_chassis_template_var_col_chassis);
>>         ovsdb_idl_track_add_all(ovnsb_idl_loop.idl);
>>       ovsdb_idl_omit_alert(ovnsb_idl_loop.idl,
>> @@ -3707,6 +3961,7 @@ main(int argc, char *argv[])
>>         /* Define inc-proc-engine nodes. */
>>       ENGINE_NODE(sb_ro, "sb_ro");
>> +    ENGINE_NODE_WITH_CLEAR_TRACK_DATA(template_vars, "template_vars");
>>       ENGINE_NODE_WITH_CLEAR_TRACK_DATA_IS_VALID(ct_zones, "ct_zones");
>>       ENGINE_NODE_WITH_CLEAR_TRACK_DATA(ovs_interface_shadow,
>>                                         "ovs_interface_shadow");
>> @@ -3733,6 +3988,10 @@ main(int argc, char *argv[])
>>   #undef OVS_NODE
>>         /* Add dependencies between inc-proc-engine nodes. */
>> +    engine_add_input(&en_template_vars, &en_ovs_open_vswitch, NULL);
>> +    engine_add_input(&en_template_vars, &en_sb_chassis, NULL);
>> +    engine_add_input(&en_template_vars, &en_sb_chassis_template_var,
>> +                     template_vars_sb_chassis_template_var_handler);
>>         engine_add_input(&en_addr_sets, &en_sb_address_set,
>>                        addr_sets_sb_address_set_handler);
>> @@ -3800,6 +4059,8 @@ main(int argc, char *argv[])
>>                        lflow_output_addr_sets_handler);
>>       engine_add_input(&en_lflow_output, &en_port_groups,
>>                        lflow_output_port_groups_handler);
>> +    engine_add_input(&en_lflow_output, &en_template_vars,
>> +                     lflow_output_template_vars_handler);
>>       engine_add_input(&en_lflow_output, &en_runtime_data,
>>                        lflow_output_runtime_data_handler);
>>       engine_add_input(&en_lflow_output, &en_non_vif_data,
>> @@ -3908,6 +4169,8 @@ main(int argc, char *argv[])
>>                                   sbrec_mac_binding_by_datapath);
>>       engine_ovsdb_node_add_index(&en_sb_static_mac_binding, "datapath",
>>                                   sbrec_static_mac_binding_by_datapath);
>> +    engine_ovsdb_node_add_index(&en_sb_chassis_template_var, "chassis",
>> +                               
>> sbrec_chassis_template_var_index_by_chassis);
>>         struct ed_type_lflow_output *lflow_output_data =
>>           engine_get_internal_data(&en_lflow_output);
>> @@ -3917,6 +4180,8 @@ main(int argc, char *argv[])
>>           engine_get_internal_data(&en_ct_zones);
>>       struct ed_type_runtime_data *runtime_data =
>>           engine_get_internal_data(&en_runtime_data);
>> +    struct ed_type_template_vars *template_vars_data =
>> +        engine_get_internal_data(&en_template_vars);
>>         ofctrl_init(&lflow_output_data->group_table,
>>                   &lflow_output_data->meter_table,
>> @@ -3978,6 +4243,10 @@ main(int argc, char *argv[])
>>                                debug_dump_lflow_conj_ids,
>>                                &lflow_output_data->conj_ids);
>>   +    unixctl_command_register("debug/dump-local-template-vars", "",
>> 0, 0,
>> +                             debug_dump_local_template_vars,
>> +                             &template_vars_data->local_templates);
>> +
>>       unixctl_command_register("debug/ignore-startup-delay", "", 0, 0,
>>                                debug_ignore_startup_delay, NULL);
>>   @@ -4354,9 +4623,12 @@ main(int argc, char *argv[])
>>                       engine_get_data(&en_addr_sets);
>>                   struct ed_type_port_groups *pg_data =
>>                       engine_get_data(&en_port_groups);
>> -                if (br_int && chassis && as_data && pg_data) {
>> +                struct ed_type_template_vars *tv_data =
>> +                    engine_get_data(&en_template_vars);
>> +                if (br_int && chassis && as_data && pg_data &&
>> tv_data) {
>>                       char *error = ofctrl_inject_pkt(br_int,
>> pending_pkt.flow_s,
>> -                        &as_data->addr_sets,
>> &pg_data->port_groups_cs_local);
>> +                        &as_data->addr_sets,
>> &pg_data->port_groups_cs_local,
>> +                        &tv_data->local_templates);
>>                       if (error) {
>>                          
>> unixctl_command_reply_error(pending_pkt.conn, error);
>>                           free(error);
>> @@ -4825,6 +5097,17 @@ debug_dump_lflow_conj_ids(struct unixctl_conn
>> *conn, int argc OVS_UNUSED,
>>       ds_destroy(&conj_ids_dump);
>>   }
>>   +static void
>> +debug_dump_local_template_vars(struct unixctl_conn *conn, int argc
>> OVS_UNUSED,
>> +                               const char *argv[] OVS_UNUSED, void
>> *local_vars)
>> +{
>> +    struct ds tv_str = DS_EMPTY_INITIALIZER;
>> +    ds_put_cstr(&tv_str, "Local template vars:\n");
>> +    local_templates_to_string(local_vars, &tv_str);
>> +    unixctl_command_reply(conn, ds_cstr(&tv_str));
>> +    ds_destroy(&tv_str);
>> +}
>> +
>>   static void
>>   debug_ignore_startup_delay(struct unixctl_conn *conn, int argc
>> OVS_UNUSED,
>>                              const char *argv[] OVS_UNUSED, void *arg
>> OVS_UNUSED)
>> diff --git a/include/ovn/expr.h b/include/ovn/expr.h
>> index 3b141b034c..80d95ff675 100644
>> --- a/include/ovn/expr.h
>> +++ b/include/ovn/expr.h
>> @@ -59,6 +59,7 @@
>>   #include "openvswitch/match.h"
>>   #include "openvswitch/meta-flow.h"
>>   #include "logical-fields.h"
>> +#include "smap.h"
>>     struct ds;
>>   struct expr;
>> @@ -521,7 +522,8 @@ union expr_constant {
>>       char *string;
>>   };
>>   -bool expr_constant_parse(struct lexer *, const struct expr_field *,
>> +bool expr_constant_parse(struct lexer *,
>> +                         const struct expr_field *,
>>                            union expr_constant *);
>>   void expr_constant_format(const union expr_constant *,
>>                             enum expr_constant_type, struct ds *);
>> diff --git a/include/ovn/lex.h b/include/ovn/lex.h
>> index ecb7ace243..516bbc2bd8 100644
>> --- a/include/ovn/lex.h
>> +++ b/include/ovn/lex.h
>> @@ -23,6 +23,8 @@
>>    * This is a simple lexical analyzer (or tokenizer) for OVN match
>> expressions
>>    * and ACLs. */
>>   +#include "smap.h"
>> +#include "sset.h"
>>   #include "openvswitch/meta-flow.h"
>>     struct ds;
>> @@ -37,7 +39,8 @@ enum lex_type {
>>       LEX_T_INTEGER,              /* 12345 or 1.2.3.4 or ::1 or
>> 01:02:03:04:05 */
>>       LEX_T_MASKED_INTEGER,       /* 12345/10 or 1.2.0.0/16 or ::2/127
>> or... */
>>       LEX_T_MACRO,                /* $NAME */
>> -    LEX_T_PORT_GROUP,            /* @NAME */
>> +    LEX_T_PORT_GROUP,           /* @NAME */
>> +    LEX_T_TEMPLATE,             /* ^NAME */
>>       LEX_T_ERROR,                /* invalid input */
>>         /* Bare tokens. */
>> @@ -86,9 +89,9 @@ struct lex_token {
>>       /* One of LEX_*. */
>>       enum lex_type type;
>>   -    /* Meaningful for LEX_T_ID, LEX_T_STRING, LEX_T_ERROR,
>> LEX_T_MACRO only.
>> -     * For these token types, 's' may point to 'buffer'; otherwise,
>> it points
>> -     * to malloc()ed memory owned by the token.
>> +    /* Meaningful for LEX_T_ID, LEX_T_STRING, LEX_T_ERROR, LEX_T_MACRO,
>> +     * LEX_T_TEMPLATE only.  For these token types, 's' may point to
>> 'buffer';
>> +     * otherwise, it points to malloc()ed memory owned by the token.
>>        *
>>        * Must be NULL for other token types.
>>        *
>> @@ -151,4 +154,8 @@ void lexer_syntax_error(struct lexer *, const char
>> *message, ...)
>>     char *lexer_steal_error(struct lexer *);
>>   +const char *lexer_parse_template_string(const char *s,
>> +                                        const struct smap
>> *template_vars,
>> +                                        struct sset *template_vars_ref,
>> +                                        char **out_expanded);
>>   #endif /* ovn/lex.h */
>> diff --git a/lib/actions.c b/lib/actions.c
>> index adbb42db4f..d7d4868835 100644
>> --- a/lib/actions.c
>> +++ b/lib/actions.c
>> @@ -258,8 +258,8 @@ add_prerequisite(struct action_context *ctx, const
>> char *prerequisite)
>>       struct expr *expr;
>>       char *error;
>>   -    expr = expr_parse_string(prerequisite, ctx->pp->symtab, NULL,
>> NULL,
>> -                             NULL, NULL, 0, &error);
>> +    expr = expr_parse_string(prerequisite, ctx->pp->symtab, NULL,
>> NULL, NULL,
>> +                             NULL, 0, &error);
>>       ovs_assert(!error);
>>       ctx->prereqs = expr_combine(EXPR_T_AND, ctx->prereqs, expr);
>>   }
>> @@ -4695,6 +4695,11 @@ parse_set_action(struct action_context *ctx)
>>   static bool
>>   parse_action(struct action_context *ctx)
>>   {
>> +    if (ctx->lexer->token.type == LEX_T_TEMPLATE) {
>> +        lexer_error(ctx->lexer, "Unexpanded template.");
>> +        return false;
>> +    }
>> +
>>       if (ctx->lexer->token.type != LEX_T_ID) {
>>           lexer_syntax_error(ctx->lexer, NULL);
>>           return false;
>> diff --git a/lib/expr.c b/lib/expr.c
>> index d1f9d28ca6..43f44771cf 100644
>> --- a/lib/expr.c
>> +++ b/lib/expr.c
>> @@ -695,7 +695,9 @@ parse_field(struct expr_context *ctx, struct
>> expr_field *f)
>>           return false;
>>       }
>>   -    symbol = shash_find_data(ctx->symtab, ctx->lexer->token.s);
>> +    symbol = ctx->symtab
>> +             ? shash_find_data(ctx->symtab, ctx->lexer->token.s)
>> +             : NULL;
>>       if (!symbol) {
>>           lexer_syntax_error(ctx->lexer, "expecting field name");
>>           return false;
>> @@ -894,7 +896,10 @@ parse_constant(struct expr_context *ctx, struct
>> expr_constant_set *cs,
>>           cs->as_name = NULL;
>>       }
>>   -    if (ctx->lexer->token.type == LEX_T_STRING) {
>> +    if (ctx->lexer->token.type == LEX_T_TEMPLATE) {
>> +        lexer_error(ctx->lexer, "Unexpanded template.");
>> +        return false;
>> +    } else if (ctx->lexer->token.type == LEX_T_STRING) {
>>           if (!assign_constant_set_type(ctx, cs, EXPR_C_STRING)) {
>>               return false;
>>           }
>> @@ -978,7 +983,9 @@ expr_constant_parse(struct lexer *lexer, const
>> struct expr_field *f,
>>           return false;
>>       }
>>   -    struct expr_context ctx = { .lexer = lexer };
>> +    struct expr_context ctx = {
>> +        .lexer = lexer,
>> +    };
>>         struct expr_constant_set cs;
>>       memset(&cs, 0, sizeof cs);
>> @@ -1332,7 +1339,10 @@ expr_parse_primary(struct expr_context *ctx,
>> bool *atomic)
>>           return e;
>>       }
>>   -    if (ctx->lexer->token.type == LEX_T_ID) {
>> +    if (ctx->lexer->token.type == LEX_T_TEMPLATE) {
>> +        lexer_error(ctx->lexer, "Unexpanded template.");
>> +        return NULL;
>> +    } else if (ctx->lexer->token.type == LEX_T_ID) {
>>           struct expr_field f;
>>           enum expr_relop r;
>>           struct expr_constant_set c;
>> diff --git a/lib/lex.c b/lib/lex.c
>> index c84d52aa8d..7b44bc3537 100644
>> --- a/lib/lex.c
>> +++ b/lib/lex.c
>> @@ -235,6 +235,10 @@ lex_token_format(const struct lex_token *token,
>> struct ds *s)
>>           ds_put_format(s, "@%s", token->s);
>>           break;
>>   +    case LEX_T_TEMPLATE:
>> +        ds_put_format(s, "^%s", token->s);
>> +        break;
>> +
>>       case LEX_T_LPAREN:
>>           ds_put_cstr(s, "(");
>>           break;
>> @@ -588,6 +592,18 @@ lex_parse_port_group(const char *p, struct
>> lex_token *token)
>>       return lex_parse_id(p, LEX_T_PORT_GROUP, token);
>>   }
>>   +static const char *
>> +lex_parse_template(const char *p, struct lex_token *token)
>> +{
>> +    p++;
>> +    if (!lex_is_id1(*p)) {
>> +        lex_error(token, "`^' must be followed by a valid identifier.");
>> +        return p;
>> +    }
>> +
>> +    return lex_parse_id(p, LEX_T_TEMPLATE, token);
>> +}
>> +
>>   /* Initializes 'token' and parses the first token from the beginning of
>>    * null-terminated string 'p' into 'token'.  Stores a pointer to the
>> start of
>>    * the token (after skipping white space and comments, if any) into
>> '*startp'.
>> @@ -766,6 +782,10 @@ next:
>>           p = lex_parse_port_group(p, token);
>>           break;
>>   +    case '^':
>> +        p = lex_parse_template(p, token);
>> +        break;
>> +
>>       case ':':
>>           if (p[1] != ':') {
>>               token->type = LEX_T_COLON;
>> @@ -1031,3 +1051,40 @@ lexer_steal_error(struct lexer *lexer)
>>       lexer->error = NULL;
>>       return error;
>>   }
>> +
>> +/* Takes ownership of 's' and expands all templates that are encountered
>> + * in the contents of 's', if possible.  Adds the encountered
>> template names
>> + * to 'template_vars_ref'.
>> + */
>> +const char *
>> +lexer_parse_template_string(const char *s, const struct smap
>> *template_vars,
>> +                            struct sset *template_vars_ref,
>> +                            char **out_expanded)
>> +{
>> +    /* No '^' means no templates. */
>> +    if (!strchr(s, '^')) {
>> +        *out_expanded = NULL;
>> +        return s;
>> +    }
>> +
>> +    struct ds expanded = DS_EMPTY_INITIALIZER;
>> +
>> +    struct lexer lexer;
>> +    lexer_init(&lexer, s);
>> +
>> +    while (lexer_get(&lexer) != LEX_T_END) {
>> +        if (lexer.token.type == LEX_T_TEMPLATE) {
>> +            ds_put_cstr(&expanded, smap_get_def(template_vars,
>> lexer.token.s,
>> +                                                lexer.token.s));
>> +            if (template_vars_ref) {
>> +                sset_add(template_vars_ref, lexer.token.s);
>> +            }
>> +        } else {
>> +            lex_token_format(&lexer.token, &expanded);
>> +        }
>> +    }
>> +
>> +    lexer_destroy(&lexer);
>> +    *out_expanded = ds_steal_cstr(&expanded);
>> +    return *out_expanded;
> 
> It seems odd to me that lexer_parse_template_string() sets the out
> parameter but also gives the same pointer as a return value. I could
> foresee a situation where someone makes the mistake of thinking these
> are two distinct values and causes chaos as a result (e.g. free the
> output parameter and then try to use the return value after). IMO, this
> should follow the same pattern that other parsers in OVN use. That is,
> it should use the output parameter for the desired outcome and return an
> error string in the case something unexpected occurs.
> 

You're right, this isn't great.  I'm not sure how to best implement this
however.  I went with something a bit different in v3 (a new lex_str
type wrapping in-heap or on-stack allocated lexer strings).  I guess we
should probably continue the discussion there.

>> +}
>> diff --git a/lib/objdep.c b/lib/objdep.c
>> index 092d4af261..06cf126f12 100644
>> --- a/lib/objdep.c
>> +++ b/lib/objdep.c
>> @@ -245,6 +245,7 @@ objdep_type_name(enum objdep_type type)
>>           [OBJDEP_TYPE_PORTGROUP] = "Port_Group",
>>           [OBJDEP_TYPE_PORTBINDING] = "Port_Binding",
>>           [OBJDEP_TYPE_MC_GROUP] = "Multicast_Group",
>> +        [OBJDEP_TYPE_TEMPLATE] = "Template",
>>       };
>>         ovs_assert(type < OBJDEP_TYPE_MAX);
>> diff --git a/lib/objdep.h b/lib/objdep.h
>> index 50c7b01ef1..1ea781947c 100644
>> --- a/lib/objdep.h
>> +++ b/lib/objdep.h
>> @@ -26,6 +26,7 @@ enum objdep_type {
>>       OBJDEP_TYPE_PORTGROUP,
>>       OBJDEP_TYPE_PORTBINDING,
>>       OBJDEP_TYPE_MC_GROUP,
>> +    OBJDEP_TYPE_TEMPLATE,
>>       OBJDEP_TYPE_MAX,
>>   };
>>   diff --git a/tests/ovn-controller.at b/tests/ovn-controller.at
>> index 3c3fb31c7c..5fb6c65eba 100644
>> --- a/tests/ovn-controller.at
>> +++ b/tests/ovn-controller.at
>> @@ -2337,3 +2337,53 @@ done
>>   AT_CHECK([grep "deleted interface patch" hv1/ovs-vswitchd.log], [1],
>> [ignore])
>>   OVN_CLEANUP([hv1])
>>   AT_CLEANUP
>> +
>> +OVN_FOR_EACH_NORTHD([
>> +AT_SETUP([ovn-controller - Local Chassis_Template_Var updates])
>> +AT_KEYWORDS([templates])
>> +ovn_start
>> +
>> +net_add n1
>> +sim_add hv
>> +as hv
>> +ovs-vsctl \
>> +    -- add-br br-phys \
>> +    -- add-br br-eth0 \
>> +    -- add-br br-eth1 \
>> +    -- add-br br-eth2
>> +ovn_attach n1 br-phys 192.168.0.1
>> +
>> +m4_define([GET_LOCAL_TEMPLATE_VARS],
>> +    [ovn-appctl debug/dump-local-template-vars | grep -v 'Local
>> template vars'])
>> +
>> +dnl Expect no local vars initially.
>> +AT_CHECK([GET_LOCAL_TEMPLATE_VARS], [1], [])
>> +
>> +AT_CHECK([ovn-nbctl --wait=hv create Chassis_Template_Var
>> chassis="hv"], [0], [ignore])
>> +AT_CHECK([GET_LOCAL_TEMPLATE_VARS], [1], [])
>> +
>> +dnl Expect new vars to be handled properly.
>> +check ovn-nbctl --wait=hv set Chassis_Template_Var hv
>> variables:foo=foo-hv variables:bar=bar-hv
>> +AT_CHECK([GET_LOCAL_TEMPLATE_VARS], [0], [dnl
>> +name: 'bar' value: 'bar-hv'
>> +name: 'foo' value: 'foo-hv'
>> +])
>> +
>> +dnl Expect var updates to be handled properly.
>> +check ovn-nbctl --wait=hv set Chassis_Template_Var hv
>> variables:bar=bar-new-hv
>> +AT_CHECK([GET_LOCAL_TEMPLATE_VARS], [0], [dnl
>> +name: 'bar' value: 'bar-new-hv'
>> +name: 'foo' value: 'foo-hv'
>> +])
>> +
>> +dnl Expect var deletions to be handled properly.
>> +check ovn-nbctl --wait=hv remove Chassis_Template_Var hv variables bar
>> +AT_CHECK([GET_LOCAL_TEMPLATE_VARS], [0], [dnl
>> +name: 'foo' value: 'foo-hv'
>> +])
>> +
>> +check ovn-nbctl --wait=hv remove Chassis_Template_Var hv variables foo
>> +AT_CHECK([GET_LOCAL_TEMPLATE_VARS], [1], [])
>> +
>> +AT_CLEANUP
>> +])
>> diff --git a/tests/ovn.at b/tests/ovn.at
>> index f8b8db4df8..2a59235c59 100644
>> --- a/tests/ovn.at
>> +++ b/tests/ovn.at
>> @@ -259,7 +259,7 @@ fe:x => error("Invalid numeric constant.")
>>   & => error("`&' is only valid as part of `&&'.")
>>   | => error("`|' is only valid as part of `||'.")
>>   -^ => error("Invalid character `^' in input.")
>> +^ => error("`^' must be followed by a valid identifier.")
>>   ])
>>   AT_CAPTURE_FILE([input.txt])
>>   sed 's/ =>.*//' test-cases.txt > input.txt
>> @@ -32975,3 +32975,81 @@ check ovn-nbctl --wait=hv sync
>>   OVN_CLEANUP([hv1])
>>   AT_CLEANUP
>>   ])
>> +
>> +OVN_FOR_EACH_NORTHD([
>> +AT_SETUP([Logical flows 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])
>> +
>> +check ovn-nbctl pg-add pg1 lsp1 lsp2
>> +AT_CHECK([ovn-nbctl create address_set name=as1
>> addresses=\"1.1.1.1\",\"1.1.1.2\"], [0], [ignore])
>> +
>> +dnl Create an ACL that uses an "uninstantiated" template.
>> +check ovn-nbctl acl-add sw from-lport 1 "ip4.src == 42.42.42.42 &&
>> ^CONDITION" allow
>> +
>> +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 ACL is not translated to OpenFlow.
>> +as hv1
>> +AT_CHECK([ovs-ofctl dump-flows br-int | grep '42\.42\.42\.42'], [1], [])
>> +
>> +dnl Create a Chassis_Template_Var mapping for CONDITION.
>> +check ovn-nbctl --wait=hv set Chassis_Template_Var hv1
>> variables:CONDITION='inport == @pg1'
>> +
>> +lsp1=0x$(fetch_column Port_Binding tunnel_key logical_port=lsp1)
>> +lsp2=0x$(fetch_column Port_Binding tunnel_key logical_port=lsp2)
>> +
>> +dnl Ensure the ACL is translated to OpenFlows expanding pg1.
>> +as hv1
>> +AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int | grep
>> '42\.42\.42\.42' | ofctl_strip_all], [0], [dnl
>> + table=16,
>> priority=1001,ip,reg14=$lsp1,metadata=0x1,nw_src=42.42.42.42
>> actions=resubmit(,17)
>> + table=16,
>> priority=1001,ip,reg14=$lsp2,metadata=0x1,nw_src=42.42.42.42
>> actions=resubmit(,17)
>> +])
>> +
>> +dnl Remove a port from pg1 and expect OpenFlows to be correctly updated.
>> +check ovn-nbctl --wait=hv pg-set-ports pg1 lsp2
>> +AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int | grep
>> '42\.42\.42\.42' | ofctl_strip_all], [0], [dnl
>> + table=16,
>> priority=1001,ip,reg14=$lsp2,metadata=0x1,nw_src=42.42.42.42
>> actions=resubmit(,17)
>> +])
>> +
>> +dnl Change the Chassis_Template_Var mapping to use the address set.
>> +check ovn-nbctl --wait=hv set Chassis_Template_Var hv1
>> variables:CONDITION='ip4.dst == $as1'
>> +
>> +dnl Ensure the ACL is translated to OpenFlows expanding as1.
>> +as hv1
>> +AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int | grep
>> '42\.42\.42\.42' | ofctl_strip_all], [0], [dnl
>> + table=16,
>> priority=1001,ip,metadata=0x1,nw_src=42.42.42.42,nw_dst=1.1.1.1
>> actions=resubmit(,17)
>> + table=16,
>> priority=1001,ip,metadata=0x1,nw_src=42.42.42.42,nw_dst=1.1.1.2
>> actions=resubmit(,17)
>> +])
>> +
>> +dnl Remove an IP from AS1 and expect OpenFlows to be correctly updated.
>> +check ovn-nbctl set address_set as1 addresses=\"1.1.1.1\"
>> +AT_CHECK_UNQUOTED([ovs-ofctl dump-flows br-int | grep
>> '42\.42\.42\.42' | ofctl_strip_all], [0], [dnl
>> + table=16,
>> priority=1001,ip,metadata=0x1,nw_src=42.42.42.42,nw_dst=1.1.1.1
>> actions=resubmit(,17)
>> +])
>> +
>> +dnl Remove the mapping and expect OpenFlows to be removed.
>> +check ovn-nbctl --wait=hv clear Chassis_Template_Var hv1 variables
>> +AT_CHECK([ovs-ofctl dump-flows br-int | grep '42\.42\.42\.42'], [1], [])
>> +
>> +OVN_CLEANUP([hv1])
>> +AT_CLEANUP
>> +])
>> diff --git a/tests/test-ovn.c b/tests/test-ovn.c
>> index a241f150d1..c422bafd70 100644
>> --- a/tests/test-ovn.c
>> +++ b/tests/test-ovn.c
>> @@ -291,12 +291,14 @@ test_parse_expr__(int steps)
>>       struct shash symtab;
>>       struct shash addr_sets;
>>       struct shash port_groups;
>> +    struct smap template_vars;
>>       struct simap ports;
>>       struct ds input;
>>         create_symtab(&symtab);
>>       create_addr_sets(&addr_sets);
>>       create_port_groups(&port_groups);
>> +    smap_init(&template_vars);
>>         simap_init(&ports);
>>       simap_put(&ports, "eth0", 5);
>> @@ -355,6 +357,7 @@ test_parse_expr__(int steps)
>>       shash_destroy(&addr_sets);
>>       expr_const_sets_destroy(&port_groups);
>>       shash_destroy(&port_groups);
>> +    smap_destroy(&template_vars);
>>   }
>>     static void
>> @@ -913,8 +916,8 @@ test_tree_shape_exhaustively(struct expr *expr,
>> struct shash *symtab,
>>               expr_format(expr, &s);
>>                 char *error;
>> -            modified = expr_parse_string(ds_cstr(&s), symtab, NULL,
>> -                                         NULL, NULL, NULL, 0, &error);
>> +            modified = expr_parse_string(ds_cstr(&s), symtab, NULL,
>> NULL, NULL,
>> +                                         NULL, 0, &error);
>>               if (error) {
>>                   fprintf(stderr, "%s fails to parse (%s)\n",
>>                           ds_cstr(&s), error);
>> @@ -1286,6 +1289,8 @@ test_parse_actions(struct ovs_cmdl_context *ctx
>> OVS_UNUSED)
>>       struct ds input;
>>       bool ok = true;
>>   +    struct smap template_vars = SMAP_INITIALIZER(&template_vars);
>> +
>>       create_symtab(&symtab);
>>       create_gen_opts(&dhcp_opts, &dhcpv6_opts, &nd_ra_opts,
>> &event_opts);
>>   @@ -1322,7 +1327,11 @@ test_parse_actions(struct ovs_cmdl_context
>> *ctx OVS_UNUSED)
>>               .n_tables = 24,
>>               .cur_ltable = 10,
>>           };
>> -        error = ovnacts_parse_string(ds_cstr(&input), &pp, &ovnacts,
>> &prereqs);
>> +        char *exp_input_expanded = NULL;
>> +        const char *exp_input =
>> +            lexer_parse_template_string(ds_cstr(&input),
>> &template_vars, NULL,
>> +                                        &exp_input_expanded);
>> +        error = ovnacts_parse_string(exp_input, &pp, &ovnacts,
>> &prereqs);
>>           if (!error) {
>>               /* Convert the parsed representation back to a string
>> and print it,
>>                * if it's different from the input. */
>> @@ -1409,6 +1418,7 @@ test_parse_actions(struct ovs_cmdl_context *ctx
>> OVS_UNUSED)
>>           expr_destroy(prereqs);
>>           ovnacts_free(ovnacts.data, ovnacts.size);
>>           ofpbuf_uninit(&ovnacts);
>> +        free(exp_input_expanded);
>>       }
>>       ds_destroy(&input);
>>   @@ -1419,6 +1429,7 @@ test_parse_actions(struct ovs_cmdl_context
>> *ctx OVS_UNUSED)
>>       dhcp_opts_destroy(&dhcpv6_opts);
>>       nd_ra_opts_destroy(&nd_ra_opts);
>>       controller_event_opts_destroy(&event_opts);
>> +    smap_destroy(&template_vars);
>>       ovn_extend_table_destroy(&group_table);
>>       ovn_extend_table_destroy(&meter_table);
>>       exit(ok ? EXIT_SUCCESS : EXIT_FAILURE);
>> diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
>> index 6fa5137d95..b837884ccc 100644
>> --- a/utilities/ovn-trace.c
>> +++ b/utilities/ovn-trace.c
>> @@ -505,6 +505,7 @@ static struct hmap dhcp_opts;   /* Contains
>> "struct gen_opts_map"s. */
>>   static struct hmap dhcpv6_opts; /* Contains "struct gen_opts_map"s. */
>>   static struct hmap nd_ra_opts; /* Contains "struct gen_opts_map"s. */
>>   static struct controller_event_options event_opts;
>> +static struct smap template_vars;
>>     static struct ovntrace_datapath *
>>   ovntrace_datapath_find_by_sb_uuid(const struct uuid *sb_uuid)
>> @@ -955,9 +956,15 @@ parse_lflow_for_datapath(const struct
>> sbrec_logical_flow *sblf,
>>             char *error;
>>           struct expr *match;
>> -        match = expr_parse_string(sblf->match, &symtab, &address_sets,
>> +        char *match_expanded_s = NULL;
>> +        const char *match_s = lexer_parse_template_string(sblf->match,
>> +                                                         
>> &template_vars,
>> +                                                          NULL,
>> +                                                         
>> &match_expanded_s);
>> +        match = expr_parse_string(match_s, &symtab, &address_sets,
>>                                     &port_groups, NULL, NULL,
>> dp->tunnel_key,
>>                                     &error);
>> +        free(match_expanded_s);
>>           if (error) {
>>               VLOG_WARN("%s: parsing expression failed (%s)",
>>                         sblf->match, error);
>> @@ -980,7 +987,12 @@ parse_lflow_for_datapath(const struct
>> sbrec_logical_flow *sblf,
>>           uint64_t stub[1024 / 8];
>>           struct ofpbuf ovnacts = OFPBUF_STUB_INITIALIZER(stub);
>>           struct expr *prereqs;
>> -        error = ovnacts_parse_string(sblf->actions, &pp, &ovnacts,
>> &prereqs);
>> +        char *actions_expanded_s = NULL;
>> +        const char *actions_s =
>> +            lexer_parse_template_string(sblf->actions, &template_vars,
>> +                                        NULL, &actions_expanded_s);
>> +        error = ovnacts_parse_string(actions_s, &pp, &ovnacts,
>> &prereqs);
>> +        free(actions_expanded_s);
>>           if (error) {
>>               VLOG_WARN("%s: parsing actions failed (%s)",
>> sblf->actions, error);
>>               free(error);
>> @@ -1078,6 +1090,7 @@ read_gen_opts(void)
>>       nd_ra_opts_init(&nd_ra_opts);
>>         controller_event_opts_init(&event_opts);
>> +    smap_init(&template_vars);
>>   }
>>     static void
>> @@ -3420,9 +3433,15 @@ trace_parse(const char *dp_s, const char *flow_s,
>>            *
>>            * First make sure that the expression parses. */
>>           char *error;
>> -        struct expr *e = expr_parse_string(flow_s, &symtab,
>> &address_sets,
>> +        char *flow_expanded_s = NULL;
>> +        const char *flow_exp_s = lexer_parse_template_string(flow_s,
>> +                                                            
>> &template_vars,
>> +                                                             NULL,
>> +                                                            
>> &flow_expanded_s);
>> +        struct expr *e = expr_parse_string(flow_exp_s, &symtab,
>> &address_sets,
>>                                              &port_groups, NULL, NULL, 0,
>>                                              &error);
>> +        free(flow_expanded_s);
>>           if (!e) {
>>               return trace_parse_error(error);
>>           }
>> @@ -3447,9 +3466,15 @@ trace_parse(const char *dp_s, const char *flow_s,
>>           free(port_name);
>>       }
>>   -    char *error = expr_parse_microflow(flow_s, &symtab, &address_sets,
>> +    char *flow_expanded_s = NULL;
>> +    const char *flow_exp_s = lexer_parse_template_string(flow_s,
>> +                                                         &template_vars,
>> +                                                         NULL,
>> +                                                        
>> &flow_expanded_s);
>> +    char *error = expr_parse_microflow(flow_exp_s, &symtab,
>> &address_sets,
>>                                          &port_groups,
>> ovntrace_lookup_port,
>>                                          *dpp, uflow);
>> +    free(flow_expanded_s);
>>       if (error) {
>>           return trace_parse_error(error);
>>       }
>>
> 

Thanks,
Dumitru

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

Reply via email to