]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
hostname: add API for getting custom fields from machine-info
authorseaeunlee <seaeunlee@microsoft.com>
Fri, 6 Mar 2026 00:33:01 +0000 (00:33 +0000)
committerLuca Boccassi <luca.boccassi@gmail.com>
Wed, 18 Mar 2026 20:47:00 +0000 (20:47 +0000)
man/org.freedesktop.hostname1.xml
src/hostname/hostnamed.c
src/libsystemd/sd-bus/bus-common-errors.c
src/libsystemd/sd-bus/bus-common-errors.h
test/units/TEST-71-HOSTNAME.sh

index c70258459c2464cf884caa1a928ad128743d1dea..3d98b88ebc17791c75e8bf3fcdb3ee68ed20349c 100644 (file)
@@ -60,6 +60,8 @@ node /org/freedesktop/hostname1 {
                      out ay uuid);
       GetHardwareSerial(out s serial);
       Describe(out s json);
+      GetMachineInfo(in  s field,
+                     out s value);
     properties:
       readonly s Hostname = '...';
       readonly s StaticHostname = '...';
@@ -146,6 +148,8 @@ node /org/freedesktop/hostname1 {
 
     <variablelist class="dbus-method" generated="True" extra-ref="Describe()"/>
 
+    <variablelist class="dbus-method" generated="True" extra-ref="GetMachineInfo()"/>
+
     <variablelist class="dbus-property" generated="True" extra-ref="Hostname"/>
 
     <variablelist class="dbus-property" generated="True" extra-ref="StaticHostname"/>
@@ -377,6 +381,13 @@ node /org/freedesktop/hostname1 {
       access to unprivileged clients through the polkit framework.</para>
 
       <para><function>Describe()</function> returns a JSON representation of all properties in one.</para>
+
+      <para><function>GetMachineInfo()</function> returns the value of the given field from
+      <filename>/etc/machine-info</filename>. For well-known fields, this method reads values from the same
+      internal cache used by the corresponding D-Bus property getters, but returns only the raw
+      <filename>/etc/machine-info</filename> values (i.e. without property-level fallback logic such as
+      DMI/chassis-based detection). Custom (non-standard) fields are read directly from the file.
+      An error is returned if the field name is empty, invalid, or not set in the file.</para>
     </refsect2>
 
     <refsect2>
@@ -494,6 +505,7 @@ node /org/freedesktop/hostname1 {
       <varname>OperatingSystemImageVersion</varname>, <varname>HardwareSKU</varname>, and
       <varname>HardwareVersion</varname> were added in version 258.</para>
       <para><varname>OperatingSystemFancyName</varname> was added in version 260.</para>
+      <para><function>GetMachineInfo()</function> was added in version 261.</para>
     </refsect2>
   </refsect1>
 
index 04c72e8137bf1e74460aafbc48f7110541a20d9f..ce7187161834f4eb52de4b071a696ce2bca18439 100644 (file)
@@ -1652,6 +1652,65 @@ static int method_get_hardware_serial(sd_bus_message *m, void *userdata, sd_bus_
         return sd_bus_reply_method_return(m, "s", serial);
 }
 
+static int method_get_machine_info(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+        static const struct {
+                const char *name;
+                HostProperty prop;
+        } field_table[] = {
+                { "PRETTY_HOSTNAME",  PROP_PRETTY_HOSTNAME  },
+                { "ICON_NAME",        PROP_ICON_NAME        },
+                { "CHASSIS",          PROP_CHASSIS          },
+                { "DEPLOYMENT",       PROP_DEPLOYMENT       },
+                { "LOCATION",         PROP_LOCATION         },
+                { "HARDWARE_VENDOR",  PROP_HARDWARE_VENDOR  },
+                { "HARDWARE_MODEL",   PROP_HARDWARE_MODEL   },
+                { "HARDWARE_SKU",     PROP_HARDWARE_SKU     },
+                { "HARDWARE_VERSION", PROP_HARDWARE_VERSION },
+        };
+
+        Context *c = ASSERT_PTR(userdata);
+        const char *field;
+        int r;
+
+        assert(m);
+
+        r = sd_bus_message_read(m, "s", &field);
+        if (r < 0)
+                return r;
+
+        if (isempty(field))
+                return sd_bus_error_set(error, SD_BUS_ERROR_INVALID_ARGS, "Field name must not be empty.");
+
+        if (!env_name_is_valid(field))
+                return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid field name '%s'.", field);
+
+        FOREACH_ELEMENT(e, field_table)
+                if (streq(field, e->name)) {
+                        /* For fields that are also exposed as D-Bus properties, use the same Context cache as the
+                         * property getters. Note that this returns the raw /etc/machine-info value only: property-level
+                         * fallback logic (e.g. DMI/chassis-based synthesis) is not applied here. For custom/unknown
+                         * fields, fall back to reading the file directly. */
+                        context_read_machine_info(c);
+
+                        if (isempty(c->data[e->prop]))
+                                return sd_bus_error_setf(error, BUS_ERROR_FIELD_NOT_SET, "Field '%s' is not set or empty in /etc/machine-info.", field);
+
+                        return sd_bus_reply_method_return(m, "s", c->data[e->prop]);
+                }
+
+        _cleanup_free_ char *value = NULL;
+
+        r = parse_env_file(NULL, etc_machine_info(),
+                           field, &value);
+        if (r < 0 && r != -ENOENT)
+                return sd_bus_error_set_errnof(error, r, "Failed to read /etc/machine-info: %m");
+
+        if (isempty(value))
+                return sd_bus_error_setf(error, BUS_ERROR_FIELD_NOT_SET, "Field '%s' is not set or empty in /etc/machine-info.", field);
+
+        return sd_bus_reply_method_return(m, "s", value);
+}
+
 static int build_describe_response(Context *c, bool privileged, sd_json_variant **ret) {
         _cleanup_free_ char *hn = NULL, *dhn = NULL, *in = NULL,
                 *chassis = NULL, *vendor = NULL, *model = NULL, *serial = NULL, *firmware_version = NULL,
@@ -1883,6 +1942,11 @@ static const sd_bus_vtable hostname_vtable[] = {
                                 SD_BUS_RESULT("s", json),
                                 method_describe,
                                 SD_BUS_VTABLE_UNPRIVILEGED),
+        SD_BUS_METHOD_WITH_ARGS("GetMachineInfo",
+                                SD_BUS_ARGS("s", field),
+                                SD_BUS_RESULT("s", value),
+                                method_get_machine_info,
+                                SD_BUS_VTABLE_UNPRIVILEGED),
 
         SD_BUS_VTABLE_END,
 };
index f0fc09f8a919227337a976ae7eaab39b730fd200..6c2b0fd8814b12856d28d543a1a1171831d453b5 100644 (file)
@@ -111,6 +111,7 @@ BUS_ERROR_MAP_ELF_REGISTER const sd_bus_error_map bus_common_errors[] = {
 
         SD_BUS_ERROR_MAP(BUS_ERROR_NO_PRODUCT_UUID,              EOPNOTSUPP),
         SD_BUS_ERROR_MAP(BUS_ERROR_NO_HARDWARE_SERIAL,           EOPNOTSUPP),
+        SD_BUS_ERROR_MAP(BUS_ERROR_FIELD_NOT_SET,                ENODATA),
         SD_BUS_ERROR_MAP(BUS_ERROR_FILE_IS_PROTECTED,            EACCES),
         SD_BUS_ERROR_MAP(BUS_ERROR_READ_ONLY_FILESYSTEM,         EROFS),
 
index 36676e83db509b32208d6480b3fd829dc0ee95e5..b4788433bfce8f14c9531f7e35b889fae1968ea5 100644 (file)
 
 #define BUS_ERROR_NO_PRODUCT_UUID              "org.freedesktop.hostname1.NoProductUUID"
 #define BUS_ERROR_NO_HARDWARE_SERIAL           "org.freedesktop.hostname1.NoHardwareSerial"
+#define BUS_ERROR_FIELD_NOT_SET                "org.freedesktop.hostname1.FieldNotSet"
 #define BUS_ERROR_FILE_IS_PROTECTED            "org.freedesktop.hostname1.FileIsProtected"
 #define BUS_ERROR_READ_ONLY_FILESYSTEM         "org.freedesktop.hostname1.ReadOnlyFilesystem"
 
index ffb110be68832e6766cc9715855de561bb0df87b..9bd743dcaa6613f46c7a528722e308fa17fd4fa6 100755 (executable)
@@ -320,6 +320,74 @@ EOF
     assert_in "CHASSIS=watch" "$(cat /run/alternate-path/mymachine-info)"
 }
 
+testcase_get_machine_info() {
+    if [[ -f /etc/machine-info ]]; then
+        cp /etc/machine-info /tmp/machine-info.bak
+    fi
+
+    trap restore_machine_info RETURN
+
+    # Test all standard cached fields
+    cat >/etc/machine-info <<EOF
+PRETTY_HOSTNAME="Pretty Test"
+ICON_NAME=computer-laptop
+CHASSIS=laptop
+DEPLOYMENT=production
+LOCATION="Server Room 42"
+HARDWARE_VENDOR="Test Vendor"
+HARDWARE_MODEL="Test Model"
+HARDWARE_SKU="SKU-001"
+HARDWARE_VERSION="v1.0"
+TEST_CUSTOM_FIELD_1=still-here
+EOF
+
+    # Custom field should still work alongside standard fields
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s TEST_CUSTOM_FIELD_1 | cut -d'"' -f2)" "still-here"
+    # Should fail for a field that doesn't exist
+    assert_rc 1 busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s NONEXISTENT_FIELD
+    # Should fail for an empty field name
+    assert_rc 1 busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s ""
+    # Should fail for an invalid field name
+    assert_rc 1 busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s "invalid-name!"
+
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s PRETTY_HOSTNAME | cut -d'"' -f2)" "Pretty Test"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s ICON_NAME | cut -d'"' -f2)" "computer-laptop"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s CHASSIS | cut -d'"' -f2)" "laptop"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s DEPLOYMENT | cut -d'"' -f2)" "production"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s LOCATION | cut -d'"' -f2)" "Server Room 42"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s HARDWARE_VENDOR | cut -d'"' -f2)" "Test Vendor"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s HARDWARE_MODEL | cut -d'"' -f2)" "Test Model"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s HARDWARE_SKU | cut -d'"' -f2)" "SKU-001"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s HARDWARE_VERSION | cut -d'"' -f2)" "v1.0"
+
+    # Verify cache consistency for all standard fields against D-Bus properties
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s PRETTY_HOSTNAME | cut -d'"' -f2)" \
+              "$(busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 PrettyHostname | cut -d'"' -f2)"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s ICON_NAME | cut -d'"' -f2)" \
+              "$(busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 IconName | cut -d'"' -f2)"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s CHASSIS | cut -d'"' -f2)" \
+              "$(busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 Chassis | cut -d'"' -f2)"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s DEPLOYMENT | cut -d'"' -f2)" \
+              "$(busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 Deployment | cut -d'"' -f2)"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s LOCATION | cut -d'"' -f2)" \
+              "$(busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 Location | cut -d'"' -f2)"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s HARDWARE_VENDOR | cut -d'"' -f2)" \
+              "$(busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 HardwareVendor | cut -d'"' -f2)"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s HARDWARE_MODEL | cut -d'"' -f2)" \
+              "$(busctl get-property org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 HardwareModel | cut -d'"' -f2)"
+
+    # Test file modification is reflected (cache invalidation via stat)
+    cat >/etc/machine-info <<EOF
+PRETTY_HOSTNAME="Updated Host"
+TEST_CUSTOM_FIELD_1=updated-value
+EOF
+
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s PRETTY_HOSTNAME | cut -d'"' -f2)" "Updated Host"
+    assert_eq "$(busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s TEST_CUSTOM_FIELD_1 | cut -d'"' -f2)" "updated-value"
+    # Fields removed from the file should now fail
+    assert_rc 1 busctl call org.freedesktop.hostname1 /org/freedesktop/hostname1 org.freedesktop.hostname1 GetMachineInfo s CHASSIS
+}
+
 run_testcases
 
 touch /testok