diff --git a/TODO.md b/TODO.md
index 354af92755..03d0462ef7 100644
--- a/TODO.md
+++ b/TODO.md
@@ -265,9 +265,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- sysupdate: add concept for enabling/disabling specific components explicitly,
just like features.
-- udev: add a MACHINE_TAGS field, that augments /etc/machine-info configured
- tags.
-
- hostnamectl: management, collation of all tags. four sources: udev,
/etc/machine-info, credentials, and /etc/machine-tags.d/*.conf
@@ -780,7 +777,6 @@ SPDX-License-Identifier: LGPL-2.1-or-later
- clean up date formatting and parsing so that all absolute/relative timestamps we format can also be parsed
- **complete varlink introspection comments:**
- - io.systemd.Hostname
- io.systemd.ManagedOOM
- io.systemd.Network
- io.systemd.PCRLock
@@ -2431,8 +2427,8 @@ SPDX-License-Identifier: LGPL-2.1-or-later
PR_SET_DUMPABLE so that it cannot be ptraced from the host. Should have
CAP_SYS_BPF as only service around.
-- SIGRTMIN+18 and memory pressure handling should still be added to: hostnamed,
- localed, oomd, timedated.
+- SIGRTMIN+18 and memory pressure handling should still be added to: localed,
+ oomd, timedated.
- socket units: allow creating a udev monitor socket with ListenDevices= or so,
with matches, then activate app through that passing socket over
diff --git a/man/hostnamectl.xml b/man/hostnamectl.xml
index 878837bee1..c120b938c9 100644
--- a/man/hostnamectl.xml
+++ b/man/hostnamectl.xml
@@ -177,6 +177,14 @@
resulting list is sorted before being stored. To remove all tags, invoke the command with a single
empty string argument.
+ Alternatively, each argument may be prefixed with + or -
+ to incrementally update the configured tags rather than replacing them: an argument prefixed with
+ + adds all of its (colon-separated) tags to the list, one prefixed with
+ - removes them. For example, hostnamectl tags +foo:bar -quux:ooo
+ adds the tags foo and bar and removes quux
+ and ooo. The prefix applies to the whole argument, i.e. to every tag listed within
+ it. Prefixed and unprefixed arguments may not be combined in a single invocation.
+
Machine tags are short labels that may be used to classify and group machines for management
purposes, for example to identify the role a machine plays in a deployment, the fleet or
organizational unit it belongs to, or any other administrator-defined attribute. Each individual tag
diff --git a/man/org.freedesktop.hostname1.xml b/man/org.freedesktop.hostname1.xml
index d5571de207..67b52e1ed4 100644
--- a/man/org.freedesktop.hostname1.xml
+++ b/man/org.freedesktop.hostname1.xml
@@ -57,6 +57,8 @@ node /org/freedesktop/hostname1 {
SetLocation(in s location,
in b interactive);
SetTags(in as tags);
+ AddAndRemoveTags(in as add,
+ in as remove);
GetProductUUID(in b interactive,
out ay uuid);
GetHardwareSerial(out s serial);
@@ -146,6 +148,8 @@ node /org/freedesktop/hostname1 {
+
+
@@ -385,6 +389,13 @@ node /org/freedesktop/hostname1 {
alphanumeric characters, - and .). Passing an empty list removes
the TAGS= field from /etc/machine-info.
+ AddAndRemoveTags() incrementally updates the Tags
+ property instead of replacing it. It takes two lists of machine tags: the tags in add
+ are added to the current list, and the tags in remove are dropped from it (the
+ resulting list is (current ∪ add) \ remove, sorted and deduplicated). This is useful
+ when several independent agents each manage a subset of the tags without racing on a read-modify-write
+ of the full list. Each tag must be a valid machine tag, as for SetTags().
+
PrettyHostname, IconName, Chassis,
Deployment, Location, and Tags are stored in
/etc/machine-info. See
@@ -421,7 +432,8 @@ node /org/freedesktop/hostname1 {
SetStaticHostname() and SetPrettyHostname() it is
org.freedesktop.hostname1.set-static-hostname. For
SetIconName(), SetChassis(), SetDeployment(),
- SetLocation() and SetTags() it is
+ SetLocation(), SetTags() and
+ AddAndRemoveTags() it is
org.freedesktop.hostname1.set-machine-info.
@@ -527,6 +539,7 @@ node /org/freedesktop/hostname1 {
OperatingSystemFancyName was added in version 260.
GetMachineInfo(), Tags and
SetTags() were added in version 261.
+ AddAndRemoveTags() was added in version 262.
diff --git a/man/rules/meson.build b/man/rules/meson.build
index 92238b717a..a3d8500cc6 100644
--- a/man/rules/meson.build
+++ b/man/rules/meson.build
@@ -1107,6 +1107,7 @@ manpages = [
['systemd-loop@.service', '8', [], ''],
['systemd-machine-id-commit.service', '8', [], ''],
['systemd-machine-id-setup', '1', [], ''],
+ ['systemd-machine-tag@.service', '8', [], 'ENABLE_HOSTNAMED'],
['systemd-machined.service', '8', ['systemd-machined'], 'ENABLE_MACHINED'],
['systemd-makefs@.service',
'8',
diff --git a/man/systemd-machine-tag@.service.xml b/man/systemd-machine-tag@.service.xml
new file mode 100644
index 0000000000..b5bc1d81c7
--- /dev/null
+++ b/man/systemd-machine-tag@.service.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+ systemd-machine-tag@.service
+ systemd
+
+
+
+ systemd-machine-tag@.service
+ 8
+
+
+
+ systemd-machine-tag@.service
+ Add a machine tag to the host
+
+
+
+ systemd-machine-tag@.service
+
+
+
+ Description
+
+ systemd-machine-tag@TAG.service is a service
+ template that adds the machine tag TAG (taken from the instance name) to the
+ host's list of machine tags. Whenever an instance of this template is pulled in, the corresponding tag is
+ added; the operation is idempotent, so pulling it in repeatedly has no further effect.
+
+ The primary use of this template is automatic, hardware-driven tagging: the
+ SYSTEMD_MACHINE_TAG udev property, when set on a device, causes the matching instance
+ of this template to be pulled in (via SYSTEMD_WANTS=), so that plugging in or
+ detecting a specific device automatically tags the host. See
+ systemd.device5 for
+ details on the SYSTEMD_WANTS= mechanism.
+
+ Machine tags may be inspected and edited with the tags verb of
+ hostnamectl1, and are
+ stored in the TAGS= field of
+ machine-info5. Each
+ tag must be 1…255 characters long and consist only of ASCII alphanumeric characters,
+ - and ..
+
+
+
+
+
+ See Also
+
+ systemd1
+ hostnamectl1
+ systemd-hostnamed.service8
+ machine-info5
+ udev7
+
+
+
+
diff --git a/rules.d/99-systemd.rules.in b/rules.d/99-systemd.rules.in
index e7b6947e81..fb7d6462af 100644
--- a/rules.d/99-systemd.rules.in
+++ b/rules.d/99-systemd.rules.in
@@ -98,4 +98,7 @@ SUBSYSTEM=="tpm", KERNEL=="tpm[0-9]*", TAG+="systemd"
SUBSYSTEM=="block", ENV{ID_CDROM}=="1", ENV{ID_PART_GPT_AUTO_ROOT_DISK_NEEDS_LOOP}=="1", \
ENV{SYSTEMD_WANTS}+="systemd-loop@.service"
+# A device carrying the SYSTEMD_MACHINE_TAG property auto-tags the host with the specified machine tag.
+ENV{SYSTEMD_MACHINE_TAG}=="?*", TAG+="systemd", ENV{SYSTEMD_WANTS}+="systemd-machine-tag@$env{SYSTEMD_MACHINE_TAG}.service"
+
LABEL="systemd_end"
diff --git a/src/basic/strv.c b/src/basic/strv.c
index 4044a129c6..e4f9373f8a 100644
--- a/src/basic/strv.c
+++ b/src/basic/strv.c
@@ -812,16 +812,14 @@ bool strv_is_uniq(char * const *l) {
}
char** strv_remove(char **l, const char *s) {
- char **f, **t;
-
if (!l)
return NULL;
assert(s);
- /* Drops every occurrence of s in the string list, edits
- * in-place. */
+ /* Drops every occurrence of s in the string list, edits in-place. */
+ char **f, **t;
for (f = t = l; *f; f++)
if (streq(*f, s))
free(*f);
@@ -832,6 +830,17 @@ char** strv_remove(char **l, const char *s) {
return l;
}
+char** strv_remove_strv(char **l, char *const*ll) {
+
+ if (strv_isempty(l))
+ return l;
+
+ STRV_FOREACH(i, ll)
+ strv_remove(l, *i);
+
+ return l;
+}
+
bool strv_overlap(char * const *a, char * const *b) {
POINTER_MAY_BE_NULL(a);
POINTER_MAY_BE_NULL(b);
diff --git a/src/basic/strv.h b/src/basic/strv.h
index cf6fa158b6..7bac1b66fa 100644
--- a/src/basic/strv.h
+++ b/src/basic/strv.h
@@ -85,6 +85,7 @@ int strv_consume_pair(char ***l, char *a, char *b);
int strv_consume_prepend(char ***l, char *value);
char** strv_remove(char **l, const char *s);
+char** strv_remove_strv(char **l, char *const*ll);
char** strv_uniq(char **l);
bool strv_is_uniq(char * const *l) _pure_;
diff --git a/src/hostname/hostnamectl.c b/src/hostname/hostnamectl.c
index d52580fbf5..a6ecad5178 100644
--- a/src/hostname/hostnamectl.c
+++ b/src/hostname/hostnamectl.c
@@ -772,25 +772,75 @@ static int verb_get_or_set_tags(int argc, char *argv[], uintptr_t _data, void *u
return 0;
}
- _cleanup_strv_free_ char **l = NULL;
+ _cleanup_strv_free_ char **plain = NULL, **add = NULL, **remove = NULL;
+ bool prefixed = false, unprefixed = false;
for (int i = 1; i < argc; i++) {
- r = strv_split_and_extend(&l, argv[i], ":", /* filter_duplicates= */ true);
- if (r < 0)
+ const char *e = argv[i];
+ char ***dest;
+
+ /* The first character of each argument selects the mode for all (colon-separated) tags in
+ * that argument: a leading '+' adds them, a leading '-' removes them, and otherwise the
+ * argument lists tags to set verbatim. Setting and adding/removing may not be mixed. */
+ if (e[0] == '+') {
+ dest = &add;
+ e++;
+ prefixed = true;
+ } else if (e[0] == '-') {
+ dest = &remove;
+ e++;
+ prefixed = true;
+ } else {
+ dest = &plain;
+ unprefixed = true;
+ }
+
+ if (isempty(e)) {
+ if (dest == &plain && !*dest) {
+ *dest = new0(char*, 1);
+ if (!*dest)
+ return log_oom();
+ }
+ } else if (strv_split_and_extend(dest, e, ":", /* filter_duplicates= */ true) < 0)
return log_oom();
}
- strv_sort(l);
+ if (prefixed && unprefixed)
+ return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
+ "Plain tags may not be combined with '+'/'-' prefixed tags.");
(void) polkit_agent_open_if_enabled(arg_transport, arg_ask_password);
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
- r = bus_message_new_method_call(bus, &m, bus_hostname, "SetTags");
- if (r < 0)
- return bus_log_create_error(r);
- r = sd_bus_message_append_strv(m, l);
- if (r < 0)
- return bus_log_create_error(r);
+ if (!prefixed) {
+ /* No prefixes used: replace the whole tag list with the given tags (possibly empty, which
+ * clears all tags). */
+ strv_sort(plain);
+
+ r = bus_message_new_method_call(bus, &m, bus_hostname, "SetTags");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_strv(m, plain);
+ if (r < 0)
+ return bus_log_create_error(r);
+ } else {
+ /* Prefixes used: incrementally add and/or remove tags. */
+ strv_sort(add);
+ strv_sort(remove);
+
+ r = bus_message_new_method_call(bus, &m, bus_hostname, "AddAndRemoveTags");
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_strv(m, add);
+ if (r < 0)
+ return bus_log_create_error(r);
+
+ r = sd_bus_message_append_strv(m, remove);
+ if (r < 0)
+ return bus_log_create_error(r);
+ }
r = sd_bus_call(bus, m, /* usec= */ 0, &error, /* ret_reply= */ NULL);
if (r < 0)
diff --git a/src/hostname/hostnamed.c b/src/hostname/hostnamed.c
index 73c0f2e8ef..098ce80500 100644
--- a/src/hostname/hostnamed.c
+++ b/src/hostname/hostnamed.c
@@ -17,6 +17,7 @@
#include "bus-object.h"
#include "bus-polkit.h"
#include "bus-util.h"
+#include "common-signal.h"
#include "constants.h"
#include "daemon-util.h"
#include "device-private.h"
@@ -36,7 +37,6 @@
#include "nulstr-util.h"
#include "os-util.h"
#include "parse-util.h"
-#include "path-util.h"
#include "service-util.h"
#include "stat-util.h"
#include "string-util.h"
@@ -541,7 +541,7 @@ static int get_firmware_date(Context *c, usec_t *ret) {
return 0;
}
-static const char* valid_chassis(const char *chassis) {
+static const char* chassis_is_valid(const char *chassis) {
assert(chassis);
return nulstr_get(
@@ -558,10 +558,29 @@ static const char* valid_chassis(const char *chassis) {
chassis);
}
-static bool valid_deployment(const char *deployment) {
+static bool deployment_is_valid(const char *deployment) {
assert(deployment);
- return in_charset(deployment, VALID_DEPLOYMENT_CHARS);
+ return !isempty(deployment) &&
+ in_charset(deployment, VALID_DEPLOYMENT_CHARS);
+}
+
+static bool pretty_hostname_is_valid(const char *pretty_hostname) {
+ assert(pretty_hostname);
+
+ return string_is_safe(pretty_hostname, STRING_ALLOW_BACKSLASHES|STRING_ALLOW_QUOTES|STRING_ALLOW_GLOBS);
+}
+
+static bool icon_name_is_valid(const char *icon_name) {
+ assert(icon_name);
+
+ return string_is_safe(icon_name, STRING_FILENAME);
+}
+
+static bool location_is_valid(const char *location) {
+ assert(location);
+
+ return string_is_safe(location, STRING_ALLOW_BACKSLASHES|STRING_ALLOW_QUOTES|STRING_ALLOW_GLOBS);
}
static const char* fallback_chassis_by_virtualization(void) {
@@ -710,7 +729,7 @@ static const char* fallback_chassis_by_device_tree(Context *c) {
*
* https://github.com/devicetree-org/devicetree-specification/blob/master/source/chapter3-devicenodes.rst */
- chassis = valid_chassis(type);
+ chassis = chassis_is_valid(type);
if (!chassis)
log_debug("Invalid device-tree chassis type \"%s\", ignoring.", type);
return chassis;
@@ -828,7 +847,7 @@ static int context_write_data_static_hostname(Context *c) {
if (isempty(c->data[PROP_STATIC_HOSTNAME])) {
if (unlink(etc_hostname()) < 0 && errno != ENOENT)
- return -errno;
+ return log_error_errno(errno, "Failed to remove '%s': %m", etc_hostname());
TAKE_PTR(s);
return 0;
@@ -836,7 +855,7 @@ static int context_write_data_static_hostname(Context *c) {
r = write_string_file(etc_hostname(), c->data[PROP_STATIC_HOSTNAME], WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_LABEL);
if (r < 0)
- return r;
+ return log_error_errno(r, "Failed to write '%s': %m", etc_hostname());
TAKE_PTR(s);
return 0;
@@ -863,19 +882,18 @@ static int context_write_data_machine_info(Context *c) {
r = load_env_file(NULL, etc_machine_info(), &l);
if (r < 0 && r != -ENOENT)
- return r;
+ return log_error_errno(r, "Failed to read '%s': %m", etc_machine_info());
for (HostProperty p = _PROP_MACHINE_INFO_SETTABLE_FIRST; p <= _PROP_MACHINE_INFO_SETTABLE_LAST; p++) {
assert(name[p]);
- r = strv_env_assign(&l, name[p], empty_to_null(c->data[p]));
- if (r < 0)
- return r;
+ if (strv_env_assign(&l, name[p], empty_to_null(c->data[p])) < 0)
+ return log_oom();
}
if (strv_isempty(l)) {
if (unlink(etc_machine_info()) < 0 && errno != ENOENT)
- return -errno;
+ return log_error_errno(errno, "Failed to unlink '%s': %m", etc_machine_info());
TAKE_PTR(s);
return 0;
@@ -888,7 +906,7 @@ static int context_write_data_machine_info(Context *c) {
l,
WRITE_ENV_FILE_LABEL);
if (r < 0)
- return r;
+ return log_error_errno(r, "Failed to write '%s': %m", etc_machine_info());
TAKE_PTR(s);
return 0;
@@ -1348,7 +1366,7 @@ static int property_get_vsock_cid(
return sd_bus_message_append(reply, "u", (uint32_t) local_cid);
}
-static int validate_and_substitute_hostname(const char *name, char **ret_substituted, sd_bus_error *error) {
+static int validate_and_substitute_hostname(const char *name, char **ret_substituted) {
int r;
assert(ret_substituted);
@@ -1367,12 +1385,24 @@ static int validate_and_substitute_hostname(const char *name, char **ret_substit
return log_error_errno(r, "Failed to substitute wildcards in hostname: %m");
if (!hostname_is_valid(substituted, 0))
- return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid hostname '%s'", name);
+ return -EUCLEAN;
*ret_substituted = TAKE_PTR(substituted);
return 1;
}
+static int bus_validate_and_substitute_hostname(const char *name, char **ret_substituted, sd_bus_error *error) {
+ int r;
+
+ assert(ret_substituted);
+
+ r = validate_and_substitute_hostname(name, ret_substituted);
+ if (r == -EUCLEAN)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid hostname '%s'", name);
+
+ return r;
+}
+
static int method_set_hostname(sd_bus_message *m, void *userdata, sd_bus_error *error) {
Context *c = ASSERT_PTR(userdata);
const char *name;
@@ -1390,7 +1420,7 @@ static int method_set_hostname(sd_bus_message *m, void *userdata, sd_bus_error *
* we might want to adjust hostname source information even if the actual hostname is unchanged. */
_cleanup_free_ char *substituted = NULL;
- r = validate_and_substitute_hostname(name, &substituted, error);
+ r = bus_validate_and_substitute_hostname(name, &substituted, error);
if (r < 0)
return r;
@@ -1441,7 +1471,7 @@ static int method_set_static_hostname(sd_bus_message *m, void *userdata, sd_bus_
return sd_bus_reply_method_return(m, NULL);
_cleanup_free_ char *substituted = NULL;
- r = validate_and_substitute_hostname(name, &substituted, error);
+ r = bus_validate_and_substitute_hostname(name, &substituted, error);
if (r < 0)
return r;
@@ -1466,7 +1496,6 @@ static int method_set_static_hostname(sd_bus_message *m, void *userdata, sd_bus_
r = context_write_data_static_hostname(c);
if (r < 0) {
- log_error_errno(r, "Failed to write static hostname: %m");
if (ERRNO_IS_PRIVILEGE(r))
return sd_bus_error_set(error, BUS_ERROR_FILE_IS_PROTECTED, "Not allowed to update /etc/hostname.");
if (r == -EROFS)
@@ -1475,10 +1504,8 @@ static int method_set_static_hostname(sd_bus_message *m, void *userdata, sd_bus_
}
r = context_update_kernel_hostname(c, /* transient_hostname= */ NULL);
- if (r < 0) {
- log_error_errno(r, "Failed to set hostname: %m");
+ if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to set hostname: %m");
- }
(void) sd_bus_emit_properties_changed(sd_bus_message_get_bus(m),
"/org/freedesktop/hostname1", "org.freedesktop.hostname1",
@@ -1510,15 +1537,15 @@ static int set_machine_info(Context *c, sd_bus_message *m, int prop, sd_bus_mess
/* The icon name might ultimately be used as file
* name, so better be safe than sorry */
- if (prop == PROP_ICON_NAME && !filename_is_valid(name))
+ if (prop == PROP_ICON_NAME && !icon_name_is_valid(name))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid icon name '%s'", name);
- if (prop == PROP_PRETTY_HOSTNAME && string_has_cc(name, NULL))
+ if (prop == PROP_PRETTY_HOSTNAME && !pretty_hostname_is_valid(name))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid pretty hostname '%s'", name);
- if (prop == PROP_CHASSIS && !valid_chassis(name))
+ if (prop == PROP_CHASSIS && !chassis_is_valid(name))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid chassis '%s'", name);
- if (prop == PROP_DEPLOYMENT && !valid_deployment(name))
+ if (prop == PROP_DEPLOYMENT && !deployment_is_valid(name))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid deployment '%s'", name);
- if (prop == PROP_LOCATION && string_has_cc(name, NULL))
+ if (prop == PROP_LOCATION && !location_is_valid(name))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid location '%s'", name);
}
@@ -1544,7 +1571,6 @@ static int set_machine_info(Context *c, sd_bus_message *m, int prop, sd_bus_mess
r = context_write_data_machine_info(c);
if (r < 0) {
- log_error_errno(r, "Failed to write machine info: %m");
if (ERRNO_IS_PRIVILEGE(r))
return sd_bus_error_set(error, BUS_ERROR_FILE_IS_PROTECTED, "Not allowed to update /etc/machine-info.");
if (r == -EROFS)
@@ -1590,6 +1616,52 @@ static int method_set_location(sd_bus_message *m, void *userdata, sd_bus_error *
return set_machine_info(userdata, m, PROP_LOCATION, method_set_location, error);
}
+static int context_store_tags(Context *c, char **tags) {
+ int r;
+
+ assert(c);
+
+ /* Persists the given machine tags (which must already be validated, sorted and deduplicated) to
+ * /etc/machine-info and emits a PropertiesChanged signal on the Tags property. */
+
+ if (strv_isempty(tags))
+ c->data[PROP_TAGS] = mfree(c->data[PROP_TAGS]);
+ else {
+ _cleanup_free_ char *j = strv_join(tags, ":");
+ if (!j)
+ return log_oom();
+
+ free_and_replace(c->data[PROP_TAGS], j);
+ }
+
+ r = context_write_data_machine_info(c);
+ if (r < 0)
+ return r;
+
+ log_info("Changed tags to '%s'", strempty(c->data[PROP_TAGS]));
+
+ (void) sd_bus_emit_properties_changed(
+ c->bus,
+ "/org/freedesktop/hostname1",
+ "org.freedesktop.hostname1",
+ "Tags",
+ NULL);
+
+ return 0;
+}
+
+static int bus_error_from_tags_write(sd_bus_error *error, int r) {
+ assert(error);
+ assert(r < 0);
+
+ log_error_errno(r, "Failed to write machine info: %m");
+ if (ERRNO_IS_PRIVILEGE(r))
+ return sd_bus_error_set(error, BUS_ERROR_FILE_IS_PROTECTED, "Not allowed to update /etc/machine-info.");
+ if (r == -EROFS)
+ return sd_bus_error_set(error, BUS_ERROR_READ_ONLY_FILESYSTEM, "/etc/machine-info is in a read-only filesystem.");
+ return sd_bus_error_set_errnof(error, r, "Failed to write machine info: %m");
+}
+
static int method_set_tags(sd_bus_message *m, void *userdata, sd_bus_error *error) {
Context *c = ASSERT_PTR(userdata);
int r;
@@ -1615,7 +1687,12 @@ static int method_set_tags(sd_bus_message *m, void *userdata, sd_bus_error *erro
context_read_machine_info(c);
- if (streq_ptr(empty_to_null(j), empty_to_null(c->data[PROP_TAGS])))
+ _cleanup_strv_free_ char **current = NULL;
+ r = machine_tags_from_string(c->data[PROP_TAGS], /* graceful= */ true, ¤t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse current machine tags: %m");
+
+ if (strv_equal(current, tags))
return sd_bus_reply_method_return(m, NULL);
r = bus_verify_polkit_async_full(
@@ -1631,29 +1708,98 @@ static int method_set_tags(sd_bus_message *m, void *userdata, sd_bus_error *erro
if (r == 0)
return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
- if (strv_isempty(tags))
- c->data[PROP_TAGS] = mfree(c->data[PROP_TAGS]);
- else
- free_and_replace(c->data[PROP_TAGS], j);
+ r = context_store_tags(c, tags);
+ if (r < 0)
+ return bus_error_from_tags_write(error, r);
- r = context_write_data_machine_info(c);
- if (r < 0) {
- log_error_errno(r, "Failed to write machine info: %m");
- if (ERRNO_IS_PRIVILEGE(r))
- return sd_bus_error_set(error, BUS_ERROR_FILE_IS_PROTECTED, "Not allowed to update /etc/machine-info.");
- if (r == -EROFS)
- return sd_bus_error_set(error, BUS_ERROR_READ_ONLY_FILESYSTEM, "/etc/machine-info is in a read-only filesystem.");
- return sd_bus_error_set_errnof(error, r, "Failed to write machine info: %m");
+ return sd_bus_reply_method_return(m, NULL);
+}
+
+static int machine_tags_add_remove(char * const *base, char * const *add, char * const *remove, char ***ret) {
+ int r;
+
+ assert(ret);
+
+ /* Computes the resulting machine tag list when adding 'add' to and removing 'remove' from the 'base'
+ * list, i.e. (base ∪ add) \ remove, sorted and deduplicated. Shared by the D-Bus AddAndRemoveTags()
+ * and Varlink SetTags() implementations. */
+
+ _cleanup_strv_free_ char **tags = strv_copy(base);
+ if (!tags)
+ return -ENOMEM;
+
+ r = strv_extend_strv(&tags, add, /* filter_duplicates= */ true);
+ if (r < 0)
+ return r;
+
+ strv_remove_strv(tags, remove);
+
+ strv_sort_uniq(tags);
+
+ if (strv_length(tags) > MACHINE_TAGS_MAX)
+ return -E2BIG;
+
+ *ret = TAKE_PTR(tags);
+ return 0;
+}
+
+static int method_add_and_remove_tags(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+ Context *c = ASSERT_PTR(userdata);
+ int r;
+
+ assert(m);
+
+ _cleanup_strv_free_ char **add = NULL, **remove = NULL;
+ r = sd_bus_message_read_strv(m, &add);
+ if (r < 0)
+ return r;
+ r = sd_bus_message_read_strv(m, &remove);
+ if (r < 0)
+ return r;
+
+ if (!machine_tag_list_is_valid(add)) {
+ _cleanup_free_ char *j = strv_join(add, ":");
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid tags to add '%s'", strna(j));
+ }
+ if (!machine_tag_list_is_valid(remove)) {
+ _cleanup_free_ char *j = strv_join(remove, ":");
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid tags to remove '%s'", strna(j));
}
- log_info("Changed tags to '%s'", strempty(c->data[PROP_TAGS]));
+ context_read_machine_info(c);
- (void) sd_bus_emit_properties_changed(
- sd_bus_message_get_bus(m),
- "/org/freedesktop/hostname1",
- "org.freedesktop.hostname1",
- "Tags",
- NULL);
+ /* Start from the current tags, add the requested ones, then drop the ones to be removed. */
+ _cleanup_strv_free_ char **current = NULL;
+ r = machine_tags_from_string(c->data[PROP_TAGS], /* graceful= */ true, ¤t);
+ if (r < 0)
+ return log_error_errno(r, "Failed to parse current machine tags: %m");
+
+ _cleanup_strv_free_ char **tags = NULL;
+ r = machine_tags_add_remove(current, add, remove, &tags);
+ if (r == -E2BIG)
+ return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Too many machine tags specified.");
+ if (r < 0)
+ return log_oom();
+
+ if (strv_equal(current, tags))
+ return sd_bus_reply_method_return(m, NULL);
+
+ r = bus_verify_polkit_async_full(
+ m,
+ "org.freedesktop.hostname1.set-machine-info",
+ /* details= */ NULL,
+ /* good_user= */ UID_INVALID,
+ /* flags= */ 0,
+ &c->polkit_registry,
+ error);
+ if (r < 0)
+ return r;
+ if (r == 0)
+ return 1; /* No authorization for now, but the async polkit stuff will call us again when it has it */
+
+ r = context_store_tags(c, tags);
+ if (r < 0)
+ return bus_error_from_tags_write(error, r);
return sd_bus_reply_method_return(m, NULL);
}
@@ -2014,6 +2160,11 @@ static const sd_bus_vtable hostname_vtable[] = {
SD_BUS_NO_RESULT,
method_set_tags,
SD_BUS_VTABLE_UNPRIVILEGED),
+ SD_BUS_METHOD_WITH_ARGS("AddAndRemoveTags",
+ SD_BUS_ARGS("as", add, "as", remove),
+ SD_BUS_NO_RESULT,
+ method_add_and_remove_tags,
+ SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("GetProductUUID",
SD_BUS_ARGS("b", interactive),
SD_BUS_RESULT("ay", uuid),
@@ -2109,6 +2260,333 @@ static int vl_method_describe(sd_varlink *link, sd_json_variant *parameters, sd_
return sd_varlink_reply(link, v);
}
+static int vl_validate_and_substitute_hostname(sd_varlink *link, const char *name, char **ret_substituted) {
+ int r;
+
+ assert(link);
+ assert(ret_substituted);
+
+ r = validate_and_substitute_hostname(name, ret_substituted);
+ if (r == -EUCLEAN)
+ return sd_varlink_error_invalid_parameter_name(link, "newValue");
+
+ return r;
+}
+
+static int vl_method_set_hostname(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "newValue", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, 0, SD_JSON_NULLABLE },
+ VARLINK_DISPATCH_POLKIT_FIELD,
+ {}
+ };
+
+ Context *c = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link);
+ assert(parameters);
+
+ _cleanup_free_ char *value = NULL;
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &value);
+ if (r != 0)
+ return r;
+
+ const char *name = empty_to_null(value);
+
+ /* We always go through with the procedure below without comparing to the current hostname, because
+ * we might want to adjust hostname source information even if the actual hostname is unchanged. */
+
+ _cleanup_free_ char *substituted = NULL;
+ r = vl_validate_and_substitute_hostname(link, name, &substituted);
+ if (r < 0)
+ return r;
+
+ name = substituted;
+
+ r = varlink_verify_polkit_async(
+ link,
+ c->bus,
+ "org.freedesktop.hostname1.set-hostname",
+ /* details= */ NULL,
+ &c->polkit_registry);
+ if (r <= 0)
+ return r;
+
+ context_read_etc_hostname(c);
+
+ r = context_update_kernel_hostname(c, name);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ (void) sd_bus_emit_properties_changed(c->bus,
+ "/org/freedesktop/hostname1", "org.freedesktop.hostname1",
+ "Hostname", "HostnameSource", NULL);
+
+ return sd_varlink_reply(link, NULL);
+}
+
+static int vl_method_set_static_hostname(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "newValue", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, 0, SD_JSON_NULLABLE },
+ VARLINK_DISPATCH_POLKIT_FIELD,
+ {}
+ };
+
+ Context *c = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link);
+ assert(parameters);
+
+ _cleanup_free_ char *value = NULL;
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &value);
+ if (r != 0)
+ return r;
+
+ const char *name = empty_to_null(value);
+
+ context_read_etc_hostname(c);
+
+ if (streq_ptr(name, c->data[PROP_STATIC_HOSTNAME]))
+ return sd_varlink_reply(link, NULL);
+
+ _cleanup_free_ char *substituted = NULL;
+ r = vl_validate_and_substitute_hostname(link, name, &substituted);
+ if (r < 0)
+ return r;
+
+ r = varlink_verify_polkit_async(
+ link,
+ c->bus,
+ "org.freedesktop.hostname1.set-static-hostname",
+ /* details= */ NULL,
+ &c->polkit_registry);
+ if (r <= 0)
+ return r;
+
+ r = free_and_strdup_warn(&c->data[PROP_STATIC_HOSTNAME], name);
+ if (r < 0)
+ return r;
+
+ free_and_replace(c->data[PROP_STATIC_HOSTNAME_SUBSTITUTED_WILDCARDS], substituted);
+
+ r = context_write_data_static_hostname(c);
+ if (r < 0)
+ return r;
+
+ r = context_update_kernel_hostname(c, /* transient_hostname= */ NULL);
+ if (r < 0)
+ return r;
+
+ (void) sd_bus_emit_properties_changed(c->bus,
+ "/org/freedesktop/hostname1", "org.freedesktop.hostname1",
+ "StaticHostname", "Hostname", "HostnameSource", NULL);
+
+ return sd_varlink_reply(link, NULL);
+}
+
+static int vl_set_machine_info(sd_varlink *link, sd_json_variant *parameters, void *userdata, HostProperty prop) {
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "newValue", SD_JSON_VARIANT_STRING, sd_json_dispatch_string, 0, SD_JSON_NULLABLE },
+ VARLINK_DISPATCH_POLKIT_FIELD,
+ {}
+ };
+
+ Context *c = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link);
+ assert(parameters);
+
+ _cleanup_free_ char *value = NULL;
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &value);
+ if (r != 0)
+ return r;
+
+ const char *name = empty_to_null(value),
+ *polkit_action = "org.freedesktop.hostname1.set-machine-info",
+ *bus_property, *human_name;
+
+ switch (prop) {
+
+ case PROP_PRETTY_HOSTNAME:
+ if (name && !pretty_hostname_is_valid(name))
+ return sd_varlink_error_invalid_parameter_name(link, "newValue");
+ bus_property = "PrettyHostname";
+ human_name = "pretty hostname";
+ /* Since the pretty hostname should always be changed at the same time as the static one, use
+ * the same policy action for both... */
+ polkit_action = "org.freedesktop.hostname1.set-static-hostname";
+ break;
+
+ case PROP_ICON_NAME:
+ /* The icon name might ultimately be used as file name, so better be safe than sorry. */
+ if (name && !icon_name_is_valid(name))
+ return sd_varlink_error_invalid_parameter_name(link, "newValue");
+ bus_property = "IconName";
+ human_name = "icon name";
+ break;
+
+ case PROP_CHASSIS:
+ if (name && !chassis_is_valid(name))
+ return sd_varlink_error_invalid_parameter_name(link, "newValue");
+ bus_property = "Chassis";
+ human_name = "chassis";
+ break;
+
+ case PROP_DEPLOYMENT:
+ if (name && !deployment_is_valid(name))
+ return sd_varlink_error_invalid_parameter_name(link, "newValue");
+ bus_property = "Deployment";
+ human_name = "deployment";
+ break;
+
+ case PROP_LOCATION:
+ if (name && !location_is_valid(name))
+ return sd_varlink_error_invalid_parameter_name(link, "newValue");
+ bus_property = "Location";
+ human_name = "location";
+ break;
+
+ default:
+ assert_not_reached();
+ }
+
+ context_read_machine_info(c);
+
+ if (streq_ptr(name, c->data[prop]))
+ return sd_varlink_reply(link, NULL);
+
+ r = varlink_verify_polkit_async(
+ link,
+ c->bus,
+ polkit_action,
+ /* details= */ NULL,
+ &c->polkit_registry);
+ if (r <= 0)
+ return r;
+
+ r = free_and_strdup_warn(&c->data[prop], name);
+ if (r < 0)
+ return r;
+
+ r = context_write_data_machine_info(c);
+ if (r < 0)
+ return r;
+
+ log_info("Changed %s to '%s'", human_name, strna(c->data[prop]));
+
+ (void) sd_bus_emit_properties_changed(
+ c->bus,
+ "/org/freedesktop/hostname1",
+ "org.freedesktop.hostname1",
+ bus_property,
+ NULL);
+
+ return sd_varlink_reply(link, NULL);
+}
+
+static int vl_method_set_pretty_hostname(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ return vl_set_machine_info(link, parameters, userdata, PROP_PRETTY_HOSTNAME);
+}
+
+static int vl_method_set_icon_name(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ return vl_set_machine_info(link, parameters, userdata, PROP_ICON_NAME);
+}
+
+static int vl_method_set_chassis(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ return vl_set_machine_info(link, parameters, userdata, PROP_CHASSIS);
+}
+
+static int vl_method_set_deployment(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ return vl_set_machine_info(link, parameters, userdata, PROP_DEPLOYMENT);
+}
+
+static int vl_method_set_location(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ return vl_set_machine_info(link, parameters, userdata, PROP_LOCATION);
+}
+
+typedef struct SetTagsParameters {
+ char **set;
+ char **add;
+ char **remove;
+} SetTagsParameters;
+
+static void set_tags_parameters_done(SetTagsParameters *p) {
+ assert(p);
+
+ strv_free(p->set);
+ strv_free(p->add);
+ strv_free(p->remove);
+}
+
+static int vl_method_set_tags(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
+ static const sd_json_dispatch_field dispatch_table[] = {
+ { "set", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(SetTagsParameters, set), SD_JSON_NULLABLE },
+ { "add", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(SetTagsParameters, add), SD_JSON_NULLABLE },
+ { "remove", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_strv, offsetof(SetTagsParameters, remove), SD_JSON_NULLABLE },
+ VARLINK_DISPATCH_POLKIT_FIELD,
+ {}
+ };
+
+ Context *c = ASSERT_PTR(userdata);
+ int r;
+
+ assert(link);
+ assert(parameters);
+
+ _cleanup_(set_tags_parameters_done) SetTagsParameters p = {};
+ r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
+ if (r != 0)
+ return r;
+
+ /* Both an absent 'set' field and an explicit null map to a NULL strv after dispatching, but only the
+ * former should keep the current tags — an explicit (possibly empty) 'set' resets the list. Hence
+ * check for the field's presence separately. */
+ bool reset = sd_json_variant_by_key(parameters, "set");
+
+ if (reset && !machine_tag_list_is_valid(p.set))
+ return sd_varlink_error_invalid_parameter_name(link, "set");
+ if (!machine_tag_list_is_valid(p.add))
+ return sd_varlink_error_invalid_parameter_name(link, "add");
+ if (!machine_tag_list_is_valid(p.remove))
+ return sd_varlink_error_invalid_parameter_name(link, "remove");
+
+ context_read_machine_info(c);
+
+ _cleanup_strv_free_ char **current = NULL;
+ r = machine_tags_from_string(c->data[PROP_TAGS], /* graceful= */ true, ¤t);
+ if (r < 0)
+ return r;
+
+ /* Use either the explicitly specified tag list or the current one as the basis, then apply the
+ * additions and removals on top. */
+ _cleanup_strv_free_ char **tags = NULL;
+ r = machine_tags_add_remove(reset ? p.set : current, p.add, p.remove, &tags);
+ if (r == -E2BIG)
+ return sd_varlink_error_invalid_parameter_name(link, "add");
+ if (r < 0)
+ return r;
+
+ if (strv_equal(tags, current))
+ return sd_varlink_reply(link, NULL);
+
+ r = varlink_verify_polkit_async(
+ link,
+ c->bus,
+ "org.freedesktop.hostname1.set-machine-info",
+ /* details= */ NULL,
+ &c->polkit_registry);
+ if (r <= 0)
+ return r;
+
+ r = context_store_tags(c, tags);
+ if (r < 0)
+ return r;
+
+ return sd_varlink_reply(link, NULL);
+}
+
static int connect_varlink(Context *c) {
int r;
@@ -2132,10 +2610,18 @@ static int connect_varlink(Context *c) {
r = sd_varlink_server_bind_method_many(
c->varlink_server,
- "io.systemd.Hostname.Describe", vl_method_describe,
- "io.systemd.service.Ping", varlink_method_ping,
- "io.systemd.service.SetLogLevel", varlink_method_set_log_level,
- "io.systemd.service.GetEnvironment", varlink_method_get_environment);
+ "io.systemd.Hostname.Describe", vl_method_describe,
+ "io.systemd.Hostname.SetHostname", vl_method_set_hostname,
+ "io.systemd.Hostname.SetStaticHostname", vl_method_set_static_hostname,
+ "io.systemd.Hostname.SetPrettyHostname", vl_method_set_pretty_hostname,
+ "io.systemd.Hostname.SetIconName", vl_method_set_icon_name,
+ "io.systemd.Hostname.SetChassis", vl_method_set_chassis,
+ "io.systemd.Hostname.SetDeployment", vl_method_set_deployment,
+ "io.systemd.Hostname.SetLocation", vl_method_set_location,
+ "io.systemd.Hostname.SetTags", vl_method_set_tags,
+ "io.systemd.service.Ping", varlink_method_ping,
+ "io.systemd.service.SetLogLevel", varlink_method_set_log_level,
+ "io.systemd.service.GetEnvironment", varlink_method_get_environment);
if (r < 0)
return log_error_errno(r, "Failed to bind Varlink method calls: %m");
@@ -2207,6 +2693,14 @@ static int run(int argc, char *argv[]) {
if (r < 0)
return log_error_errno(r, "Failed to install SIGINT/SIGTERM handlers: %m");
+ r = sd_event_add_signal(context.event, /* ret= */ NULL, (SIGRTMIN+18)|SD_EVENT_SIGNAL_PROCMASK, sigrtmin18_handler, /* userdata= */ NULL);
+ if (r < 0)
+ return log_error_errno(r, "Failed to install SIGRTMIN+18 handler: %m");
+
+ r = sd_event_add_memory_pressure(context.event, /* ret= */ NULL, /* callback= */ NULL, /* userdata= */ NULL);
+ if (r < 0)
+ log_debug_errno(r, "Failed to allocate memory pressure event source, ignoring: %m");
+
r = connect_bus(&context);
if (r < 0)
return r;
diff --git a/src/shared/varlink-io.systemd.Hostname.c b/src/shared/varlink-io.systemd.Hostname.c
index e06587039b..ca7b2b534b 100644
--- a/src/shared/varlink-io.systemd.Hostname.c
+++ b/src/shared/varlink-io.systemd.Hostname.c
@@ -6,45 +6,146 @@
static SD_VARLINK_DEFINE_METHOD(
Describe,
VARLINK_DEFINE_POLKIT_INPUT,
+ SD_VARLINK_FIELD_COMMENT("The current system hostname."),
SD_VARLINK_DEFINE_OUTPUT(Hostname, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The statically configured hostname, from /etc/hostname. Null if none is configured."),
SD_VARLINK_DEFINE_OUTPUT(StaticHostname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The pretty (free-form, UTF-8) hostname. Null if none is configured."),
SD_VARLINK_DEFINE_OUTPUT(PrettyHostname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The fallback hostname used when neither a static nor a transient hostname is set."),
SD_VARLINK_DEFINE_OUTPUT(DefaultHostname, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("Indicates where the current hostname originates from, one of 'static', 'transient' or 'default'."),
SD_VARLINK_DEFINE_OUTPUT(HostnameSource, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The name of an icon representing this system, following the XDG icon naming spec. Null if none is configured."),
SD_VARLINK_DEFINE_OUTPUT(IconName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The chassis type of this system, e.g. 'desktop', 'laptop', 'server', 'vm', … Null if unknown."),
SD_VARLINK_DEFINE_OUTPUT(Chassis, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("An unique identifier of the system chassis."),
SD_VARLINK_DEFINE_OUTPUT(ChassisAssetTag, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The deployment environment of this system, e.g. 'production' or 'staging'. Null if none is configured."),
SD_VARLINK_DEFINE_OUTPUT(Deployment, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("A human-readable description of the physical location of this system. Null if none is configured."),
SD_VARLINK_DEFINE_OUTPUT(Location, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The operating system kernel name, as reported by uname(2), e.g. 'Linux'."),
SD_VARLINK_DEFINE_OUTPUT(KernelName, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The kernel release string, as reported by uname(2)."),
SD_VARLINK_DEFINE_OUTPUT(KernelRelease, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The kernel version string, as reported by uname(2)."),
SD_VARLINK_DEFINE_OUTPUT(KernelVersion, SD_VARLINK_STRING, 0),
SD_VARLINK_FIELD_COMMENT("'Pretty' name of the OS. This is the primary OS identifier that is suitable for presentation to the user. Typically includes a version too, but doesn't have to."),
SD_VARLINK_DEFINE_OUTPUT(OperatingSystemPrettyName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("'Fancy' name of the OS; may contain non-ASCII Unicode chars, as well as basic ANSI sequences. This is similar to 'OperatingSystemPrettyName', but is preferably used on terminals that support ANSI sequences and full Unicode."),
SD_VARLINK_DEFINE_OUTPUT(OperatingSystemFancyName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The CPE name of the OS, from the CPE_NAME= field of os-release."),
SD_VARLINK_DEFINE_OUTPUT(OperatingSystemCPEName, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The home URL of the OS, from the HOME_URL= field of os-release."),
SD_VARLINK_DEFINE_OUTPUT(OperatingSystemHomeURL, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The end-of-support time of the OS, in µs since the UNIX epoch. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(OperatingSystemSupportEnd, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The full contents of os-release, as an array of 'KEY=VALUE' strings."),
SD_VARLINK_DEFINE_OUTPUT(OperatingSystemReleaseData, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
+ SD_VARLINK_FIELD_COMMENT("The OS image identifier, from the IMAGE_ID= field of os-release."),
SD_VARLINK_DEFINE_OUTPUT(OperatingSystemImageID, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The OS image version, from the IMAGE_VERSION= field of os-release."),
SD_VARLINK_DEFINE_OUTPUT(OperatingSystemImageVersion, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The full contents of /etc/machine-info, as an array of 'KEY=VALUE' strings."),
SD_VARLINK_DEFINE_OUTPUT(MachineInformationData, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
+ SD_VARLINK_FIELD_COMMENT("The hardware vendor of this system. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(HardwareVendor, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The hardware model of this system. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(HardwareModel, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The hardware serial number of this system. Reading it requires privileges. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(HardwareSerial, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The hardware SKU (stock keeping unit) of this system. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(HardwareSKU, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The hardware version of this system. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(HardwareVersion, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The firmware version of this system. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(FirmwareVersion, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The firmware vendor of this system. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(FirmwareVendor, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The firmware build date, in µs since the UNIX epoch. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(FirmwareDate, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The 128-bit machine ID of this system, formatted as a hexadecimal string."),
SD_VARLINK_DEFINE_OUTPUT(MachineID, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The 128-bit boot ID of the current boot, formatted as a hexadecimal string."),
SD_VARLINK_DEFINE_OUTPUT(BootID, SD_VARLINK_STRING, 0),
+ SD_VARLINK_FIELD_COMMENT("The DMI product UUID of this system. Reading it requires privileges. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(ProductUUID, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ SD_VARLINK_FIELD_COMMENT("The AF_VSOCK context ID (CID) of this system. Null if not known."),
SD_VARLINK_DEFINE_OUTPUT(VSockCID, SD_VARLINK_INT, SD_VARLINK_NULLABLE));
+static SD_VARLINK_DEFINE_METHOD(
+ SetHostname,
+ SD_VARLINK_FIELD_COMMENT("The transient (runtime) hostname to set. If null the transient hostname is reset, so that the static hostname is used again."),
+ SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ VARLINK_DEFINE_POLKIT_INPUT);
+
+static SD_VARLINK_DEFINE_METHOD(
+ SetStaticHostname,
+ SD_VARLINK_FIELD_COMMENT("The static hostname to set, stored in /etc/hostname. If null the static hostname is removed."),
+ SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ VARLINK_DEFINE_POLKIT_INPUT);
+
+static SD_VARLINK_DEFINE_METHOD(
+ SetPrettyHostname,
+ SD_VARLINK_FIELD_COMMENT("The pretty (free-form, UTF-8) hostname to set. If null the pretty hostname is removed."),
+ SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ VARLINK_DEFINE_POLKIT_INPUT);
+
+static SD_VARLINK_DEFINE_METHOD(
+ SetIconName,
+ SD_VARLINK_FIELD_COMMENT("The icon name to set. If null the icon name is removed."),
+ SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ VARLINK_DEFINE_POLKIT_INPUT);
+
+static SD_VARLINK_DEFINE_METHOD(
+ SetChassis,
+ SD_VARLINK_FIELD_COMMENT("The chassis type to set. If null the chassis type is removed."),
+ SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ VARLINK_DEFINE_POLKIT_INPUT);
+
+static SD_VARLINK_DEFINE_METHOD(
+ SetDeployment,
+ SD_VARLINK_FIELD_COMMENT("The deployment environment to set. If null the deployment is removed."),
+ SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ VARLINK_DEFINE_POLKIT_INPUT);
+
+static SD_VARLINK_DEFINE_METHOD(
+ SetLocation,
+ SD_VARLINK_FIELD_COMMENT("The physical location to set. If null the location is removed."),
+ SD_VARLINK_DEFINE_INPUT(newValue, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
+ VARLINK_DEFINE_POLKIT_INPUT);
+
+static SD_VARLINK_DEFINE_METHOD(
+ SetTags,
+ SD_VARLINK_FIELD_COMMENT("If specified, the machine tag list is first reset to exactly these tags, before the 'add' and 'remove' fields are applied. If null, the current tag list is used as basis instead."),
+ SD_VARLINK_DEFINE_INPUT(set, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
+ SD_VARLINK_FIELD_COMMENT("Machine tags to add to the list."),
+ SD_VARLINK_DEFINE_INPUT(add, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
+ SD_VARLINK_FIELD_COMMENT("Machine tags to remove from the list."),
+ SD_VARLINK_DEFINE_INPUT(remove, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_ARRAY),
+ VARLINK_DEFINE_POLKIT_INPUT);
+
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_Hostname,
"io.systemd.Hostname",
- &vl_method_Describe);
+ SD_VARLINK_INTERFACE_COMMENT("APIs for querying and changing the system hostname and related machine metadata."),
+ SD_VARLINK_SYMBOL_COMMENT("Returns hostname, kernel, OS, hardware, firmware and other machine metadata in one call."),
+ &vl_method_Describe,
+ SD_VARLINK_SYMBOL_COMMENT("Sets the transient (runtime) hostname."),
+ &vl_method_SetHostname,
+ SD_VARLINK_SYMBOL_COMMENT("Sets the static hostname, stored in /etc/hostname."),
+ &vl_method_SetStaticHostname,
+ SD_VARLINK_SYMBOL_COMMENT("Sets the pretty (free-form, UTF-8) hostname."),
+ &vl_method_SetPrettyHostname,
+ SD_VARLINK_SYMBOL_COMMENT("Sets the icon name representing this system."),
+ &vl_method_SetIconName,
+ SD_VARLINK_SYMBOL_COMMENT("Sets the chassis type of this system."),
+ &vl_method_SetChassis,
+ SD_VARLINK_SYMBOL_COMMENT("Sets the deployment environment of this system."),
+ &vl_method_SetDeployment,
+ SD_VARLINK_SYMBOL_COMMENT("Sets the physical location of this system."),
+ &vl_method_SetLocation,
+ SD_VARLINK_SYMBOL_COMMENT("Edits the machine tag list: optionally resets it to 'set', then adds 'add' and removes 'remove'."),
+ &vl_method_SetTags);
diff --git a/src/test/test-strv.c b/src/test/test-strv.c
index 283ae5c865..386c54f787 100644
--- a/src/test/test-strv.c
+++ b/src/test/test-strv.c
@@ -510,6 +510,26 @@ TEST(strv_overlap) {
assert_se(!strv_overlap((char **)input_table, (char**)input_table_unique));
}
+TEST(strv_remove_strv) {
+ _cleanup_strv_free_ char **l = NULL;
+
+ ASSERT_OK(strv_extend_strv(&l, STRV_MAKE("one", "two", "three", "four"), false));
+
+ ASSERT_PTR_EQ(strv_remove_strv(l, STRV_MAKE("two", "four")), l);
+ ASSERT_TRUE(strv_equal(l, STRV_MAKE("one", "three")));
+
+ /* Removing entries not present is a no-op. */
+ ASSERT_PTR_EQ(strv_remove_strv(l, STRV_MAKE("nope")), l);
+ ASSERT_TRUE(strv_equal(l, STRV_MAKE("one", "three")));
+
+ /* Empty removal list leaves the list untouched. */
+ ASSERT_PTR_EQ(strv_remove_strv(l, STRV_EMPTY), l);
+ ASSERT_TRUE(strv_equal(l, STRV_MAKE("one", "three")));
+
+ /* Removing from an empty/NULL list is fine. */
+ ASSERT_NULL(strv_remove_strv(NULL, STRV_MAKE("one")));
+}
+
TEST(strv_sort) {
const char* input_table[] = {
"durian",
diff --git a/test/units/TEST-71-HOSTNAME.sh b/test/units/TEST-71-HOSTNAME.sh
index 9bd743dcaa..369bed5f6e 100755
--- a/test/units/TEST-71-HOSTNAME.sh
+++ b/test/units/TEST-71-HOSTNAME.sh
@@ -388,6 +388,123 @@ EOF
assert_rc 1 busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s CHASSIS
}
+testcase_tags() {
+ if [[ -f /etc/machine-info ]]; then
+ cp /etc/machine-info /tmp/machine-info.bak
+ fi
+
+ trap restore_machine_info RETURN
+
+ # Start from a clean slate.
+ hostnamectl tags ""
+ assert_eq "$(hostnamectl tags)" ""
+
+ # Plain arguments replace the whole list; each argument may itself be a colon-separated list, and the
+ # result is deduplicated and sorted.
+ hostnamectl tags foo bar:baz
+ assert_eq "$(hostnamectl tags)" "bar:baz:foo"
+
+ # A leading '+' adds, a leading '-' removes, and the prefix applies to all (colon-separated) tags of
+ # the argument.
+ hostnamectl tags +quux
+ assert_eq "$(hostnamectl tags)" "bar:baz:foo:quux"
+ hostnamectl tags -- -foo
+ assert_eq "$(hostnamectl tags)" "bar:baz:quux"
+ hostnamectl tags -- +alpha:beta -bar:baz
+ assert_eq "$(hostnamectl tags)" "alpha:beta:quux"
+
+ # Adding an existing tag and removing a non-existing one are no-ops.
+ hostnamectl tags +alpha
+ assert_eq "$(hostnamectl tags)" "alpha:beta:quux"
+ hostnamectl tags -- -nonexistent
+ assert_eq "$(hostnamectl tags)" "alpha:beta:quux"
+
+ # Plain and prefixed arguments may not be mixed.
+ (! hostnamectl tags foo +bar)
+ (! hostnamectl tags +bar foo)
+ # Invalid tag characters are refused.
+ (! hostnamectl tags "in valid")
+ (! hostnamectl tags "+in/valid")
+
+ # The AddAndRemoveTags() D-Bus method directly.
+ hostnamectl tags ""
+ busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 \
+ AddAndRemoveTags asas 2 one two 0
+ assert_eq "$(hostnamectl tags)" "one:two"
+ busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 \
+ AddAndRemoveTags asas 1 three 1 one
+ assert_eq "$(hostnamectl tags)" "three:two"
+
+ # The SetTags() Varlink method: 'set' resets the list, then 'add'/'remove' are applied on top.
+ varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags \
+ '{"set":["x","y"],"add":["z"],"remove":["x"]}'
+ assert_eq "$(hostnamectl tags)" "y:z"
+ # An explicit empty 'set' clears the list first.
+ varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"set":[]}'
+ assert_eq "$(hostnamectl tags)" ""
+ # Without 'set' the current list is the basis.
+ varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"add":["aaa","bbb"]}'
+ assert_eq "$(hostnamectl tags)" "aaa:bbb"
+ varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"remove":["aaa"]}'
+ assert_eq "$(hostnamectl tags)" "bbb"
+ # Invalid tags are refused.
+ (! varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"add":["in valid"]}')
+
+ hostnamectl tags ""
+}
+
+testcase_varlink_setters() {
+ local bus=/run/systemd/io.systemd.Hostname
+
+ if [[ -f /etc/hostname ]]; then
+ cp /etc/hostname /tmp/hostname.bak
+ fi
+ if [[ -f /etc/machine-info ]]; then
+ cp /etc/machine-info /tmp/machine-info.bak
+ fi
+
+ trap 'restore_hostname; restore_machine_info' RETURN
+
+ # Static hostname.
+ varlinkctl call "$bus" io.systemd.Hostname.SetStaticHostname '{"newValue":"vl-static"}'
+ assert_eq "$(cat /etc/hostname)" "vl-static"
+ assert_in "Static hostname: vl-static" "$(hostnamectl)"
+
+ varlinkctl call "$bus" io.systemd.Hostname.SetStaticHostname '{"newValue":null}'
+ test ! -e /etc/hostname
+
+ # Transient hostname; a null value resets it back to the fallback
+ varlinkctl call "$bus" io.systemd.Hostname.SetHostname '{"newValue":"vl-transient"}'
+ assert_eq "$(hostname)" "vl-transient"
+ varlinkctl call "$bus" io.systemd.Hostname.SetHostname '{"newValue":null}'
+ assert_neq "$(hostname)" "vl-transient"
+
+ # Machine-info fields.
+ varlinkctl call "$bus" io.systemd.Hostname.SetPrettyHostname '{"newValue":"Pretty VL"}'
+ assert_eq "$(hostnamectl --json=short | jq --raw-output .PrettyHostname)" "Pretty VL"
+
+ varlinkctl call "$bus" io.systemd.Hostname.SetIconName '{"newValue":"computer-vl"}'
+ assert_eq "$(hostnamectl --json=short | jq --raw-output .IconName)" "computer-vl"
+
+ varlinkctl call "$bus" io.systemd.Hostname.SetChassis '{"newValue":"tablet"}'
+ assert_eq "$(hostnamectl chassis)" "tablet"
+
+ varlinkctl call "$bus" io.systemd.Hostname.SetDeployment '{"newValue":"staging"}'
+ assert_eq "$(hostnamectl --json=short | jq --raw-output .Deployment)" "staging"
+
+ varlinkctl call "$bus" io.systemd.Hostname.SetLocation '{"newValue":"Rack 7"}'
+ assert_eq "$(hostnamectl --json=short | jq --raw-output .Location)" "Rack 7"
+
+ # A null value removes the field again.
+ varlinkctl call "$bus" io.systemd.Hostname.SetLocation '{"newValue":null}'
+ assert_eq "$(hostnamectl --json=short | jq --raw-output .Location)" "null"
+
+ # Invalid values are refused.
+ (! varlinkctl call "$bus" io.systemd.Hostname.SetChassis '{"newValue":"nonsense"}')
+ (! varlinkctl call "$bus" io.systemd.Hostname.SetIconName '{"newValue":"in/valid"}')
+ (! varlinkctl call "$bus" io.systemd.Hostname.SetStaticHostname '{"newValue":"invalid hostname"}')
+}
+
run_testcases
touch /testok
diff --git a/units/meson.build b/units/meson.build
index 11b73b9b1c..47c48bcabc 100644
--- a/units/meson.build
+++ b/units/meson.build
@@ -503,6 +503,10 @@ units = [
'file' : 'systemd-machine-id-commit.service',
'symlinks' : ['sysinit.target.wants/'],
},
+ {
+ 'file' : 'systemd-machine-tag@.service',
+ 'conditions' : ['ENABLE_HOSTNAMED'],
+ },
{
'file' : 'systemd-machined.service.in',
'conditions' : ['ENABLE_MACHINED'],
diff --git a/units/systemd-machine-tag@.service b/units/systemd-machine-tag@.service
new file mode 100644
index 0000000000..7fc578d7e8
--- /dev/null
+++ b/units/systemd-machine-tag@.service
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# This file is part of systemd.
+#
+# systemd is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+
+[Unit]
+Description=Add Machine Tag %i
+Documentation=man:systemd-machine-tag@.service(8)
+DefaultDependencies=no
+ConditionPathExists=!/etc/initrd-release
+After=local-fs.target systemd-hostnamed.socket
+Requisite=local-fs.target systemd-hostnamed.socket
+Before=shutdown.target
+Conflicts=shutdown.target
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=varlinkctl call /run/systemd/io.systemd.Hostname io.systemd.Hostname.SetTags '{"add":["%i"]}'