From ac722389a777adbed7722175365bbd933bbe6831 Mon Sep 17 00:00:00 2001 From: Yu Watanabe Date: Sun, 2 Feb 2025 12:07:48 +0900 Subject: [PATCH] udevadm-test: allow to dump result in json format This adds --json=MODE option for 'udevadm test' command. When specified, all messages, except for the final result, will be written to stderr, and the final result is shown in JSON format to stdout. It may be useful for parsing the test result. --- man/udevadm.xml | 2 + shell-completion/bash/udevadm | 6 +- shell-completion/zsh/_udevadm | 1 + src/udev/udev-dump.c | 272 +++++++++++++++++++++++++++++++++- src/udev/udev-dump.h | 4 +- src/udev/udev-rules.c | 2 +- src/udev/udevadm-test.c | 30 +++- test/units/TEST-17-UDEV.10.sh | 4 + 8 files changed, 309 insertions(+), 12 deletions(-) diff --git a/man/udevadm.xml b/man/udevadm.xml index 80184608f9..c9e58e9cf5 100644 --- a/man/udevadm.xml +++ b/man/udevadm.xml @@ -930,6 +930,8 @@ + + diff --git a/shell-completion/bash/udevadm b/shell-completion/bash/udevadm index e9d11d32a3..34b24eccb8 100644 --- a/shell-completion/bash/udevadm +++ b/shell-completion/bash/udevadm @@ -99,7 +99,7 @@ _udevadm() { [MONITOR_STANDALONE]='-k --kernel -u --udev -p --property' [MONITOR_ARG]='-s --subsystem-match -t --tag-match' [TEST_STANDALONE]='-v --verbose' - [TEST_ARG]='-a --action -N --resolve-names -D --extra-rules-dir' + [TEST_ARG]='-a --action -N --resolve-names -D --extra-rules-dir --json' [TEST_BUILTIN]='-a --action' [VERIFY_STANDALONE]='--no-summary --no-style' [VERIFY_ARG]='-N --resolve-names --root' @@ -263,6 +263,10 @@ _udevadm() { -D|--extra-rules-dir) comps='' compopt -o dirnames + ;; + --json) + comps=$( udevadm test --json help ) + ;; esac elif [[ $cur = -* ]]; then comps="${OPTS[COMMON]} ${OPTS[TEST_ARG]} ${OPTS[TEST_STANDALONE]}" diff --git a/shell-completion/zsh/_udevadm b/shell-completion/zsh/_udevadm index ac112e751f..71f75a549c 100644 --- a/shell-completion/zsh/_udevadm +++ b/shell-completion/zsh/_udevadm @@ -92,6 +92,7 @@ _udevadm_test(){ '--subsystem=[The subsystem string.]' \ '(-D --extra-rules-dir=)'{-D,--extra-rules-dir=}'[Also load rules from the directory.]' \ '(-v --verbose)'{-v,--verbose}'[Show verbose logs.]' \ + '--json=[Generate JSON output]:MODE:(pretty short off)' \ '*::devpath:_files -P /sys/ -W /sys' } diff --git a/src/udev/udev-dump.c b/src/udev/udev-dump.c index b13779e5f7..d832989805 100644 --- a/src/udev/udev-dump.c +++ b/src/udev/udev-dump.c @@ -6,6 +6,8 @@ #include "devnum-util.h" #include "format-util.h" #include "fs-util.h" +#include "json-util.h" +#include "parse-util.h" #include "udev-builtin.h" #include "udev-dump.h" #include "udev-event.h" @@ -29,7 +31,273 @@ void event_cache_written_sysctl(UdevEvent *event, const char *attr, const char * event_cache_written_value(&event->written_sysctls, attr, value); } -void dump_event(UdevEvent *event, FILE *f) { +static int dump_event_json(UdevEvent *event, sd_json_format_flags_t flags, FILE *f) { + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + const char *str; + int r; + + if (sd_device_get_devpath(dev, &str) >= 0) { + r = sd_json_variant_set_field_string(&v, "path", str); + if (r < 0) + return r; + } + + if (sd_device_get_sysname(dev, &str) >= 0) { + r = sd_json_variant_set_field_string(&v, "name", str); + if (r < 0) + return r; + } + + unsigned sysnum; + if (device_get_sysnum_unsigned(dev, &sysnum) >= 0) { + r = sd_json_variant_set_field_unsigned(&v, "number", sysnum); + if (r < 0) + return r; + } + + if (sd_device_get_device_id(dev, &str) >= 0) { + r = sd_json_variant_set_field_string(&v, "id", str); + if (r < 0) + return r; + } + + const char *subsys = NULL; + if (sd_device_get_subsystem(dev, &subsys) >= 0) { + r = sd_json_variant_set_field_string(&v, "subsystem", subsys); + if (r < 0) + return r; + } + + if (sd_device_get_driver_subsystem(dev, &str) >= 0) { + r = sd_json_variant_set_field_string(&v, "driverSubsystem", str); + if (r < 0) + return r; + } + + if (sd_device_get_devtype(dev, &str) >= 0) { + r = sd_json_variant_set_field_string(&v, "type", str); + if (r < 0) + return r; + } + + if (sd_device_get_driver(dev, &str) >= 0) { + r = sd_json_variant_set_field_string(&v, "driver", str); + if (r < 0) + return r; + } + + if (sd_device_get_devname(dev, &str) >= 0) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *node = NULL; + + r = sd_json_variant_set_field_string(&node, "path", str); + if (r < 0) + return r; + + r = sd_json_variant_set_field_string(&node, "type", streq_ptr(subsys, "block") ? "block" : "char"); + if (r < 0) + return r; + + dev_t devnum; + if (sd_device_get_devnum(dev, &devnum) >= 0) { + r = sd_json_variant_set_fieldb(&node, "rdev", JSON_BUILD_DEVNUM(devnum)); + if (r < 0) + return r; + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *owner = NULL; + + uid_t uid = event->uid; + if (!uid_is_valid(uid)) + (void) device_get_devnode_uid(dev, &uid); + if (uid_is_valid(uid)) { + _cleanup_free_ char *user = uid_to_name(uid); + if (!user) + return -ENOMEM; + + r = sd_json_variant_set_field_unsigned(&owner, "uid", uid); + if (r < 0) + return r; + + r = sd_json_variant_set_field_string(&owner, "userName", user); + if (r < 0) + return r; + } + + gid_t gid = event->gid; + if (!gid_is_valid(gid)) + (void) device_get_devnode_gid(dev, &gid); + if (gid_is_valid(gid)) { + _cleanup_free_ char *group = gid_to_name(gid); + if (!group) + return -ENOMEM; + + r = sd_json_variant_set_field_unsigned(&owner, "gid", gid); + if (r < 0) + return r; + + r = sd_json_variant_set_field_string(&owner, "groupName", group); + if (r < 0) + return r; + } + + r = json_variant_set_field_non_null(&node, "owner", owner); + if (r < 0) + return r; + + mode_t mode = event->mode; + if (mode == MODE_INVALID) + (void) device_get_devnode_mode(dev, &mode); + if (mode != MODE_INVALID) { + char mode_str[STRLEN("0755")+1]; + xsprintf(mode_str, "%04o", mode & ~S_IFMT); + + r = sd_json_variant_set_field_string(&node, "mode", mode_str); + if (r < 0) + return r; + } + + _cleanup_strv_free_ char **links = NULL; + FOREACH_DEVICE_DEVLINK(dev, devlink) { + r = strv_extend(&links, devlink); + if (r < 0) + return r; + } + + if (!strv_isempty(links)) { + int prio = 0; + (void) device_get_devlink_priority(dev, &prio); + + r = sd_json_variant_set_field_integer(&node, "symlinkPriority", prio); + if (r < 0) + return r; + + r = sd_json_variant_set_field_strv(&node, "symlinks", strv_sort(links)); + if (r < 0) + return r; + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *labels = NULL; + const char *name, *label; + ORDERED_HASHMAP_FOREACH_KEY(label, name, event->seclabel_list) { + r = sd_json_variant_append_arraybo( + &labels, + SD_JSON_BUILD_PAIR_STRING("name", name), + SD_JSON_BUILD_PAIR_STRING("label", label)); + if (r < 0) + return r; + } + + r = json_variant_set_field_non_null(&node, "securityLabels", labels); + if (r < 0) + return r; + + r = sd_json_variant_set_field_boolean(&node, "inotifyWatch", event->inotify_watch); + if (r < 0) + return r; + + r = json_variant_set_field_non_null(&v, "node", node); + if (r < 0) + return r; + } + + int ifindex; + if (sd_device_get_ifindex(dev, &ifindex) >= 0) { + _cleanup_(sd_json_variant_unrefp) sd_json_variant *netif = NULL; + + r = sd_json_variant_set_field_integer(&netif, "index", ifindex); + if (r < 0) + return r; + + if (!isempty(event->name)) { + r = sd_json_variant_set_field_string(&netif, "name", event->name); + if (r < 0) + return r; + } + + if (!strv_isempty(event->altnames)) { + r = sd_json_variant_set_field_strv(&netif, "alternativeNames", strv_sort(event->altnames)); + if (r < 0) + return r; + } + + r = json_variant_set_field_non_null(&v, "networkInterface", netif); + if (r < 0) + return r; + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *sysattrs = NULL; + const char *key, *value; + HASHMAP_FOREACH_KEY(value, key, event->written_sysattrs) { + r = sd_json_variant_append_arraybo( + &sysattrs, + SD_JSON_BUILD_PAIR_STRING("path", key), + SD_JSON_BUILD_PAIR_STRING("value", value)); + if (r < 0) + return r; + } + + r = json_variant_set_field_non_null(&v, "sysfsAttributes", sysattrs); + if (r < 0) + return r; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *sysctls = NULL; + HASHMAP_FOREACH_KEY(value, key, event->written_sysctls) { + r = sd_json_variant_append_arraybo( + &sysctls, + SD_JSON_BUILD_PAIR_STRING("path", key), + SD_JSON_BUILD_PAIR_STRING("value", value)); + if (r < 0) + return r; + } + + r = json_variant_set_field_non_null(&v, "sysctl", sysctls); + if (r < 0) + return r; + + _cleanup_strv_free_ char **tags = NULL; + FOREACH_DEVICE_TAG(dev, tag) { + r = strv_extend(&tags, tag); + if (r < 0) + return r; + } + + if (!strv_isempty(tags)) { + r = sd_json_variant_set_field_strv(&v, "tags", strv_sort(tags)); + if (r < 0) + return r; + } + + char **properties; + if (device_get_properties_strv(dev, &properties) >= 0 && !strv_isempty(properties)) { + r = sd_json_variant_set_field_strv(&v, "properties", strv_sort(properties)); + if (r < 0) + return r; + } + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *commands = NULL; + void *val; + const char *command; + ORDERED_HASHMAP_FOREACH_KEY(val, command, event->run_list) { + r = sd_json_variant_append_arraybo( + &commands, + SD_JSON_BUILD_PAIR_STRING("type", PTR_TO_UDEV_BUILTIN_CMD(val) >= 0 ? "builtin" : "program"), + SD_JSON_BUILD_PAIR_STRING("command", command)); + if (r < 0) + return r; + } + + r = json_variant_set_field_non_null(&v, "queuedCommands", commands); + if (r < 0) + return r; + + return sd_json_variant_dump(v, flags, f, /* prefix = */ NULL); +} + +int dump_event(UdevEvent *event, sd_json_format_flags_t flags, FILE *f) { + if (sd_json_format_enabled(flags)) + return dump_event_json(event, flags, f); + sd_device *dev = ASSERT_PTR(ASSERT_PTR(event)->dev); const char *subsys = NULL, *str; @@ -168,4 +436,6 @@ void dump_event(UdevEvent *event, FILE *f) { fprintf(f, " RUN{program} : %s\n", command); } } + + return 0; } diff --git a/src/udev/udev-dump.h b/src/udev/udev-dump.h index 514f8267a7..f64499b653 100644 --- a/src/udev/udev-dump.h +++ b/src/udev/udev-dump.h @@ -3,8 +3,10 @@ #include +#include "sd-json.h" + typedef struct UdevEvent UdevEvent; void event_cache_written_sysattr(UdevEvent *event, const char *attr, const char *value); void event_cache_written_sysctl(UdevEvent *event, const char *attr, const char *value); -void dump_event(UdevEvent *event, FILE *f); +int dump_event(UdevEvent *event, sd_json_format_flags_t flags, FILE *f); diff --git a/src/udev/udev-rules.c b/src/udev/udev-rules.c index fc1af5d849..32a4461deb 100644 --- a/src/udev/udev-rules.c +++ b/src/udev/udev-rules.c @@ -2591,7 +2591,7 @@ static int udev_rule_apply_token_to_event( if (!f) return log_oom(); - dump_event(event, f); + (void) dump_event(event, SD_JSON_FORMAT_OFF, f); _cleanup_free_ char *buf = NULL; r = memstream_finalize(&m, &buf, NULL); diff --git a/src/udev/udevadm-test.c b/src/udev/udevadm-test.c index df22d9717b..40031e2119 100644 --- a/src/udev/udevadm-test.c +++ b/src/udev/udevadm-test.c @@ -25,6 +25,7 @@ static ResolveNameTiming arg_resolve_name_timing = RESOLVE_NAME_EARLY; static const char *arg_syspath = NULL; static char **arg_extra_rules_dir = NULL; static bool arg_verbose = false; +static sd_json_format_flags_t arg_json_format_flags = SD_JSON_FORMAT_OFF; STATIC_DESTRUCTOR_REGISTER(arg_extra_rules_dir, strv_freep); @@ -37,20 +38,26 @@ static int help(void) { " -a --action=ACTION|help Set action string\n" " -N --resolve-names=early|late|never When to resolve names\n" " -D --extra-rules-dir=DIR Also load rules from the directory\n" - " -v --verbose Show verbose logs\n", + " -v --verbose Show verbose logs\n" + " --json=pretty|short|off Generate JSON output\n", program_invocation_short_name); return 0; } static int parse_argv(int argc, char *argv[]) { + enum { + ARG_JSON = 0x100, + }; + static const struct option options[] = { - { "action", required_argument, NULL, 'a' }, - { "resolve-names", required_argument, NULL, 'N' }, - { "extra-rules-dir", required_argument, NULL, 'D' }, - { "verbose", no_argument, NULL, 'v' }, - { "version", no_argument, NULL, 'V' }, - { "help", no_argument, NULL, 'h' }, + { "action", required_argument, NULL, 'a' }, + { "resolve-names", required_argument, NULL, 'N' }, + { "extra-rules-dir", required_argument, NULL, 'D' }, + { "verbose", no_argument, NULL, 'v' }, + { "json", required_argument, NULL, ARG_JSON }, + { "version", no_argument, NULL, 'V' }, + { "help", no_argument, NULL, 'h' }, {} }; @@ -83,6 +90,11 @@ static int parse_argv(int argc, char *argv[]) { case 'v': arg_verbose = true; break; + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r <= 0) + return r; + break; case 'V': return print_version(); case 'h': @@ -168,7 +180,9 @@ int test_main(int argc, char *argv[], void *userdata) { log_info("Processing udev rules done."); maybe_insert_empty_line(); - dump_event(event, NULL); + r = dump_event(event, arg_json_format_flags, NULL); + if (r < 0) + return log_error_errno(r, "Failed to dump result: %m"); maybe_insert_empty_line(); return 0; diff --git a/test/units/TEST-17-UDEV.10.sh b/test/units/TEST-17-UDEV.10.sh index b8f5648a47..796d8dc5d2 100755 --- a/test/units/TEST-17-UDEV.10.sh +++ b/test/units/TEST-17-UDEV.10.sh @@ -153,6 +153,10 @@ udevadm test -N late /sys/class/net/$netdev udevadm test --resolve-names never /sys/class/net/$netdev (! udevadm test -N hello /sys/class/net/$netdev) udevadm test -v /sys/class/net/$netdev +udevadm test --json=off /sys/class/net/$netdev +udevadm test --json=pretty /sys/class/net/$netdev | jq . >/dev/null +udevadm test --json=short /sys/class/net/$netdev | jq . >/dev/null +udevadm test --json=help udevadm test -h # udevadm test-builtin path_id "$loopdev"