net/ena: extend logs for invalid request ID resets
[dpdk.git] / lib / pipeline / rte_swx_ctl.c
index 5d04e75..f52ccff 100644 (file)
@@ -10,6 +10,8 @@
 #include <rte_common.h>
 #include <rte_byteorder.h>
 
+#include <rte_swx_table_selector.h>
+
 #include "rte_swx_ctl.h"
 
 #define CHECK(condition, err_code)                                             \
@@ -89,11 +91,58 @@ struct table {
        uint32_t n_delete;
 };
 
+struct selector {
+       /* Selector table info. */
+       struct rte_swx_ctl_selector_info info;
+
+       /* group_id field. */
+       struct rte_swx_ctl_table_match_field_info group_id_field;
+
+       /* selector fields. */
+       struct rte_swx_ctl_table_match_field_info *selector_fields;
+
+       /* member_id field. */
+       struct rte_swx_ctl_table_match_field_info member_id_field;
+
+       /* Current selector table. Array of info.n_groups_max elements.*/
+       struct rte_swx_table_selector_group **groups;
+
+       /* Pending selector table subject to the next commit. Array of info.n_groups_max elements.
+        */
+       struct rte_swx_table_selector_group **pending_groups;
+
+       /* Valid flag per group. Array of n_groups_max elements. */
+       int *groups_added;
+
+       /* Pending delete flag per group. Group deletion is subject to the next commit. Array of
+        * info.n_groups_max elements.
+        */
+       int *groups_pending_delete;
+
+       /* Params. */
+       struct rte_swx_table_selector_params params;
+};
+
+struct learner {
+       struct rte_swx_ctl_learner_info info;
+       struct rte_swx_ctl_table_match_field_info *mf;
+       struct rte_swx_ctl_table_action_info *actions;
+       uint32_t action_data_size;
+
+       /* The pending default action: this is NOT the current default action;
+        * this will be the new default action after the next commit, if the
+        * next commit operation is successful.
+        */
+       struct rte_swx_table_entry *pending_default;
+};
+
 struct rte_swx_ctl_pipeline {
        struct rte_swx_ctl_pipeline_info info;
        struct rte_swx_pipeline *p;
        struct action *actions;
        struct table *tables;
+       struct selector *selectors;
+       struct learner *learners;
        struct rte_swx_table_state *ts;
        struct rte_swx_table_state *ts_next;
        int numa_node;
@@ -183,9 +232,6 @@ table_params_get(struct rte_swx_ctl_pipeline *ctl, uint32_t table_id)
 
                if (n_match_fields_em == table->info.n_match_fields)
                        match_type = RTE_SWX_TABLE_MATCH_EXACT;
-               else if ((n_match_fields_em == table->info.n_match_fields - 1) &&
-                        (last->match_type == RTE_SWX_TABLE_MATCH_LPM))
-                       match_type = RTE_SWX_TABLE_MATCH_LPM;
 
                /* key_offset. */
                key_offset = first->offset / 8;
@@ -312,52 +358,48 @@ table_entry_check(struct rte_swx_ctl_pipeline *ctl,
 
        CHECK(entry, EINVAL);
 
-       if (key_check) {
-               if (table->is_stub) {
-                       /* key. */
-                       CHECK(!entry->key, EINVAL);
-
-                       /* key_mask. */
-                       CHECK(!entry->key_mask, EINVAL);
-               } else {
-                       /* key. */
-                       CHECK(entry->key, EINVAL);
-
-                       /* key_mask. */
-                       switch (table->params.match_type) {
-                       case RTE_SWX_TABLE_MATCH_WILDCARD:
-                               break;
-
-                       case RTE_SWX_TABLE_MATCH_LPM:
-                               /* TBD Check that key mask is prefix. */
-                               break;
-
-                       case RTE_SWX_TABLE_MATCH_EXACT:
-                               status = table_entry_key_check_em(table, entry);
-                               if (status)
-                                       return status;
-                               break;
+       if (key_check && !table->is_stub) {
+               /* key. */
+               CHECK(entry->key, EINVAL);
 
-                       default:
-                               CHECK(0, EINVAL);
-                       }
+               /* key_mask. */
+               if (table->params.match_type == RTE_SWX_TABLE_MATCH_EXACT) {
+                       status = table_entry_key_check_em(table, entry);
+                       if (status)
+                               return status;
                }
        }
 
        if (data_check) {
                struct action *a;
+               struct rte_swx_ctl_table_action_info *tai;
                uint32_t i;
 
                /* action_id. */
-               for (i = 0; i < table->info.n_actions; i++)
-                       if (entry->action_id == table->actions[i].action_id)
+               for (i = 0; i < table->info.n_actions; i++) {
+                       tai = &table->actions[i];
+
+                       if (entry->action_id == tai->action_id)
                                break;
+               }
 
                CHECK(i < table->info.n_actions, EINVAL);
 
                /* action_data. */
                a = &ctl->actions[entry->action_id];
                CHECK(!(a->data_size && !entry->action_data), EINVAL);
+
+               /* When both key_check and data_check are true, we are interested in both the entry
+                * key and data, which means the operation is _regular_ table entry add.
+                */
+               if (key_check && !tai->action_is_for_table_entries)
+                       return -EINVAL;
+
+               /* When key_check is false while data_check is true, we are only interested in the
+                * entry data, which means the operation is _default_ table entry add.
+                */
+               if (!key_check && !tai->action_is_for_default_entry)
+                       return -EINVAL;
        }
 
        return 0;
@@ -710,17 +752,285 @@ table_free(struct rte_swx_ctl_pipeline *ctl)
 }
 
 static void
-table_state_free(struct rte_swx_ctl_pipeline *ctl)
+selector_group_members_free(struct selector *s, uint32_t group_id)
+{
+       struct rte_swx_table_selector_group *group = s->groups[group_id];
+
+       if (!group)
+               return;
+
+       for ( ; ; ) {
+               struct rte_swx_table_selector_member *m;
+
+               m = TAILQ_FIRST(&group->members);
+               if (!m)
+                       break;
+
+               TAILQ_REMOVE(&group->members, m, node);
+               free(m);
+       }
+
+       free(group);
+       s->groups[group_id] = NULL;
+}
+
+static void
+selector_pending_group_members_free(struct selector *s, uint32_t group_id)
+{
+       struct rte_swx_table_selector_group *group = s->pending_groups[group_id];
+
+       if (!group)
+               return;
+
+       for ( ; ; ) {
+               struct rte_swx_table_selector_member *m;
+
+               m = TAILQ_FIRST(&group->members);
+               if (!m)
+                       break;
+
+               TAILQ_REMOVE(&group->members, m, node);
+               free(m);
+       }
+
+       free(group);
+       s->pending_groups[group_id] = NULL;
+}
+
+static int
+selector_group_duplicate_to_pending(struct selector *s, uint32_t group_id)
+{
+       struct rte_swx_table_selector_group *g, *gp;
+       struct rte_swx_table_selector_member *m;
+
+       selector_pending_group_members_free(s, group_id);
+
+       g = s->groups[group_id];
+       gp = s->pending_groups[group_id];
+
+       if (!gp) {
+               gp = calloc(1, sizeof(struct rte_swx_table_selector_group));
+               if (!gp)
+                       goto error;
+
+               TAILQ_INIT(&gp->members);
+
+               s->pending_groups[group_id] = gp;
+       }
+
+       if (!g)
+               return 0;
+
+       TAILQ_FOREACH(m, &g->members, node) {
+               struct rte_swx_table_selector_member *mp;
+
+               mp = calloc(1, sizeof(struct rte_swx_table_selector_member));
+               if (!mp)
+                       goto error;
+
+               memcpy(mp, m, sizeof(struct rte_swx_table_selector_member));
+
+               TAILQ_INSERT_TAIL(&gp->members, mp, node);
+       }
+
+       return 0;
+
+error:
+       selector_pending_group_members_free(s, group_id);
+       return -ENOMEM;
+}
+
+static void
+selector_free(struct rte_swx_ctl_pipeline *ctl)
+{
+       uint32_t i;
+
+       if (!ctl->selectors)
+               return;
+
+       for (i = 0; i < ctl->info.n_selectors; i++) {
+               struct selector *s = &ctl->selectors[i];
+               uint32_t i;
+
+               /* selector_fields. */
+               free(s->selector_fields);
+
+               /* groups. */
+               if (s->groups)
+                       for (i = 0; i < s->info.n_groups_max; i++)
+                               selector_group_members_free(s, i);
+
+               free(s->groups);
+
+               /* pending_groups. */
+               if (s->pending_groups)
+                       for (i = 0; i < s->info.n_groups_max; i++)
+                               selector_pending_group_members_free(s, i);
+
+               free(s->pending_groups);
+
+               /* groups_added. */
+               free(s->groups_added);
+
+               /* groups_pending_delete. */
+               free(s->groups_pending_delete);
+
+               /* params. */
+               free(s->params.selector_mask);
+       }
+
+       free(ctl->selectors);
+       ctl->selectors = NULL;
+}
+
+static struct selector *
+selector_find(struct rte_swx_ctl_pipeline *ctl, const char *selector_name)
+{
+       uint32_t i;
+
+       for (i = 0; i < ctl->info.n_selectors; i++) {
+               struct selector *s = &ctl->selectors[i];
+
+               if (!strcmp(selector_name, s->info.name))
+                       return s;
+       }
+
+       return NULL;
+}
+
+static int
+selector_params_get(struct rte_swx_ctl_pipeline *ctl, uint32_t selector_id)
+{
+       struct selector *s = &ctl->selectors[selector_id];
+       struct rte_swx_ctl_table_match_field_info *first = NULL, *last = NULL;
+       uint8_t *selector_mask = NULL;
+       uint32_t selector_size = 0, selector_offset = 0, i;
+
+       /* Find first (smallest offset) and last (biggest offset) match fields. */
+       first = &s->selector_fields[0];
+       last = &s->selector_fields[0];
+
+       for (i = 1; i < s->info.n_selector_fields; i++) {
+               struct rte_swx_ctl_table_match_field_info *f = &s->selector_fields[i];
+
+               if (f->offset < first->offset)
+                       first = f;
+
+               if (f->offset > last->offset)
+                       last = f;
+       }
+
+       /* selector_offset. */
+       selector_offset = first->offset / 8;
+
+       /* selector_size. */
+       selector_size = (last->offset + last->n_bits - first->offset) / 8;
+
+       /* selector_mask. */
+       selector_mask = calloc(1, selector_size);
+       if (!selector_mask)
+               return -ENOMEM;
+
+       for (i = 0; i < s->info.n_selector_fields; i++) {
+               struct rte_swx_ctl_table_match_field_info *f = &s->selector_fields[i];
+               uint32_t start;
+               size_t size;
+
+               start = (f->offset - first->offset) / 8;
+               size = f->n_bits / 8;
+
+               memset(&selector_mask[start], 0xFF, size);
+       }
+
+       /* Fill in. */
+       s->params.group_id_offset = s->group_id_field.offset / 8;
+       s->params.selector_size = selector_size;
+       s->params.selector_offset = selector_offset;
+       s->params.selector_mask = selector_mask;
+       s->params.member_id_offset = s->member_id_field.offset / 8;
+       s->params.n_groups_max = s->info.n_groups_max;
+       s->params.n_members_per_group_max = s->info.n_members_per_group_max;
+
+       return 0;
+}
+
+static void
+learner_pending_default_free(struct learner *l)
+{
+       if (!l->pending_default)
+               return;
+
+       free(l->pending_default->action_data);
+       free(l->pending_default);
+       l->pending_default = NULL;
+}
+
+
+static void
+learner_free(struct rte_swx_ctl_pipeline *ctl)
+{
+       uint32_t i;
+
+       if (!ctl->learners)
+               return;
+
+       for (i = 0; i < ctl->info.n_learners; i++) {
+               struct learner *l = &ctl->learners[i];
+
+               free(l->mf);
+               free(l->actions);
+
+               learner_pending_default_free(l);
+       }
+
+       free(ctl->learners);
+       ctl->learners = NULL;
+}
+
+static struct learner *
+learner_find(struct rte_swx_ctl_pipeline *ctl, const char *learner_name)
 {
        uint32_t i;
 
+       for (i = 0; i < ctl->info.n_learners; i++) {
+               struct learner *l = &ctl->learners[i];
+
+               if (!strcmp(learner_name, l->info.name))
+                       return l;
+       }
+
+       return NULL;
+}
+
+static uint32_t
+learner_action_data_size_get(struct rte_swx_ctl_pipeline *ctl, struct learner *l)
+{
+       uint32_t action_data_size = 0, i;
+
+       for (i = 0; i < l->info.n_actions; i++) {
+               uint32_t action_id = l->actions[i].action_id;
+               struct action *a = &ctl->actions[action_id];
+
+               if (a->data_size > action_data_size)
+                       action_data_size = a->data_size;
+       }
+
+       return action_data_size;
+}
+
+static void
+table_state_free(struct rte_swx_ctl_pipeline *ctl)
+{
+       uint32_t table_base_index, selector_base_index, learner_base_index, i;
+
        if (!ctl->ts_next)
                return;
 
        /* For each table, free its table state. */
+       table_base_index = 0;
        for (i = 0; i < ctl->info.n_tables; i++) {
                struct table *table = &ctl->tables[i];
-               struct rte_swx_table_state *ts = &ctl->ts_next[i];
+               struct rte_swx_table_state *ts = &ctl->ts_next[table_base_index + i];
 
                /* Default action data. */
                free(ts->default_action_data);
@@ -730,6 +1040,25 @@ table_state_free(struct rte_swx_ctl_pipeline *ctl)
                        table->ops.free(ts->obj);
        }
 
+       /* For each selector table, free its table state. */
+       selector_base_index = ctl->info.n_tables;
+       for (i = 0; i < ctl->info.n_selectors; i++) {
+               struct rte_swx_table_state *ts = &ctl->ts_next[selector_base_index + i];
+
+               /* Table object. */
+               if (ts->obj)
+                       rte_swx_table_selector_free(ts->obj);
+       }
+
+       /* For each learner table, free its table state. */
+       learner_base_index = ctl->info.n_tables + ctl->info.n_selectors;
+       for (i = 0; i < ctl->info.n_learners; i++) {
+               struct rte_swx_table_state *ts = &ctl->ts_next[learner_base_index + i];
+
+               /* Default action data. */
+               free(ts->default_action_data);
+       }
+
        free(ctl->ts_next);
        ctl->ts_next = NULL;
 }
@@ -737,20 +1066,22 @@ table_state_free(struct rte_swx_ctl_pipeline *ctl)
 static int
 table_state_create(struct rte_swx_ctl_pipeline *ctl)
 {
+       uint32_t table_base_index, selector_base_index, learner_base_index, i;
        int status = 0;
-       uint32_t i;
 
-       ctl->ts_next = calloc(ctl->info.n_tables,
+       ctl->ts_next = calloc(ctl->info.n_tables + ctl->info.n_selectors + ctl->info.n_learners,
                              sizeof(struct rte_swx_table_state));
        if (!ctl->ts_next) {
                status = -ENOMEM;
                goto error;
        }
 
+       /* Tables. */
+       table_base_index = 0;
        for (i = 0; i < ctl->info.n_tables; i++) {
                struct table *table = &ctl->tables[i];
-               struct rte_swx_table_state *ts = &ctl->ts[i];
-               struct rte_swx_table_state *ts_next = &ctl->ts_next[i];
+               struct rte_swx_table_state *ts = &ctl->ts[table_base_index + i];
+               struct rte_swx_table_state *ts_next = &ctl->ts_next[table_base_index + i];
 
                /* Table object. */
                if (!table->is_stub && table->ops.add) {
@@ -782,6 +1113,44 @@ table_state_create(struct rte_swx_ctl_pipeline *ctl)
                ts_next->default_action_id = ts->default_action_id;
        }
 
+       /* Selector tables. */
+       selector_base_index = ctl->info.n_tables;
+       for (i = 0; i < ctl->info.n_selectors; i++) {
+               struct selector *s = &ctl->selectors[i];
+               struct rte_swx_table_state *ts_next = &ctl->ts_next[selector_base_index + i];
+
+               /* Table object. */
+               ts_next->obj = rte_swx_table_selector_create(&s->params, NULL, ctl->numa_node);
+               if (!ts_next->obj) {
+                       status = -ENODEV;
+                       goto error;
+               }
+       }
+
+       /* Learner tables. */
+       learner_base_index = ctl->info.n_tables + ctl->info.n_selectors;
+       for (i = 0; i < ctl->info.n_learners; i++) {
+               struct learner *l = &ctl->learners[i];
+               struct rte_swx_table_state *ts = &ctl->ts[learner_base_index + i];
+               struct rte_swx_table_state *ts_next = &ctl->ts_next[learner_base_index + i];
+
+               /* Table object: duplicate from the current table state. */
+               ts_next->obj = ts->obj;
+
+               /* Default action data: duplicate from the current table state. */
+               ts_next->default_action_data = malloc(l->action_data_size);
+               if (!ts_next->default_action_data) {
+                       status = -ENOMEM;
+                       goto error;
+               }
+
+               memcpy(ts_next->default_action_data,
+                      ts->default_action_data,
+                      l->action_data_size);
+
+               ts_next->default_action_id = ts->default_action_id;
+       }
+
        return 0;
 
 error:
@@ -799,6 +1168,10 @@ rte_swx_ctl_pipeline_free(struct rte_swx_ctl_pipeline *ctl)
 
        table_state_free(ctl);
 
+       learner_free(ctl);
+
+       selector_free(ctl);
+
        table_free(ctl);
 
        free(ctl);
@@ -940,37 +1313,156 @@ rte_swx_ctl_pipeline_create(struct rte_swx_pipeline *p)
                        goto error;
        }
 
-       /* ts. */
-       status = rte_swx_pipeline_table_state_get(p, &ctl->ts);
-       if (status)
+       /* selector tables. */
+       ctl->selectors = calloc(ctl->info.n_selectors, sizeof(struct selector));
+       if (!ctl->selectors)
                goto error;
 
-       /* ts_next. */
-       status = table_state_create(ctl);
-       if (status)
-               goto error;
+       for (i = 0; i < ctl->info.n_selectors; i++) {
+               struct selector *s = &ctl->selectors[i];
+               uint32_t j;
 
-       return ctl;
+               /* info. */
+               status = rte_swx_ctl_selector_info_get(p, i, &s->info);
+               if (status)
+                       goto error;
 
-error:
-       rte_swx_ctl_pipeline_free(ctl);
-       return NULL;
-}
+               /* group_id field. */
+               status = rte_swx_ctl_selector_group_id_field_info_get(p,
+                       i,
+                       &s->group_id_field);
+               if (status)
+                       goto error;
 
-int
-rte_swx_ctl_pipeline_table_entry_add(struct rte_swx_ctl_pipeline *ctl,
-                                    const char *table_name,
-                                    struct rte_swx_table_entry *entry)
-{
-       struct table *table;
-       struct rte_swx_table_entry *new_entry, *existing_entry;
-       uint32_t table_id;
+               /* selector fields. */
+               s->selector_fields = calloc(s->info.n_selector_fields,
+                       sizeof(struct rte_swx_ctl_table_match_field_info));
+               if (!s->selector_fields)
+                       goto error;
 
-       CHECK(ctl, EINVAL);
-       CHECK(table_name && table_name[0], EINVAL);
+               for (j = 0; j < s->info.n_selector_fields; j++) {
+                       status = rte_swx_ctl_selector_field_info_get(p,
+                               i,
+                               j,
+                               &s->selector_fields[j]);
+                       if (status)
+                               goto error;
+               }
 
-       table = table_find(ctl, table_name);
-       CHECK(table, EINVAL);
+               /* member_id field. */
+               status = rte_swx_ctl_selector_member_id_field_info_get(p,
+                       i,
+                       &s->member_id_field);
+               if (status)
+                       goto error;
+
+               /* groups. */
+               s->groups = calloc(s->info.n_groups_max,
+                       sizeof(struct rte_swx_table_selector_group *));
+               if (!s->groups)
+                       goto error;
+
+               /* pending_groups. */
+               s->pending_groups = calloc(s->info.n_groups_max,
+                       sizeof(struct rte_swx_table_selector_group *));
+               if (!s->pending_groups)
+                       goto error;
+
+               /* groups_added. */
+               s->groups_added = calloc(s->info.n_groups_max, sizeof(int));
+               if (!s->groups_added)
+                       goto error;
+
+               /* groups_pending_delete. */
+               s->groups_pending_delete = calloc(s->info.n_groups_max, sizeof(int));
+               if (!s->groups_pending_delete)
+                       goto error;
+
+               /* params. */
+               status = selector_params_get(ctl, i);
+               if (status)
+                       goto error;
+       }
+
+       /* learner tables. */
+       ctl->learners = calloc(ctl->info.n_learners, sizeof(struct learner));
+       if (!ctl->learners)
+               goto error;
+
+       for (i = 0; i < ctl->info.n_learners; i++) {
+               struct learner *l = &ctl->learners[i];
+               uint32_t j;
+
+               /* info. */
+               status = rte_swx_ctl_learner_info_get(p, i, &l->info);
+               if (status)
+                       goto error;
+
+               /* mf. */
+               l->mf = calloc(l->info.n_match_fields,
+                              sizeof(struct rte_swx_ctl_table_match_field_info));
+               if (!l->mf)
+                       goto error;
+
+               for (j = 0; j < l->info.n_match_fields; j++) {
+                       status = rte_swx_ctl_learner_match_field_info_get(p,
+                               i,
+                               j,
+                               &l->mf[j]);
+                       if (status)
+                               goto error;
+               }
+
+               /* actions. */
+               l->actions = calloc(l->info.n_actions,
+                       sizeof(struct rte_swx_ctl_table_action_info));
+               if (!l->actions)
+                       goto error;
+
+               for (j = 0; j < l->info.n_actions; j++) {
+                       status = rte_swx_ctl_learner_action_info_get(p,
+                               i,
+                               j,
+                               &l->actions[j]);
+                       if (status || l->actions[j].action_id >= ctl->info.n_actions)
+                               goto error;
+               }
+
+               /* action_data_size. */
+               l->action_data_size = learner_action_data_size_get(ctl, l);
+       }
+
+       /* ts. */
+       status = rte_swx_pipeline_table_state_get(p, &ctl->ts);
+       if (status)
+               goto error;
+
+       /* ts_next. */
+       status = table_state_create(ctl);
+       if (status)
+               goto error;
+
+       return ctl;
+
+error:
+       rte_swx_ctl_pipeline_free(ctl);
+       return NULL;
+}
+
+int
+rte_swx_ctl_pipeline_table_entry_add(struct rte_swx_ctl_pipeline *ctl,
+                                    const char *table_name,
+                                    struct rte_swx_table_entry *entry)
+{
+       struct table *table;
+       struct rte_swx_table_entry *new_entry, *existing_entry;
+       uint32_t table_id;
+
+       CHECK(ctl, EINVAL);
+       CHECK(table_name && table_name[0], EINVAL);
+
+       table = table_find(ctl, table_name);
+       CHECK(table, EINVAL);
        table_id = table - ctl->tables;
 
        CHECK(entry, EINVAL);
@@ -1374,9 +1866,8 @@ table_rollfwd1(struct rte_swx_ctl_pipeline *ctl, uint32_t table_id)
        action_data = table->pending_default->action_data;
        a = &ctl->actions[action_id];
 
-       memcpy(ts_next->default_action_data,
-              action_data,
-              a->data_size);
+       if (a->data_size)
+               memcpy(ts_next->default_action_data, action_data, a->data_size);
 
        ts_next->default_action_id = action_id;
 }
@@ -1499,6 +1990,469 @@ table_abort(struct rte_swx_ctl_pipeline *ctl, uint32_t table_id)
        table_pending_default_free(table);
 }
 
+int
+rte_swx_ctl_pipeline_selector_group_add(struct rte_swx_ctl_pipeline *ctl,
+                                       const char *selector_name,
+                                       uint32_t *group_id)
+{
+       struct selector *s;
+       uint32_t i;
+
+       /* Check input arguments. */
+       if (!ctl || !selector_name || !selector_name[0] || !group_id)
+               return -EINVAL;
+
+       s = selector_find(ctl, selector_name);
+       if (!s)
+               return -EINVAL;
+
+       /* Find an unused group. */
+       for (i = 0; i < s->info.n_groups_max; i++)
+               if (!s->groups_added[i]) {
+                       *group_id = i;
+                       s->groups_added[i] = 1;
+                       return 0;
+               }
+
+       return -ENOSPC;
+}
+
+int
+rte_swx_ctl_pipeline_selector_group_delete(struct rte_swx_ctl_pipeline *ctl,
+                                          const char *selector_name,
+                                          uint32_t group_id)
+{
+       struct selector *s;
+       struct rte_swx_table_selector_group *group;
+
+       /* Check input arguments. */
+       if (!ctl || !selector_name || !selector_name[0])
+               return -EINVAL;
+
+       s = selector_find(ctl, selector_name);
+       if (!s ||
+          (group_id >= s->info.n_groups_max) ||
+          !s->groups_added[group_id])
+               return -EINVAL;
+
+       /* Check if this group is already scheduled for deletion. */
+       if (s->groups_pending_delete[group_id])
+               return 0;
+
+       /* Initialize the pending group, if needed. */
+       if (!s->pending_groups[group_id]) {
+               int status;
+
+               status = selector_group_duplicate_to_pending(s, group_id);
+               if (status)
+                       return status;
+       }
+
+       group = s->pending_groups[group_id];
+
+       /* Schedule removal of all the members from the current group. */
+       for ( ; ; ) {
+               struct rte_swx_table_selector_member *m;
+
+               m = TAILQ_FIRST(&group->members);
+               if (!m)
+                       break;
+
+               TAILQ_REMOVE(&group->members, m, node);
+               free(m);
+       }
+
+       /* Schedule the group for deletion. */
+       s->groups_pending_delete[group_id] = 1;
+
+       return 0;
+}
+
+int
+rte_swx_ctl_pipeline_selector_group_member_add(struct rte_swx_ctl_pipeline *ctl,
+                                              const char *selector_name,
+                                              uint32_t group_id,
+                                              uint32_t member_id,
+                                              uint32_t member_weight)
+{
+       struct selector *s;
+       struct rte_swx_table_selector_group *group;
+       struct rte_swx_table_selector_member *m;
+
+       if (!member_weight)
+               return rte_swx_ctl_pipeline_selector_group_member_delete(ctl,
+                                                                        selector_name,
+                                                                        group_id,
+                                                                        member_id);
+
+       /* Check input arguments. */
+       if (!ctl || !selector_name || !selector_name[0])
+               return -EINVAL;
+
+       s = selector_find(ctl, selector_name);
+       if (!s ||
+          (group_id >= s->info.n_groups_max) ||
+          !s->groups_added[group_id] ||
+          s->groups_pending_delete[group_id])
+               return -EINVAL;
+
+       /* Initialize the pending group, if needed. */
+       if (!s->pending_groups[group_id]) {
+               int status;
+
+               status = selector_group_duplicate_to_pending(s, group_id);
+               if (status)
+                       return status;
+       }
+
+       group = s->pending_groups[group_id];
+
+       /* If this member is already in this group, then simply update its weight and return. */
+       TAILQ_FOREACH(m, &group->members, node)
+               if (m->member_id == member_id) {
+                       m->member_weight = member_weight;
+                       return 0;
+               }
+
+       /* Add new member to this group. */
+       m = calloc(1, sizeof(struct rte_swx_table_selector_member));
+       if (!m)
+               return -ENOMEM;
+
+       m->member_id = member_id;
+       m->member_weight = member_weight;
+
+       TAILQ_INSERT_TAIL(&group->members, m, node);
+
+       return 0;
+}
+
+int
+rte_swx_ctl_pipeline_selector_group_member_delete(struct rte_swx_ctl_pipeline *ctl,
+                                                 const char *selector_name,
+                                                 uint32_t group_id __rte_unused,
+                                                 uint32_t member_id __rte_unused)
+{
+       struct selector *s;
+       struct rte_swx_table_selector_group *group;
+       struct rte_swx_table_selector_member *m;
+
+       /* Check input arguments. */
+       if (!ctl || !selector_name || !selector_name[0])
+               return -EINVAL;
+
+       s = selector_find(ctl, selector_name);
+       if (!s ||
+           (group_id >= s->info.n_groups_max) ||
+           !s->groups_added[group_id] ||
+           s->groups_pending_delete[group_id])
+               return -EINVAL;
+
+       /* Initialize the pending group, if needed. */
+       if (!s->pending_groups[group_id]) {
+               int status;
+
+               status = selector_group_duplicate_to_pending(s, group_id);
+               if (status)
+                       return status;
+       }
+
+       group = s->pending_groups[group_id];
+
+       /* Look for this member in the group and remove it, if found. */
+       TAILQ_FOREACH(m, &group->members, node)
+               if (m->member_id == member_id) {
+                       TAILQ_REMOVE(&group->members, m, node);
+                       free(m);
+                       return 0;
+               }
+
+       return 0;
+}
+
+static int
+selector_rollfwd(struct rte_swx_ctl_pipeline *ctl, uint32_t selector_id)
+{
+       struct selector *s = &ctl->selectors[selector_id];
+       struct rte_swx_table_state *ts_next = &ctl->ts_next[ctl->info.n_tables + selector_id];
+       uint32_t group_id;
+
+       /* Push pending group member changes (s->pending_groups[group_id]) to the selector table
+        * mirror copy (ts_next->obj).
+        */
+       for (group_id = 0; group_id < s->info.n_groups_max; group_id++) {
+               struct rte_swx_table_selector_group *group = s->pending_groups[group_id];
+               int status;
+
+               /* Skip this group if no change needed. */
+               if (!group)
+                       continue;
+
+               /* Apply the pending changes for the current group. */
+               status = rte_swx_table_selector_group_set(ts_next->obj, group_id, group);
+               if (status)
+                       return status;
+       }
+
+       return 0;
+}
+
+static void
+selector_rollfwd_finalize(struct rte_swx_ctl_pipeline *ctl, uint32_t selector_id)
+{
+       struct selector *s = &ctl->selectors[selector_id];
+       uint32_t group_id;
+
+       /* Commit pending group member changes (s->pending_groups[group_id]) to the stable group
+        * records (s->groups[group_id).
+        */
+       for (group_id = 0; group_id < s->info.n_groups_max; group_id++) {
+               struct rte_swx_table_selector_group *g = s->groups[group_id];
+               struct rte_swx_table_selector_group *gp = s->pending_groups[group_id];
+
+               /* Skip this group if no change needed. */
+               if (!gp)
+                       continue;
+
+               /* Transition the pending changes to stable. */
+               s->groups[group_id] = gp;
+               s->pending_groups[group_id] = NULL;
+
+               /* Free the old group member list. */
+               if (!g)
+                       continue;
+
+               for ( ; ; ) {
+                       struct rte_swx_table_selector_member *m;
+
+                       m = TAILQ_FIRST(&g->members);
+                       if (!m)
+                               break;
+
+                       TAILQ_REMOVE(&g->members, m, node);
+                       free(m);
+               }
+
+               free(g);
+       }
+
+       /* Commit pending group validity changes (from s->groups_pending_delete[group_id] to
+        * s->groups_added[group_id].
+        */
+       for (group_id = 0; group_id < s->info.n_groups_max; group_id++)
+               if (s->groups_pending_delete[group_id]) {
+                       s->groups_added[group_id] = 0;
+                       s->groups_pending_delete[group_id] = 0;
+               }
+}
+
+static void
+selector_rollback(struct rte_swx_ctl_pipeline *ctl, uint32_t selector_id)
+{
+       struct selector *s = &ctl->selectors[selector_id];
+       struct rte_swx_table_state *ts = &ctl->ts[ctl->info.n_tables + selector_id];
+       struct rte_swx_table_state *ts_next = &ctl->ts_next[ctl->info.n_tables + selector_id];
+       uint32_t group_id;
+
+       /* Discard any previous changes to the selector table mirror copy (ts_next->obj). */
+       for (group_id = 0; group_id < s->info.n_groups_max; group_id++) {
+               struct rte_swx_table_selector_group *gp = s->pending_groups[group_id];
+
+               if (gp) {
+                       ts_next->obj = ts->obj;
+                       break;
+               }
+       }
+}
+
+static void
+selector_abort(struct rte_swx_ctl_pipeline *ctl, uint32_t selector_id)
+{
+       struct selector *s = &ctl->selectors[selector_id];
+       uint32_t group_id;
+
+       /* Discard any pending group member changes (s->pending_groups[group_id]). */
+       for (group_id = 0; group_id < s->info.n_groups_max; group_id++)
+               selector_pending_group_members_free(s, group_id);
+
+       /* Discard any pending group deletions. */
+       memset(s->groups_pending_delete, 0, s->info.n_groups_max * sizeof(int));
+}
+
+static struct rte_swx_table_entry *
+learner_default_entry_alloc(struct learner *l)
+{
+       struct rte_swx_table_entry *entry;
+
+       entry = calloc(1, sizeof(struct rte_swx_table_entry));
+       if (!entry)
+               goto error;
+
+       /* action_data. */
+       if (l->action_data_size) {
+               entry->action_data = calloc(1, l->action_data_size);
+               if (!entry->action_data)
+                       goto error;
+       }
+
+       return entry;
+
+error:
+       table_entry_free(entry);
+       return NULL;
+}
+
+static int
+learner_default_entry_check(struct rte_swx_ctl_pipeline *ctl,
+                           uint32_t learner_id,
+                           struct rte_swx_table_entry *entry)
+{
+       struct learner *l = &ctl->learners[learner_id];
+       struct action *a;
+       uint32_t i;
+
+       CHECK(entry, EINVAL);
+
+       /* action_id. */
+       for (i = 0; i < l->info.n_actions; i++)
+               if (entry->action_id == l->actions[i].action_id)
+                       break;
+
+       CHECK(i < l->info.n_actions, EINVAL);
+
+       /* action_data. */
+       a = &ctl->actions[entry->action_id];
+       CHECK(!(a->data_size && !entry->action_data), EINVAL);
+
+       return 0;
+}
+
+static struct rte_swx_table_entry *
+learner_default_entry_duplicate(struct rte_swx_ctl_pipeline *ctl,
+                               uint32_t learner_id,
+                               struct rte_swx_table_entry *entry)
+{
+       struct learner *l = &ctl->learners[learner_id];
+       struct rte_swx_table_entry *new_entry = NULL;
+       struct action *a;
+       uint32_t i;
+
+       if (!entry)
+               goto error;
+
+       new_entry = calloc(1, sizeof(struct rte_swx_table_entry));
+       if (!new_entry)
+               goto error;
+
+       /* action_id. */
+       for (i = 0; i < l->info.n_actions; i++)
+               if (entry->action_id == l->actions[i].action_id)
+                       break;
+
+       if (i >= l->info.n_actions)
+               goto error;
+
+       new_entry->action_id = entry->action_id;
+
+       /* action_data. */
+       a = &ctl->actions[entry->action_id];
+       if (a->data_size && !entry->action_data)
+               goto error;
+
+       /* The table layer provisions a constant action data size per
+        * entry, which should be the largest data size for all the
+        * actions enabled for the current table, and attempts to copy
+        * this many bytes each time a table entry is added, even if the
+        * specific action requires less data or even no data at all,
+        * hence we always have to allocate the max.
+        */
+       new_entry->action_data = calloc(1, l->action_data_size);
+       if (!new_entry->action_data)
+               goto error;
+
+       if (a->data_size)
+               memcpy(new_entry->action_data, entry->action_data, a->data_size);
+
+       return new_entry;
+
+error:
+       table_entry_free(new_entry);
+       return NULL;
+}
+
+int
+rte_swx_ctl_pipeline_learner_default_entry_add(struct rte_swx_ctl_pipeline *ctl,
+                                              const char *learner_name,
+                                              struct rte_swx_table_entry *entry)
+{
+       struct learner *l;
+       struct rte_swx_table_entry *new_entry;
+       uint32_t learner_id;
+
+       CHECK(ctl, EINVAL);
+
+       CHECK(learner_name && learner_name[0], EINVAL);
+       l = learner_find(ctl, learner_name);
+       CHECK(l, EINVAL);
+       learner_id = l - ctl->learners;
+       CHECK(!l->info.default_action_is_const, EINVAL);
+
+       CHECK(entry, EINVAL);
+       CHECK(!learner_default_entry_check(ctl, learner_id, entry), EINVAL);
+
+       CHECK(l->actions[entry->action_id].action_is_for_default_entry, EINVAL);
+
+       new_entry = learner_default_entry_duplicate(ctl, learner_id, entry);
+       CHECK(new_entry, ENOMEM);
+
+       learner_pending_default_free(l);
+
+       l->pending_default = new_entry;
+       return 0;
+}
+
+static void
+learner_rollfwd(struct rte_swx_ctl_pipeline *ctl, uint32_t learner_id)
+{
+       struct learner *l = &ctl->learners[learner_id];
+       struct rte_swx_table_state *ts_next = &ctl->ts_next[ctl->info.n_tables +
+               ctl->info.n_selectors + learner_id];
+       struct action *a;
+       uint8_t *action_data;
+       uint64_t action_id;
+
+       /* Copy the pending default entry. */
+       if (!l->pending_default)
+               return;
+
+       action_id = l->pending_default->action_id;
+       action_data = l->pending_default->action_data;
+       a = &ctl->actions[action_id];
+
+       if (a->data_size)
+               memcpy(ts_next->default_action_data, action_data, a->data_size);
+
+       ts_next->default_action_id = action_id;
+}
+
+static void
+learner_rollfwd_finalize(struct rte_swx_ctl_pipeline *ctl, uint32_t learner_id)
+{
+       struct learner *l = &ctl->learners[learner_id];
+
+       /* Free up the pending default entry, as it is now part of the table. */
+       learner_pending_default_free(l);
+}
+
+static void
+learner_abort(struct rte_swx_ctl_pipeline *ctl, uint32_t learner_id)
+{
+       struct learner *l = &ctl->learners[learner_id];
+
+       /* Free up the pending default entry, as it is no longer going to be added to the table. */
+       learner_pending_default_free(l);
+}
+
 int
 rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
 {
@@ -1508,8 +2462,9 @@ rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
 
        CHECK(ctl, EINVAL);
 
-       /* Operate the changes on the current ts_next before it becomes the new
-        * ts.
+       /* Operate the changes on the current ts_next before it becomes the new ts. First, operate
+        * all the changes that can fail; if no failure, then operate the changes that cannot fail.
+        * We must be able to fully revert all the changes that can fail as if they never happened.
         */
        for (i = 0; i < ctl->info.n_tables; i++) {
                status = table_rollfwd0(ctl, i, 0);
@@ -1517,9 +2472,21 @@ rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
                        goto rollback;
        }
 
+       for (i = 0; i < ctl->info.n_selectors; i++) {
+               status = selector_rollfwd(ctl, i);
+               if (status)
+                       goto rollback;
+       }
+
+       /* Second, operate all the changes that cannot fail. Since nothing can fail from this point
+        * onwards, the transaction is guaranteed to be successful.
+        */
        for (i = 0; i < ctl->info.n_tables; i++)
                table_rollfwd1(ctl, i);
 
+       for (i = 0; i < ctl->info.n_learners; i++)
+               learner_rollfwd(ctl, i);
+
        /* Swap the table state for the data plane. The current ts and ts_next
         * become the new ts_next and ts, respectively.
         */
@@ -1529,7 +2496,10 @@ rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
        ctl->ts = ctl->ts_next;
        ctl->ts_next = ts;
 
-       /* Operate the changes on the current ts_next, which is the previous ts.
+       /* Operate the changes on the current ts_next, which is the previous ts, in order to get
+        * the current ts_next in sync with the current ts. Since the changes that can fail did
+        * not fail on the previous ts_next, it is guaranteed that they will not fail on the
+        * current ts_next, hence no error checking is needed.
         */
        for (i = 0; i < ctl->info.n_tables; i++) {
                table_rollfwd0(ctl, i, 1);
@@ -1537,6 +2507,16 @@ rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
                table_rollfwd2(ctl, i);
        }
 
+       for (i = 0; i < ctl->info.n_selectors; i++) {
+               selector_rollfwd(ctl, i);
+               selector_rollfwd_finalize(ctl, i);
+       }
+
+       for (i = 0; i < ctl->info.n_learners; i++) {
+               learner_rollfwd(ctl, i);
+               learner_rollfwd_finalize(ctl, i);
+       }
+
        return 0;
 
 rollback:
@@ -1546,6 +2526,16 @@ rollback:
                        table_abort(ctl, i);
        }
 
+       for (i = 0; i < ctl->info.n_selectors; i++) {
+               selector_rollback(ctl, i);
+               if (abort_on_fail)
+                       selector_abort(ctl, i);
+       }
+
+       if (abort_on_fail)
+               for (i = 0; i < ctl->info.n_learners; i++)
+                       learner_abort(ctl, i);
+
        return status;
 }
 
@@ -1559,6 +2549,51 @@ rte_swx_ctl_pipeline_abort(struct rte_swx_ctl_pipeline *ctl)
 
        for (i = 0; i < ctl->info.n_tables; i++)
                table_abort(ctl, i);
+
+       for (i = 0; i < ctl->info.n_selectors; i++)
+               selector_abort(ctl, i);
+
+       for (i = 0; i < ctl->info.n_learners; i++)
+               learner_abort(ctl, i);
+}
+
+static int
+mask_to_prefix(uint64_t mask, uint32_t mask_length, uint32_t *prefix_length)
+{
+       uint32_t n_trailing_zeros = 0, n_ones = 0, i;
+
+       if (!mask) {
+               *prefix_length = 0;
+               return 0;
+       }
+
+       /* Count trailing zero bits. */
+       for (i = 0; i < 64; i++) {
+               if (mask & (1LLU << i))
+                       break;
+
+               n_trailing_zeros++;
+       }
+
+       /* Count the one bits that follow. */
+       for ( ; i < 64; i++) {
+               if (!(mask & (1LLU << i)))
+                       break;
+
+               n_ones++;
+       }
+
+       /* Check that no more one bits are present */
+       for ( ; i < 64; i++)
+               if (mask & (1LLU << i))
+                       return -EINVAL;
+
+       /* Check that the input mask is a prefix or the right length. */
+       if (n_ones + n_trailing_zeros != mask_length)
+               return -EINVAL;
+
+       *prefix_length = n_ones;
+       return 0;
 }
 
 static int
@@ -1585,8 +2620,8 @@ rte_swx_ctl_pipeline_table_entry_read(struct rte_swx_ctl_pipeline *ctl,
        struct action *action;
        struct rte_swx_table_entry *entry = NULL;
        char *s0 = NULL, *s;
-       uint32_t n_tokens = 0, arg_offset = 0, i;
-       int blank_or_comment = 0;
+       uint32_t n_tokens = 0, arg_offset = 0, lpm_prefix_length_max = 0, lpm_prefix_length = 0, i;
+       int lpm = 0, blank_or_comment = 0;
 
        /* Check input arguments. */
        if (!ctl)
@@ -1636,7 +2671,7 @@ rte_swx_ctl_pipeline_table_entry_read(struct rte_swx_ctl_pipeline *ctl,
        /*
         * Match.
         */
-       if (n_tokens && strcmp(tokens[0], "match"))
+       if (!(n_tokens && !strcmp(tokens[0], "match")))
                goto action;
 
        if (n_tokens < 1 + table->info.n_match_fields)
@@ -1661,6 +2696,19 @@ rte_swx_ctl_pipeline_table_entry_read(struct rte_swx_ctl_pipeline *ctl,
                        if (mf_mask[0])
                                goto error;
 
+                       /* LPM. */
+                       if (mf->match_type == RTE_SWX_TABLE_MATCH_LPM) {
+                               int status;
+
+                               lpm = 1;
+
+                               lpm_prefix_length_max = mf->n_bits;
+
+                               status = mask_to_prefix(mask, mf->n_bits, &lpm_prefix_length);
+                               if (status)
+                                       goto error;
+                       }
+
                        /* Endianness conversion. */
                        if (mf->is_header)
                                mask = field_hton(mask, mf->n_bits);
@@ -1715,11 +2763,139 @@ rte_swx_ctl_pipeline_table_entry_read(struct rte_swx_ctl_pipeline *ctl,
                n_tokens -= 2;
        }
 
+       /* LPM. */
+       if (lpm)
+               entry->key_priority = lpm_prefix_length_max - lpm_prefix_length;
+
        /*
         * Action.
         */
 action:
-       if (n_tokens && strcmp(tokens[0], "action"))
+       if (!(n_tokens && !strcmp(tokens[0], "action")))
+               goto other;
+
+       if (n_tokens < 2)
+               goto error;
+
+       action = action_find(ctl, tokens[1]);
+       if (!action)
+               goto error;
+
+       if (n_tokens < 2 + action->info.n_args * 2)
+               goto error;
+
+       /* action_id. */
+       entry->action_id = action - ctl->actions;
+
+       /* action_data. */
+       for (i = 0; i < action->info.n_args; i++) {
+               struct rte_swx_ctl_action_arg_info *arg = &action->args[i];
+               char *arg_name, *arg_val;
+               uint64_t val;
+
+               arg_name = tokens[2 + i * 2];
+               arg_val = tokens[2 + i * 2 + 1];
+
+               if (strcmp(arg_name, arg->name))
+                       goto error;
+
+               val = strtoull(arg_val, &arg_val, 0);
+               if (arg_val[0])
+                       goto error;
+
+               /* Endianness conversion. */
+               if (arg->is_network_byte_order)
+                       val = field_hton(val, arg->n_bits);
+
+               /* Copy to entry. */
+               memcpy(&entry->action_data[arg_offset],
+                      (uint8_t *)&val,
+                      arg->n_bits / 8);
+
+               arg_offset += arg->n_bits / 8;
+       }
+
+       tokens += 2 + action->info.n_args * 2;
+       n_tokens -= 2 + action->info.n_args * 2;
+
+other:
+       if (n_tokens)
+               goto error;
+
+       free(s0);
+       return entry;
+
+error:
+       table_entry_free(entry);
+       free(s0);
+       if (is_blank_or_comment)
+               *is_blank_or_comment = blank_or_comment;
+       return NULL;
+}
+
+struct rte_swx_table_entry *
+rte_swx_ctl_pipeline_learner_default_entry_read(struct rte_swx_ctl_pipeline *ctl,
+                                               const char *learner_name,
+                                               const char *string,
+                                               int *is_blank_or_comment)
+{
+       char *token_array[RTE_SWX_CTL_ENTRY_TOKENS_MAX], **tokens;
+       struct learner *l;
+       struct action *action;
+       struct rte_swx_table_entry *entry = NULL;
+       char *s0 = NULL, *s;
+       uint32_t n_tokens = 0, arg_offset = 0, i;
+       int blank_or_comment = 0;
+
+       /* Check input arguments. */
+       if (!ctl)
+               goto error;
+
+       if (!learner_name || !learner_name[0])
+               goto error;
+
+       l = learner_find(ctl, learner_name);
+       if (!l)
+               goto error;
+
+       if (!string || !string[0])
+               goto error;
+
+       /* Memory allocation. */
+       s0 = strdup(string);
+       if (!s0)
+               goto error;
+
+       entry = learner_default_entry_alloc(l);
+       if (!entry)
+               goto error;
+
+       /* Parse the string into tokens. */
+       for (s = s0; ; ) {
+               char *token;
+
+               token = strtok_r(s, " \f\n\r\t\v", &s);
+               if (!token || token_is_comment(token))
+                       break;
+
+               if (n_tokens >= RTE_SWX_CTL_ENTRY_TOKENS_MAX)
+                       goto error;
+
+               token_array[n_tokens] = token;
+               n_tokens++;
+       }
+
+       if (!n_tokens) {
+               blank_or_comment = 1;
+               goto error;
+       }
+
+       tokens = token_array;
+
+       /*
+        * Action.
+        */
+       if (!(n_tokens && !strcmp(tokens[0], "action")))
                goto other;
 
        if (n_tokens < 2)
@@ -1858,3 +3034,49 @@ rte_swx_ctl_pipeline_table_fprintf(FILE *f,
                n_entries);
        return 0;
 }
+
+int
+rte_swx_ctl_pipeline_selector_fprintf(FILE *f,
+                                     struct rte_swx_ctl_pipeline *ctl,
+                                     const char *selector_name)
+{
+       struct selector *s;
+       uint32_t group_id;
+
+       if (!f || !ctl || !selector_name || !selector_name[0])
+               return -EINVAL;
+
+       s = selector_find(ctl, selector_name);
+       if (!s)
+               return -EINVAL;
+
+       /* Selector. */
+       fprintf(f, "# Selector %s: max groups %u, max members per group %u\n",
+               s->info.name,
+               s->info.n_groups_max,
+               s->info.n_members_per_group_max);
+
+       /* Groups. */
+       for (group_id = 0; group_id < s->info.n_groups_max; group_id++) {
+               struct rte_swx_table_selector_group *group = s->groups[group_id];
+               struct rte_swx_table_selector_member *m;
+               uint32_t n_members = 0;
+
+               fprintf(f, "Group %u = [", group_id);
+
+               /* Non-empty group. */
+               if (group)
+                       TAILQ_FOREACH(m, &group->members, node) {
+                               fprintf(f, "%u:%u ", m->member_id, m->member_weight);
+                               n_members++;
+                       }
+
+               /* Empty group. */
+               if (!n_members)
+                       fprintf(f, "0:1 ");
+
+               fprintf(f, "]\n");
+       }
+
+       return 0;
+}