From 503da4e49107f6c20e3fce05506f8b107894a5af Mon Sep 17 00:00:00 2001 From: Franck Bui Date: Fri, 22 Jan 2021 14:57:08 +0100 Subject: [PATCH 1/1] conf-parser: introduce 'early' drop-ins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As formerly known as "downstream conf file drop-ins should never override main user conf file". Previously all drop-ins, including those shipped by downstream, shipped in /usr, could override user's main configuration file (located in /etc) because the main file was always parsed first. This was problematic for downstreams because their customization should never override the users one in general. Therefore the only way to make this logic usable was by teaching users to never use the main conf files and to put all theirs settings in drop-ins with a higher priority than the one downsteam would use. However customizing the defaults through the main conf file is something very well established since a long time hence this is not something conceivable. This patch reworks the way we parse configuration files by introducing "early" conf files (idea from Zbigniew Jędrzejewski-Szmek), which always have a priority lower than the main config file and hence other conf file drop-ins too. Early conf files can be located in any locations where regular conf snippets can be installed and are sorted between them using the same sorting rules that apply to other conf files. A conf file is considered as an early one if its filename is prefixed with "__" (double underscore). Hence for example, drop-in "/usr/lib/systemd/logind.conf.d/__99-foo.conf" will always be parsed before: /etc/systemd/logind.conf /etc/systemd/logind.conf.d/00-foo.conf /usr/lib/systemd/logind.conf.d/00-foo.conf This change isn't backwards-compatible, but the '__' prefix is something that is unlikely used. Hence the risk should be very low. Unfortunately upstream is not seing this problem as a serious one and accept that vendors' configuration files can take precedence over the main configuration files (placed in /etc). See the following links for the related discussions: https://github.com/systemd/systemd/issues/2121 (initial issue report) https://github.com/systemd/systemd/pull/17161 (first attempt to solve this issue) https://github.com/systemd/systemd/pull/18347 (introduction of early drop-in) Since SUSE heavily relies on drop-ins to customize some of the upstream default settings, there was no other choice than to diverge from upstream in this regard. But it should be noted that these early drop-ins are strictly reserved for SUSE own purpose only. IOW users should never use them and early drop-ins should never be created in /etc but only in /usr. We reserve the right to change or drop this feature at any time. Fixes: #2121 --- src/shared/conf-parser.c | 56 ++++++++++++++-- src/test/test-conf-parser.c | 125 +++++++++++++++++++++++++++++++++++- 2 files changed, 175 insertions(+), 6 deletions(-) diff --git a/src/shared/conf-parser.c b/src/shared/conf-parser.c index ec4b53b2e8..982172dfa0 100644 --- a/src/shared/conf-parser.c +++ b/src/shared/conf-parser.c @@ -477,6 +477,7 @@ int hashmap_put_stats_by_path(Hashmap **stats_by_path, const char *path, const s static int config_parse_many_files( const char* const* conf_files, + char **early_files, char **files, const char *sections, ConfigItemLookup lookup, @@ -495,6 +496,20 @@ static int config_parse_many_files( return -ENOMEM; } + STRV_FOREACH(fn, early_files) { + r = config_parse(NULL, *fn, NULL, sections, lookup, table, flags, userdata, &st); + if (r < 0) + return r; + if (r == 0) + continue; + + if (ret_stats_by_path) { + r = hashmap_put_stats_by_path(&stats_by_path, *fn, &st); + if (r < 0) + return r; + } + } + /* First read the first found main config file. */ STRV_FOREACH(fn, conf_files) { r = config_parse(NULL, *fn, NULL, sections, lookup, table, flags, userdata, &st); @@ -533,6 +548,27 @@ static int config_parse_many_files( return 0; } +static int config_parse_split_conf_files(char **files, char ***early_files, char ***late_files) { + + assert(files); + assert(early_files); + assert(late_files); + + STRV_FOREACH(f, files) { + char ***s, *p; + + p = strdup(*f); + if (!p) + return log_oom(); + + s = startswith(basename(*f), "__") ? early_files : late_files; + if (strv_push(s, p) < 0) + return log_oom(); + } + + return 0; +} + /* Parse one main config file located in /etc/systemd and its drop-ins, which is what all systemd daemons * do. */ int config_parse_config_file( @@ -543,7 +579,8 @@ int config_parse_config_file( ConfigParseFlags flags, void *userdata) { - _cleanup_strv_free_ char **dropins = NULL, **dropin_dirs = NULL; + _cleanup_strv_free_ char **dropins = NULL, **early_dropins = NULL, **dropin_dirs = NULL; + _cleanup_strv_free_ char **files = NULL; char **conf_paths = CONF_PATHS_STRV(""); int r; @@ -571,13 +608,17 @@ int config_parse_config_file( dropin_dirs[i++] = d; } - r = conf_files_list_strv(&dropins, ".conf", NULL, 0, (const char**) dropin_dirs); + r = conf_files_list_strv(&files, ".conf", NULL, 0, (const char**) dropin_dirs); + if (r < 0) + return r; + + r = config_parse_split_conf_files(files, &early_dropins, &dropins); if (r < 0) return r; const char *sysconf_file = strjoina(PKGSYSCONFDIR, "/", conf_file); - return config_parse_many_files(STRV_MAKE_CONST(sysconf_file), dropins, + return config_parse_many_files(STRV_MAKE_CONST(sysconf_file), early_dropins, dropins, sections, lookup, table, flags, userdata, NULL); } @@ -595,6 +636,7 @@ int config_parse_many( Hashmap **ret_stats_by_path, char ***ret_dropin_files) { + _cleanup_strv_free_ char **early_dropins = NULL, **dropins = NULL; _cleanup_strv_free_ char **files = NULL; int r; @@ -607,12 +649,16 @@ int config_parse_many( if (r < 0) return r; - r = config_parse_many_files(conf_files, files, sections, lookup, table, flags, userdata, ret_stats_by_path); + r = config_parse_split_conf_files(files, &early_dropins, &dropins); if (r < 0) return r; + r = config_parse_many_files(conf_files, early_dropins, dropins, + sections, lookup, table, flags, userdata, ret_stats_by_path); + + if (ret_dropin_files) - *ret_dropin_files = TAKE_PTR(files); + *ret_dropin_files = TAKE_PTR(dropins); return 0; } diff --git a/src/test/test-conf-parser.c b/src/test/test-conf-parser.c index 0acb4131b5..88f8ff11be 100644 --- a/src/test/test-conf-parser.c +++ b/src/test/test-conf-parser.c @@ -5,7 +5,10 @@ #include "fs-util.h" #include "log.h" #include "macro.h" -#include "string-util.h" +#include "mkdir.h" +#include "nulstr-util.h" +#include "path-util.h" +#include "rm-rf.h" #include "strv.h" #include "tests.h" #include "tmpfile-util.h" @@ -390,4 +393,124 @@ TEST(config_parse) { test_config_parse_one(i, config_file[i]); } +static void setup_conf_files(const char *root, char **conf_files, char ***ret_conf_dirs) { + + STRV_FOREACH(path, conf_files) { + _cleanup_free_ char *abspath = NULL; + _cleanup_fclose_ FILE *f = NULL; + + abspath = path_join(root, *path); + assert_se(abspath); + + (void) mkdir_parents(abspath, 0755); + + f = fopen(abspath, "w"); + assert_se(f); + fprintf(f, + "[Section]\n" + "name=%s\n", + *path); + + if (ret_conf_dirs) { + _cleanup_free_ char *d = NULL; + + assert_se(path_extract_directory(abspath, &d) >= 0); + assert_se(strv_consume(ret_conf_dirs, TAKE_PTR(d)) == 0); + } + } + + if (ret_conf_dirs) { + strv_uniq(*ret_conf_dirs); + strv_sort(*ret_conf_dirs); + } +} + +static void test_config_parse_many_one(const char *main, char **dropins, const char *expected) { + + _cleanup_strv_free_ char **conf_dirs = NULL; + _cleanup_free_ char *parsed = NULL; + char *conf_file; + char *tmp_dir; + int r; + + const ConfigTableItem items[] = { + { "Section", "Field", config_parse_string, 0, &parsed}, + }; + + tmp_dir = strdupa("/tmp/test-conf-parser-XXXXXX"); + assert_se(mkdtemp(tmp_dir)); + + setup_conf_files(tmp_dir, STRV_MAKE(main), NULL); + setup_conf_files(tmp_dir, dropins, &conf_dirs); + + conf_file = main ? strjoina(tmp_dir, "/", main) : NULL; + + /* Since commit bdb2d3c6889408c7, 'conf_file_dirs' can't be NULL anymore. */ + + r = config_parse_many(STRV_MAKE_CONST(conf_file), + (const char * const*)(conf_dirs ?: STRV_MAKE_EMPTY), + /* dropin_dirname= */ "", + /* root= */ NULL, + "Section\0", + config_item_table_lookup, + items, + CONFIG_PARSE_WARN, + /* userdata= */ NULL, + /* ret_stats_by_path= */ NULL, + /* ret_dropin_files= */ NULL); + + assert_se(r == 0); + + assert_se((!!expected == !!parsed)); + assert_se(!expected || streq(expected, parsed)); + + assert_se(rm_rf(tmp_dir, REMOVE_ROOT|REMOVE_PHYSICAL) == 0); +} + +TEST(config_parse_many) { + log_info("== %s ==", __func__); + + test_config_parse_many_one(/* main= */ NULL, + /* dropins */ NULL, + /* expected_name= */ NULL); + + test_config_parse_many_one("dir/main.conf", NULL, "dir/main.conf"); + + test_config_parse_many_one(NULL, + STRV_MAKE("dir1/50-foo.conf"), + "dir1/50-foo.conf"); + + test_config_parse_many_one(NULL, + STRV_MAKE("dir1/__50-foo.conf"), + "dir1/__50-foo.conf"); + + test_config_parse_many_one(NULL, + STRV_MAKE("dir1/10-foo.conf", "dir1/50-bar.conf"), + "dir1/50-bar.conf"); + + test_config_parse_many_one(NULL, + STRV_MAKE("dir1/50-foo.conf", "dir2/10-bar.conf"), + "dir1/50-foo.conf"); + + test_config_parse_many_one(NULL, + STRV_MAKE("dir1/10-foo.conf", "dir2/10-foo.conf"), + "dir1/10-foo.conf"); + + /* Early conf files should never override the main one whatever their + * priority/location. */ + test_config_parse_many_one("dir/10-main.conf", + STRV_MAKE("dir1/__10-foo.conf", "dir2/__99-foo.conf"), + "dir/10-main.conf"); + + /* Late conf files always take precendence over the early conf files + * and the main one. */ + test_config_parse_many_one("dir/50-main.conf", + STRV_MAKE("dir1/10-foo.conf"), + "dir1/10-foo.conf"); + + test_config_parse_many_one("dir/10-main.conf", + STRV_MAKE("dir1/__10-foo.conf", "dir2/__99-foo.conf", "dir2/10-foo.conf"), + "dir2/10-foo.conf"); +} + DEFINE_TEST_MAIN(LOG_INFO); -- 2.35.3