sysctl: add --save= option to save loaded settings

This may be useful for saving sysctl settings provided on the command line,
via standard input, or with --inline.
This commit is contained in:
Yu Watanabe
2026-08-21 00:45:13 +09:00
parent 1c223728cf
commit 45bd015441
+120 -3
View File
@@ -10,16 +10,20 @@
#include "constants.h"
#include "creds-util.h"
#include "errno-util.h"
#include "fs-util.h"
#include "glob-util.h"
#include "hashmap.h"
#include "log.h"
#include "main-func.h"
#include "mkdir.h"
#include "options.h"
#include "pager.h"
#include "path-util.h"
#include "pretty-print.h"
#include "string-util.h"
#include "strv.h"
#include "sysctl-util.h"
#include "tmpfile-util.h"
#include "verbs.h"
static char **arg_prefixes = NULL;
@@ -28,8 +32,10 @@ static bool arg_strict = false;
static bool arg_verify = false;
static bool arg_inline = false;
static PagerFlags arg_pager_flags = 0;
static char *arg_save_file = NULL;
STATIC_DESTRUCTOR_REGISTER(arg_prefixes, strv_freep);
STATIC_DESTRUCTOR_REGISTER(arg_save_file, freep);
COMMAND(
"systemd-sysctl\0",
@@ -226,6 +232,13 @@ static int apply_all(OrderedHashmap *sysctl_options) {
return r;
}
static void filter_sysctl_options(OrderedHashmap *sysctl_options) {
SysctlOption *option;
ORDERED_HASHMAP_FOREACH(option, sysctl_options)
if (!string_is_glob(option->key) && !test_prefix(option->key))
sysctl_option_free(ordered_hashmap_remove(sysctl_options, option->key));
}
static int parse_line(const char *fname, unsigned line, const char *buffer, bool *invalid_config, void *userdata) {
OrderedHashmap **sysctl_options = ASSERT_PTR(userdata);
_cleanup_free_ char *k = NULL, *v = NULL;
@@ -262,9 +275,19 @@ static int parse_line(const char *fname, unsigned line, const char *buffer, bool
const char *key = sysctl_normalize(strstrip(k)), *value = strstrip(v);
/* We can't filter out globs at this point, we'll need to do that later. */
if (!string_is_glob(key) && !test_prefix(key))
return 0;
if (arg_save_file) {
/* When saving the loaded rules, refuse unsafe characters, especially newlines, in keys
* and values. Otherwise, the saved file may not be parsed correctly. */
if (!string_is_safe(key, STRING_ALLOW_BACKSLASHES | STRING_ALLOW_QUOTES | STRING_ALLOW_GLOBS) ||
(value && !string_is_safe(value, STRING_ALLOW_EMPTY | STRING_ALLOW_BACKSLASHES | STRING_ALLOW_QUOTES | STRING_ALLOW_GLOBS)))
return log_syntax(NULL, LOG_WARNING, fname, line, SYNTHETIC_ERRNO(EINVAL),
"Ignoring unsafe key and/or value for '%s'.", key);
} else {
/* We can't filter out globs at this point, we'll need to do that later.
* Keep them when saving the loaded rules, and filter them after writing. */
if (!string_is_glob(key) && !test_prefix(key))
return 0;
}
SysctlOption *existing = ordered_hashmap_get(*sysctl_options, key);
if (existing) {
@@ -325,7 +348,82 @@ static int cat_config(char **files) {
return cat_files(NULL, files, arg_cat_flags);
}
static int save_sysctl_options(OrderedHashmap *sysctl_options, const char *filename) {
int r;
assert(filename);
if (ordered_hashmap_isempty(sysctl_options)) {
log_info("No sysctl settings loaded, not creating '%s'.", filename);
return 0;
}
r = mkdir_p("/run/sysctl.d/", 0755);
if (r < 0)
return log_error_errno(r, "Failed to create directory '/run/sysctl.d/': %m");
_cleanup_free_ char *path = path_join("/run/sysctl.d/", filename);
if (!path)
return log_oom();
_cleanup_fclose_ FILE *f = NULL;
_cleanup_(unlink_and_freep) char *temp_path = NULL;
r = fopen_temporary(path, &f, &temp_path);
if (r < 0)
return log_error_errno(r, "Failed to open a temporary file for '%s': %m", path);
if (fchmod(fileno(f), 0644) < 0)
return log_error_errno(errno, "Failed to chmod '%s': %m", temp_path);
fputs("# This is generated by systemd-sysctl.\n", f);
SysctlOption *option;
ORDERED_HASHMAP_FOREACH(option, sysctl_options)
fprintf(f, "%s%s%s%s\n",
option->ignore_failure || !option->value ? "-" : "",
option->key,
option->value ? " = " : "",
strempty(option->value));
r = fflush_and_check(f);
if (r < 0)
return log_error_errno(r, "Failed to flush '%s': %m", temp_path);
r = conservative_rename(temp_path, path);
if (r < 0)
return log_error_errno(r, "Failed to rename '%s' to '%s': %m", temp_path, path);
temp_path = mfree(temp_path);
return 0;
}
static int parse_conf_filename(const char *src, const char *option, char **dest) {
assert(option);
assert(dest);
if (isempty(src))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot use an empty filename for %s.", option);
if (startswith(src, "."))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Dot-prefixed filenames are not allowed for %s.", option);
_cleanup_free_ char *name = NULL;
if (!endswith(src, ".conf"))
name = strjoin(src, ".conf");
else
name = strdup(src);
if (!name)
return log_oom();
if (!filename_is_valid(name))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid file name '%s' specified for %s.", name, option);
return free_and_replace(*dest, name);
}
static int parse_argv(int argc, char *argv[], char ***remaining_args) {
int r;
assert(argc >= 0);
assert(argv);
assert(remaining_args);
@@ -391,6 +489,13 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
arg_inline = true;
break;
OPTION_LONG("save", "FILENAME",
"Write loaded settings to the specified file"):
r = parse_conf_filename(opts.arg, "--save=", &arg_save_file);
if (r < 0)
return r;
break;
OPTION_COMMON_INTROSPECT_CLI:
return introspect_cli(SD_JSON_FORMAT_OFF);
}
@@ -405,6 +510,10 @@ static int parse_argv(int argc, char *argv[], char ***remaining_args) {
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"--inline requires positional arguments.");
if (arg_save_file && strv_isempty(*remaining_args))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
"--save= requires positional arguments.");
return 1;
}
@@ -437,6 +546,14 @@ static int run(int argc, char *argv[]) {
} else
RET_GATHER(r, parse_file(&sysctl_options, *arg, false));
}
if (arg_save_file) {
r = save_sysctl_options(sysctl_options, arg_save_file);
if (r < 0)
return r;
filter_sysctl_options(sysctl_options);
}
} else {
_cleanup_strv_free_ char **files = NULL;