Add support for conf.d style dirs (UPower.conf.d)

This change adds the feature to read config from conf.d style
directories (UPower.conf.d), commonly supported by other tools, as an
extension of the main config file.

This is useful and convenient in several situations, for example:

- distributions can set different values from the defaults shipped
  upstream without having to modify the main UPower.conf

- different packages or config-management tools can change config just
  by adding, removing or modifying files in that directory

The main config file, e.g. '/etc/UPower/UPower.conf', will be
processed first, and then files in the UPower.conf.d dir, if existing.

The directory to use is derived automatically, e.g.
'/etc/UPower/UPower.conf.d/' if the main config file is
'/etc/UPower/UPower.conf'.  Only files within that directory are
considered, and only those with valid config-group 'UPower' and with
the filename format: starting with '00-' to '99-', ending in '.conf'
and with alphanumeric characters, dash or underscore in between.

The candidate files within the given directory are sorted (with
g_strcmp0(), so the ordering will be as with strcmp()).  The
configuration in the files being processed later will override
previous config, in particular the main config, but also the one from
previous files processed, if the Group and Key coincide.

Add also relevant integration test: 'test_conf_d_support'
This commit is contained in:
Manuel A. Fernandez Montecelo 2025-10-10 10:36:22 +02:00 committed by Kate Hsuan
parent 5f572ffd9a
commit 46bbc8a602
3 changed files with 297 additions and 4 deletions

View file

@ -30,7 +30,7 @@ cdata.set_quoted('PACKAGE_VERSION', meson.project_version())
cdata.set_quoted('VERSION', meson.project_version())
cdata.set_quoted('PACKAGE_SYSCONF_DIR', get_option('sysconfdir'))
glib_min_version = '2.66'
glib_min_version = '2.76'
glib_version_def = 'GLIB_VERSION_@0@_@1@'.format(
glib_min_version.split('.')[0], glib_min_version.split('.')[1])

View file

@ -5904,6 +5904,144 @@ class Tests(dbusmock.DBusTestCase):
# client.get_devices_async(None, get_devices_cb)
# ml.run()
def test_conf_d_support(self):
"""Ensure support for conf.d style directories"""
base_dir = tempfile.TemporaryDirectory(delete=False, prefix="UPower-")
conf_d_dir_path = os.path.join(base_dir.name, "UPower.conf.d")
# Low, Critical and Action are all needed to avoid fallback to defaults
config = tempfile.NamedTemporaryFile(
delete=False, mode="w", dir=base_dir.name, suffix=".conf"
)
config.write("[UPower]\n")
config.write("PercentageLow=20.0\n")
config.write("PercentageCritical=3.0\n")
config.write("PercentageAction=2.0\n")
config.write("AllowRiskyCriticalPowerAction=false\n")
config.write("CriticalPowerAction=HybridSleep\n")
config.close()
# UPower.conf.d directory does not exist
self.start_daemon(cfgfile=config.name)
self.daemon_log.check_line(
"failed to find files in 'UPower.conf.d': Error opening directory",
timeout=UP_DAEMON_ACTION_DELAY + 0.5,
)
self.stop_daemon()
# empty UPower.conf.d directory
conf_d_dir = os.mkdir(path=conf_d_dir_path)
self.start_daemon(cfgfile=config.name)
self.stop_daemon()
# override config (in a UPower.conf.d/10-*.conf file)
config2 = tempfile.NamedTemporaryFile(
delete=False, mode="w", dir=conf_d_dir_path, prefix="10-", suffix=".conf"
)
config2.write("[UPower]\n")
config2.write("AllowRiskyCriticalPowerAction=true\n")
config2.write("CriticalPowerAction=Suspend\n")
config2.close()
# check warning message when CriticalPowerAction=Suspend and
# AllowRiskyCriticalPowerAction=true, meaning that support of
# UPower.conf.d dir is working
self.start_daemon(cfgfile=config.name, warns=True)
self.daemon_log.check_line(
'The "Suspend" CriticalPowerAction setting is considered risky:',
timeout=UP_DAEMON_ACTION_DELAY + 0.5,
)
self.stop_daemon()
# override config (in a UPower.conf.d/90-*.conf file)
config3 = tempfile.NamedTemporaryFile(
delete=False, mode="w", dir=conf_d_dir_path, prefix="90-", suffix=".conf"
)
config3.write("[UPower]\n")
config3.write("CriticalPowerAction=Ignore\n")
config3.close()
# check warning message when CriticalPowerAction=Ignore and
# AllowRiskyCriticalPowerAction=true, meaning that support of
# UPower.conf.d dir is working
self.start_daemon(cfgfile=config.name, warns=True)
self.daemon_log.check_line(
'The "Ignore" CriticalPowerAction setting is considered risky:',
timeout=UP_DAEMON_ACTION_DELAY + 0.5,
)
self.stop_daemon()
# config with invalid name 99-*.conf~ file (not actually overriding)
config_inv1 = tempfile.NamedTemporaryFile(
delete=False, mode="w", dir=conf_d_dir_path, prefix="99-", suffix=".conf~"
)
config_inv1.write("[UPower]\n")
config_inv1.write("CriticalPowerAction=HybridSleep\n")
config_inv1.close()
# check warning message when CriticalPowerAction=Ignore and
# AllowRiskyCriticalPowerAction=true, because the config file
# (config_inv1) setting it to the safe 'HybridSleep' is not activated
# due to wrong name
self.start_daemon(cfgfile=config.name, warns=True)
self.daemon_log.check_line(
'The "Ignore" CriticalPowerAction setting is considered risky:',
timeout=UP_DAEMON_ACTION_DELAY + 0.5,
)
self.stop_daemon()
# config with invalid name 999-*.conf file (not actually overriding)
config_inv2 = tempfile.NamedTemporaryFile(
delete=False, mode="w", dir=conf_d_dir_path, prefix="999-", suffix=".conf"
)
config_inv2.write("[UPower]\n")
config_inv2.write("CriticalPowerAction=HybridSleep\n")
config_inv2.close()
# check warning message when CriticalPowerAction=Ignore and
# AllowRiskyCriticalPowerAction=true, because the config files
# (config_inv1 and config_inv2) setting it to the safe 'HybridSleep' are
# not activated due to wrong names
self.start_daemon(cfgfile=config.name, warns=True)
self.daemon_log.check_line(
'The "Ignore" CriticalPowerAction setting is considered risky:',
timeout=UP_DAEMON_ACTION_DELAY + 0.5,
)
self.stop_daemon()
# config with invalid name 99-badname+*.conf file (not actually overriding)
config_inv3 = tempfile.NamedTemporaryFile(
delete=False,
mode="w",
dir=conf_d_dir_path,
prefix="99-badname+",
suffix=".conf",
)
config_inv3.write("[UPower]\n")
config_inv3.write("CriticalPowerAction=HybridSleep\n")
config_inv3.close()
# check warning message when CriticalPowerAction=Ignore and
# AllowRiskyCriticalPowerAction=true, because the config files
# (config_inv1, 2 and 3) setting it to the safe 'HybridSleep' are not
# activated due to wrong names
self.start_daemon(cfgfile=config.name, warns=True)
self.daemon_log.check_line(
'The "Ignore" CriticalPowerAction setting is considered risky:',
timeout=UP_DAEMON_ACTION_DELAY + 0.5,
)
self.stop_daemon()
os.unlink(config_inv3.name)
os.unlink(config_inv2.name)
os.unlink(config_inv1.name)
os.unlink(config3.name)
os.unlink(config2.name)
os.unlink(config.name)
os.rmdir(conf_d_dir_path)
os.rmdir(base_dir.name)
#
# Helper methods
#

View file

@ -104,6 +104,142 @@ up_config_class_init (UpConfigClass *klass)
object_class->finalize = up_config_finalize;
}
/**
* up_config_list_compare_files:
**/
static gint
up_config_list_compare_files (gconstpointer a, gconstpointer b)
{
return g_strcmp0 ((const gchar*)a, (const gchar*)b);
}
/**
* up_config_list_confd_files:
*
* The format of the filename should be '^([0-9][0-9])-([a-zA-Z0-9-_])*\.conf$',
* that is, starting with "00-" to "99-", ending in ".conf", and with a mix of
* alphanumeric characters with dashes and underscores in between. For example:
* '01-upower-override.conf'.
*
* Files named differently, or containing invalid groups (currently only
* 'UPower' is valid), will not be considered.
*
* The candidate files within the given directory are sorted (with g_strcmp0(),
* so the ordering will be as with strcmp()). The configuration in the files
* being processed later will override previous config, in particular the main
* config, but also the one from previous files processed, if the Group and Key
* coincide.
*
* For example, consider 'UPower.conf' that contains the defaults:
* PercentageLow=20.0
* PercentageCritical=5.0
* PercentageAction=2.0
*
* and there is a file 'UPower.conf.d/70-change-percentages.conf'
* containing settings for all 'Percentage*' keys:
* [UPower]
* PercentageLow=15.0
* PercentageCritical=10.0
* PercentageAction=5.0
*
* and another 'UPower.conf.d/99-change-percentages-local.conf'
* containing settings only for 'PercentageAction':
* [UPower]
* PercentageAction=7.5
*
* First the main 'UPower.conf' will be processed, then
* 'UPower.conf.d/70-change-percentages.conf' overriding the defaults
* of all percentages from the main config file with the given values,
* and finally 'UPower.conf.d/99-change-percentages-local.conf'
* overriding once again only 'PercentageAction'. The final, effective
* values are:
* PercentageLow=15.0
* PercentageCritical=10.0
* PercentageAction=7.5
**/
static GPtrArray*
up_config_list_confd_files (const gchar* conf_d_path, GError** error)
{
g_autoptr (GPtrArray) ret_conf_d_files = NULL;
GDir *dir = NULL;
const gchar *filename = NULL;
const char *regex_pattern = "^([0-9][0-9])-([a-zA-Z0-9-_])*\\.conf$";
g_autoptr (GRegex) regex = NULL;
dir = g_dir_open (conf_d_path, 0, error);
if (dir == NULL)
return NULL;
regex = g_regex_new (regex_pattern, G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT, NULL);
g_assert (regex != NULL);
ret_conf_d_files = g_ptr_array_new_full (0, g_free);
while ((filename = g_dir_read_name (dir)) != NULL) {
g_autofree gchar *file_path = NULL;
g_autoptr (GFile) file = NULL;
g_autoptr (GFileInfo) file_info = NULL;
if (!g_regex_match (regex, filename, G_REGEX_MATCH_DEFAULT, NULL))
continue;
file_path = g_build_filename (conf_d_path, filename, NULL);
file = g_file_new_for_path (file_path);
file_info = g_file_query_info (file,
G_FILE_ATTRIBUTE_STANDARD_TYPE,
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
NULL,
NULL);
if (file_info != NULL) {
g_debug ("Will consider additional config file '%s'", file_path);
g_ptr_array_add (ret_conf_d_files, g_strdup (file_path));
}
}
g_dir_close (dir);
g_ptr_array_sort_values (ret_conf_d_files, up_config_list_compare_files);
return g_ptr_array_ref (ret_conf_d_files);
}
/**
* up_config_override_from_confd:
**/
static void
up_config_override_from_confd (GKeyFile *key_file, const gchar* new_config_path)
{
g_autoptr (GKeyFile) new_keyfile = NULL;
gchar **keys = NULL;
gsize keys_size = 0;
new_keyfile = g_key_file_new();
if (!g_key_file_load_from_file (new_keyfile, new_config_path, G_KEY_FILE_NONE, NULL))
return;
if (!g_key_file_has_group (new_keyfile, "UPower"))
return;
keys = g_key_file_get_keys (new_keyfile, "UPower", &keys_size, NULL);
if (keys == NULL)
return;
for (gsize i = 0; i < keys_size; i++) {
g_autofree gchar *value = NULL;
g_autofree gchar *old_value = NULL;
value = g_key_file_get_value (new_keyfile, "UPower", keys[i], NULL);
if (value == NULL)
continue;
old_value = g_key_file_get_value (key_file, "UPower", keys[i], NULL);
if (old_value != NULL)
g_key_file_set_value (key_file, "UPower", keys[i], value);
}
g_strfreev (keys);
}
/**
* up_config_init:
**/
@ -112,16 +248,24 @@ up_config_init (UpConfig *config)
{
gboolean allow_risky_critical_action = FALSE;
g_autofree gchar *critical_action = NULL;
GError *error = NULL;
g_autoptr (GError) error = NULL;
g_autofree gchar *filename = NULL;
gboolean ret;
g_autofree gchar *conf_dir = NULL;
g_autofree gchar *conf_d_path = NULL;
g_autoptr (GPtrArray) conf_d_files = NULL;
config->priv = up_config_get_instance_private (config);
config->priv->keyfile = g_key_file_new ();
filename = g_strdup (g_getenv ("UPOWER_CONF_FILE_NAME"));
if (filename == NULL)
if (filename == NULL) {
filename = g_build_filename (PACKAGE_SYSCONF_DIR,"UPower", "UPower.conf", NULL);
conf_d_path = g_build_filename (PACKAGE_SYSCONF_DIR, "UPower", "UPower.conf.d", NULL);
} else {
conf_dir = g_path_get_dirname (filename);
conf_d_path = g_build_filename (conf_dir, "UPower.conf.d", NULL);
}
/* load */
ret = g_key_file_load_from_file (config->priv->keyfile,
@ -132,7 +276,18 @@ up_config_init (UpConfig *config)
if (!ret) {
g_warning ("failed to load config file '%s': %s",
filename, error->message);
g_error_free (error);
g_clear_error (&error);
}
conf_d_files = up_config_list_confd_files (conf_d_path, &error);
if (conf_d_files != NULL) {
for (guint i = 0; i < conf_d_files->len; i++) {
const gchar* conf_d_file = (const gchar*)(g_ptr_array_index (conf_d_files, i));
up_config_override_from_confd (config->priv->keyfile,
conf_d_file);
}
} else {
g_debug ("failed to find files in 'UPower.conf.d': %s", error->message);
}
/* Warn for any dangerous configurations */