| /*! \file osmo-config-merge.c |
| * Utility program for merging config files with patches */ |
| /* |
| * (C) 2018 by Harald Welte <laforge@gnumonks.org> |
| * |
| * All Rights Reserved |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 2 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License along |
| * with this program; if not, write to the Free Software Foundation, Inc., |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| */ |
| |
| /* |
| This utility allows you to merge an incremental config "patch" |
| into an osmocom-style config file. |
| |
| The patch file follows the same syntax as the original config file. |
| |
| It works by appending the leaf nodes of the patch file to the respective |
| nodes of the input config file. |
| |
| This process allows configuration file changes/updates to be performed |
| in a more stable/reliable way than by means of [unified] diff files, |
| as they break every time the context lines break. |
| |
| osmo-config-merge doesn't suffer from this problem, as it understands |
| the tree-like nature of VTY config files. |
| |
| NITE: This only works with configuration files that have proper |
| indenting, i.e. every level in the hierarchy must be indented excatly |
| one character, not multiple. |
| */ |
| |
| #include <stdio.h> |
| #include <string.h> |
| #include <errno.h> |
| |
| #include <osmocom/core/linuxlist.h> |
| #include <osmocom/core/talloc.h> |
| #include <osmocom/core/utils.h> |
| #include <osmocom/core/msgfile.h> |
| |
| struct node { |
| struct node *parent; /* back-pointer */ |
| struct llist_head list; /* part of parent->children */ |
| struct llist_head children; /* our own children */ |
| char *line; |
| }; |
| |
| struct osmo_patch_entry { |
| struct llist_head list; |
| struct node *tree; |
| }; |
| |
| /* allocate a new node */ |
| static struct node *node_alloc(void *ctx) |
| { |
| struct node *node = talloc_zero(ctx, struct node); |
| OSMO_ASSERT(node); |
| INIT_LLIST_HEAD(&node->children); |
| return node; |
| } |
| |
| /* allocate a new node as child of given parent */ |
| static struct node *node_alloc_child(struct node *parent) |
| { |
| struct node *node = node_alloc(parent); |
| node->parent = parent; |
| llist_add_tail(&node->list, &parent->children); |
| return node; |
| } |
| |
| /* find a given child specified by name/line string within given parent */ |
| static struct node *node_find_child(struct node *parent, const char *line) |
| { |
| struct node *n; |
| |
| llist_for_each_entry(n, &parent->children, list) { |
| if (!strcmp(line, n->line)) |
| return n; |
| } |
| return NULL; |
| } |
| |
| /* count the number of spaces / indent level */ |
| static int count_indent(const char *line) |
| { |
| int i; |
| |
| for (i = 0; i < strlen(line); i++) { |
| if (line[i] != ' ') |
| return i; |
| } |
| return i; |
| } |
| |
| /* strip any triling CR / LF */ |
| static void chomp(char *line) |
| { |
| while (1) { |
| int len = strlen(line); |
| if (len == 0) |
| return; |
| char *lastch = &line[len-1]; |
| switch (*lastch) { |
| case '\n': |
| case '\r': |
| *lastch = '\0'; |
| default: |
| return; |
| } |
| } |
| } |
| |
| /* read a config file and parse it into a tree of nodes */ |
| static struct node *file_read(void *ctx, const char *fname) |
| { |
| struct node *root, *last; |
| FILE *infile; |
| char line[1024]; |
| int cur_indent = -1; |
| unsigned int line_num = 0; |
| |
| infile = fopen(fname, "r"); |
| if (!infile) { |
| fprintf(stderr, "Could not open file '%s': %s\n", |
| fname, strerror(errno)); |
| return NULL; |
| } |
| |
| root = node_alloc(ctx); |
| last = root; |
| while (fgets(line, sizeof(line), infile)) { |
| line_num++; |
| chomp(line); |
| int indent = count_indent(line); |
| struct node *n; |
| if (indent > cur_indent) { |
| if (indent > cur_indent+1) { |
| fprintf(stderr, "File '%s' isn't well-formed in line %u, aborting!\n", |
| fname, line_num); |
| fclose(infile); |
| return NULL; |
| } |
| /* new child to last node */ |
| n = node_alloc_child(last); |
| } else if (indent < cur_indent) { |
| int i; |
| for (i = 0; i < cur_indent - indent; i++) { |
| /* go to parent, add another sibling */ |
| if (last->parent) |
| last = last->parent; |
| } |
| n = node_alloc_child(last->parent); |
| } else { |
| /* add a new sibling (child of parent) */ |
| n = node_alloc_child(last->parent); |
| } |
| n->line = talloc_strdup(n, line); |
| |
| last = n; |
| cur_indent = indent; |
| } |
| |
| fclose(infile); |
| return root; |
| } |
| |
| static void append_patch(struct node *cfg, struct node *patch) |
| { |
| struct node *n; |
| |
| llist_for_each_entry(n, &patch->children, list) { |
| if (llist_empty(&n->children)) { |
| struct node *t; |
| /* we are an end-node, i.e. something that needs to be |
| * patched into the original tree. We do this by simply |
| * appending it to the list of siblings */ |
| t = node_alloc_child(cfg); |
| t->line = talloc_strdup(t, n->line); |
| } else { |
| struct node *c; |
| /* we need to iterate / recurse further */ |
| |
| /* try to find the matching original node */ |
| c = node_find_child(cfg, n->line); |
| if (!c) { |
| /* create it, if it's missing */ |
| c = node_alloc_child(cfg); |
| c->line = talloc_strdup(c, n->line); |
| } |
| append_patch(c, n); |
| } |
| } |
| } |
| |
| |
| static int level = -1; |
| |
| static void dump_node(struct node *root, FILE *out, bool print_node_depth) |
| { |
| struct node *n; |
| level++; |
| |
| if (root->line) { |
| if (print_node_depth) { |
| int i; |
| for (i = 0; i < level; i++) |
| fputc('*', out); |
| } |
| |
| fprintf(out, "%s\n", root->line); |
| } |
| |
| llist_for_each_entry(n, &root->children, list) { |
| dump_node(n, out, print_node_depth); |
| } |
| level--; |
| } |
| |
| static void exit_usage(int rc) |
| { |
| fprintf(stderr, "Usage: osmo-config-merge <config-file> <config-patch>...<config-patch> [--debug]\n"); |
| exit(rc); |
| } |
| |
| |
| int main(int argc, char **argv) |
| { |
| struct node *base_tree; |
| struct osmo_config_list *trees; |
| struct osmo_patch_entry *entry; |
| bool debug_enabled = false; |
| unsigned i; |
| void *ctx; |
| |
| if (argc < 3) |
| exit_usage(1); |
| |
| ctx = talloc_named_const(NULL, 0, "root"); |
| |
| base_tree = file_read(ctx, argv[1]); |
| trees = talloc_zero(ctx, struct osmo_config_list); |
| if (!base_tree || !trees) { |
| talloc_free(ctx); |
| return 2; |
| } |
| |
| INIT_LLIST_HEAD(&trees->entry); |
| for (i = 2; i < argc; i++) { |
| if (!strcmp(argv[i], "--debug")) |
| debug_enabled = true; |
| else { |
| entry = talloc_zero(trees, struct osmo_patch_entry); |
| if (!entry) { |
| talloc_free(ctx); |
| return 3; |
| } |
| |
| entry->tree = file_read(ctx, argv[i]); |
| if (!entry->tree) { |
| talloc_free(ctx); |
| return 4; |
| } |
| llist_add_tail(&entry->list, &trees->entry); |
| } |
| } |
| |
| if (llist_empty(&trees->entry)) |
| exit_usage(1); |
| |
| if (debug_enabled) { |
| fprintf(stderr, "====== dumping tree (base)\n"); |
| dump_node(base_tree, stderr, true); |
| } |
| |
| llist_for_each_entry(entry, &trees->entry, list) { |
| append_patch(base_tree, entry->tree); |
| if (debug_enabled) { |
| fprintf(stderr, "====== dumping tree (patch)\n"); |
| dump_node(entry->tree, stderr, true); |
| } |
| } |
| |
| if (debug_enabled) |
| fprintf(stderr, "====== dumping tree (patched)\n"); |
| dump_node(base_tree, stdout, false); |
| fflush(stdout); |
| |
| /* make AddressSanitizer / LeakSanitizer happy by recursively freeing the trees */ |
| talloc_free(trees); |
| talloc_free(base_tree); |
| talloc_free(ctx); |
| |
| return 0; |
| } |