Whamcloud - gitweb
LU-15743 utils: add --xattr option to lfs find 04/52804/11
authorThomas Bertschinger <bertschinger@lanl.gov>
Tue, 17 Oct 2023 20:32:33 +0000 (16:32 -0400)
committerOleg Drokin <green@whamcloud.com>
Fri, 23 Feb 2024 07:12:32 +0000 (07:12 +0000)
This adds a new "[!] --xattr" option to lfs find to enable listing
files that match a given extended attribute. The option takes an
argument in the form "NAME[=VALUE]" where NAME is a regular
expression for the attribute name and VALUE is an optional regular
expression to match the named attribute's value. If the option is
negated, only files that do not match the option are listed.

The provided regular expressions must match the entire name or value,
not just a substring. If only NAME is provided, files will match if
they have an extended attribute matching the name, regardless of the
attribute's contents. The option may be specified multiple times, and
files must match every provided argument in this case.

Signed-off-by: Thomas Bertschinger <bertschinger@lanl.gov>
Change-Id: I7b02e704b741ee30387a827dd5a25a20574cc3df
Reviewed-on: https://review.whamcloud.com/c/fs/lustre-release/+/52804
Reviewed-by: Oleg Drokin <green@whamcloud.com>
Reviewed-by: Jian Yu <yujian@whamcloud.com>
Reviewed-by: Alexandre Ioffe <aioffe@ddn.com>
Reviewed-by: Andreas Dilger <adilger@whamcloud.com>
Tested-by: jenkins <devops@whamcloud.com>
Tested-by: Maloo <maloo@whamcloud.com>
lustre/doc/lfs-find.1
lustre/include/lustre/lustreapi.h
lustre/tests/sanity.sh
lustre/utils/lfs.c
lustre/utils/liblustreapi.c

index bfd7f3d..4fb06c0 100644 (file)
@@ -40,6 +40,7 @@ lfs-find \- Lustre client utility to list files with specific attributes
 [[\fB!\fR] \fB--stripe-size|\fB-S\fR [\fB+-\fR]\fIn\fR[\fBKMG\fR]]
       [[\fB!\fR] \fB--type\fR|\fB-t\fR {\fBbcdflps\fR}]
 [[\fB!\fR] \fB--uid\fR|\fB-u\fR|\fB--user\fR|\fB-U  \fIUNAME\fR|\fIUID\fR]
+      [[\fB!\fR] \fB--xattr\fR \fINAME\fR[\fB=\fIVALUE\fR]]
 .SH DESCRIPTION
 .B lfs find
 is similar to the standard
@@ -459,6 +460,15 @@ File has specified numeric user ID.
 .TP
 .BR --user | -U
 File owned by specified user, numeric user ID also allowed.
+.TP
+\fB--xattr \fINAME\fR[\fB=\fIVALUE\fR]
+File has an extended attribute with name matching the regular expression
+.RB ( regex (7))
+\fINAME\fR, and optionally value matching the regular expression \fIVALUE\fR.
+The regular expressions must match the complete attribute names and values,
+and not just a substring.
+This option may be specified multiple times, and the file must match all
+provided arguments.
 .SH NOTES
 Specifying \fB!\fR before an option negates its meaning (\fIfiles
 NOT matching the parameter\fR). Using \fB+\fR before a numeric
@@ -515,6 +525,14 @@ Recursively list all out-of-sync mirrored files.
 Recursively list all but foreign files/dirs of
 .B symlink
 type.
+.TP
+.B $ lfs find -xattr user.job=202310.* /mnt/lustre
+Recursively list all files with the specified "user.job" extended attribute.
+.TP
+.B $ lfs find -xattr security.selinux ! -xattr security.selinux=.*httpd.* /var/www
+Recursively list all files in /var/www that have any SELinux extended attribute,
+but that do NOT have an SELinux extended attribute with a value containing
+"httpd".
 .SH BUGS
 The
 .B lfs find
@@ -535,4 +553,5 @@ command is part of the Lustre filesystem.
 .BR lfs-migrate (1),
 .BR lfs_migrate (1),
 .BR lustre (7),
+.BR regex (7),
 .BR xargs (1)
index f118dd5..c527b1a 100644 (file)
@@ -38,6 +38,7 @@
  */
 
 #include <glob.h>
+#include <regex.h>
 #include <stdarg.h>
 #include <stdint.h>
 #include <time.h>
@@ -247,6 +248,20 @@ enum lfs_find_perm {
        LFS_FIND_PERM_ALL   =  1,
 };
 
+/* struct for buffers and matching info for -xattr arguments to lfs find */
+struct xattr_match_info {
+       /* number of -xattr args specified (and size of xattr_regex_ arrays) */
+       int                     xattr_regex_count;
+       /* negation (!) can be specified separately for each -xattr arg */
+       bool                    *xattr_regex_exclude;
+       /* which regexes have already matched, when multiple specified */
+       bool                    *xattr_regex_matched;
+       regex_t                 **xattr_regex_name;
+       regex_t                 **xattr_regex_value;
+       char                    *xattr_name_buf;       /* [XATTR_LIST_MAX] */
+       char                    *xattr_value_buf;      /* [XATTR_SIZE_MAX] */
+};
+
 /*
  * new fields should be added to the end of this struct (unless filling a hole
  * such as in a bitfield), to preserve the ABI
@@ -428,6 +443,7 @@ struct find_param {
        nlink_t                  fp_nlink;
        __u64                    fp_attrs;
        __u64                    fp_neg_attrs;
+       struct xattr_match_info *fp_xattr_match_info;
 };
 
 int llapi_ostlist(char *path, struct find_param *param);
index 193c11c..56d7dbf 100755 (executable)
@@ -9064,6 +9064,77 @@ test_56ef() {
 }
 run_test 56ef "lfs find with multiple paths"
 
+test_56eg() {
+       local dir=$DIR/$tdir
+       local found
+
+       which setfattr > /dev/null 2>&1 || skip_env "no setfattr command"
+
+       test_mkdir -p $dir
+
+       touch $dir/$tfile
+       ln -s $dir/$tfile $dir/$tfile.symlink
+       setfattr -n "trusted.test" -v "test_target" $dir/$tfile
+       setfattr --no-dereference -n "trusted.test" -v "test_link" \
+               $dir/$tfile.symlink
+       setfattr --no-dereference -n "trusted.common" \
+               $dir/{$tfile,$tfile.symlink}
+
+       found=$($LFS find -xattr "trusted.*=test_target" \
+               -xattr "trusted.common" $dir)
+       [[ "$found" == "$dir/$tfile" ]] || {
+               getfattr -d -m trusted.* $dir/$tfile
+               error "should have found '$tfile' with xattr 'trusted.test=test_target', got '$found'"
+       }
+
+       found=$($LFS find -xattr "trusted.*=test_link" \
+               -xattr "trusted.common" $dir)
+       [[ "$found" == "$dir/$tfile.symlink" ]] || {
+               getfattr --no-dereference -d -m trusted.* $dir/$tfile.symlink
+               error "should have found '$tfile.symlink' with xattr 'trusted.test=test_link', got '$found'"
+       }
+
+       rm -f $dir/*
+
+       touch $dir/$tfile.1
+       touch $dir/$tfile.2
+       setfattr -n "user.test" -v "1" $dir/$tfile.1
+       setfattr -n "user.test" -v "2" $dir/$tfile.2
+       setfattr -n "user.test2" -v "common" $dir/$tfile.{1,2}
+
+       found=$($LFS find -xattr "user.*=common" -xattr "user.test=1" $dir)
+       [[ "$found" == "$dir/$tfile.1" ]] || {
+               getfattr -d $dir/$tfile.1
+               error "should have found '$tfile.1' with xattr user.test=1', got '$found'"
+       }
+
+       found=$($LFS find -xattr "user.*=common" ! -xattr "user.test=1" $dir)
+       [[ "$found" == "$dir/$tfile.2" ]] || {
+               getfattr -d $dir/$tfile.2
+               error "should have found '$tfile.2' without xattr 'user.test=1', got '$found'"
+       }
+
+       setfattr -n "user.empty" $dir/$tfile.1
+       found=$($LFS find -xattr "user.empty" $dir)
+       [[ "$found" == "$dir/$tfile.1" ]] || {
+               getfattr -d $dir/$tfile.1
+               error "should have found '$tfile.1' with xattr 'user.empty=', got '$found'"
+       }
+
+       # setfattr command normally does not store terminating null byte
+       # when writing a string as an xattr value.
+       #
+       # In order to test matching a value string that includes a terminating
+       # null, explicitly encode the string "test\0" with the null terminator.
+       setfattr -n "user.test" -v "0x7465737400" $dir/$tfile.1
+       found=$($LFS find -xattr "user.test=test" $dir)
+       [[ "$found" == "$dir/$tfile.1" ]] || {
+               getfattr -d --encoding=hex $dir/$tfile.1
+               error "should have found '$tfile.1' with xattr 'user.test=0x7465737400', got '$found'"
+       }
+}
+run_test 56eg "lfs find -xattr"
+
 test_57a() {
        [ $PARALLEL == "yes" ] && skip "skip parallel run"
        # note test will not do anything if MDS is not local
index 88713f2..0c0fb62 100644 (file)
@@ -3486,7 +3486,8 @@ enum {
        LFS_STATS_OPT,
        LFS_STATS_INTERVAL_OPT,
        LFS_LINKS_OPT,
-       LFS_ATTRS_OPT
+       LFS_ATTRS_OPT,
+       LFS_XATTRS_MATCH_OPT
 };
 
 #ifndef LCME_USER_MIRROR_FLAGS
@@ -4836,6 +4837,201 @@ static int name2attrs(char *name, __u64 *attrs, __u64 *neg_attrs)
        return 0;
 }
 
+/**
+ * xattr_match_info_append() - add the supplied name and value regex patterns
+ *     to the supplied xattr_match_info struct.
+ *
+ * Return: 0 for success, nonzero if any errors encountered.
+ */
+int xattr_match_info_append(struct xattr_match_info *xmi, bool exclude,
+                           char *name_pattern, char *value_pattern)
+{
+       int flags = REG_EXTENDED;
+       char *err_buf;
+       int err_len;
+       void *nptr;
+       int ret;
+       int n;
+
+       if (xmi->xattr_name_buf == NULL) {
+               xmi->xattr_name_buf = malloc(XATTR_LIST_MAX);
+               if (xmi->xattr_name_buf == NULL)
+                       goto err_out;
+       }
+
+       if (xmi->xattr_value_buf == NULL) {
+               /*
+                * an xattr value need not be null-terminated, so allocate an
+                * extra byte to append a '\0', since regexec() expects a null-
+                * terminated string.
+                */
+               xmi->xattr_value_buf = malloc(XATTR_SIZE_MAX + 1);
+               if (xmi->xattr_value_buf == NULL)
+                       goto err_out;
+       }
+
+       n = ++xmi->xattr_regex_count;
+
+       nptr = realloc(xmi->xattr_regex_matched, n * sizeof(bool));
+       if (nptr == NULL)
+               goto err_out;
+       xmi->xattr_regex_matched = nptr;
+
+       nptr = realloc(xmi->xattr_regex_exclude, n * sizeof(bool));
+       if (nptr == NULL)
+               goto err_out;
+       xmi->xattr_regex_exclude = nptr;
+
+       nptr = realloc(xmi->xattr_regex_name, n * sizeof(regex_t *));
+       if (nptr == NULL)
+               goto err_out;
+       xmi->xattr_regex_name = nptr;
+
+       nptr = realloc(xmi->xattr_regex_value, n * sizeof(regex_t *));
+       if (nptr == NULL)
+               goto err_out;
+       xmi->xattr_regex_value = nptr;
+
+       n--;
+
+       xmi->xattr_regex_exclude[n] = exclude;
+
+       xmi->xattr_regex_name[n] = malloc(sizeof(regex_t));
+       if (xmi->xattr_regex_name[n] == NULL)
+               goto err_out;
+
+       ret = regcomp(xmi->xattr_regex_name[n], name_pattern, flags);
+       if (ret) {
+               err_len = regerror(ret, xmi->xattr_regex_name[n], NULL, 0);
+               err_buf = malloc(err_len);
+               if (err_buf == NULL)
+                       goto err_out;
+
+               regerror(ret, xmi->xattr_regex_name[n], err_buf, err_len);
+               fprintf(stderr, "%s: %s: %s\n",
+                       progname, name_pattern, err_buf);
+               free(err_buf);
+               return ret;
+       }
+
+       if (value_pattern && value_pattern[0] != '\0') {
+               xmi->xattr_regex_value[n] = malloc(sizeof(regex_t));
+               ret = regcomp(xmi->xattr_regex_value[n], value_pattern, flags);
+               if (ret) {
+                       err_len = regerror(ret, xmi->xattr_regex_value[n],
+                                          NULL, 0);
+                       err_buf = malloc(err_len);
+                       if (err_buf == NULL)
+                               goto err_out;
+
+                       regerror(ret, xmi->xattr_regex_value[n], err_buf,
+                                err_len);
+                       fprintf(stderr, "%s: %s: %s\n",
+                               progname, value_pattern, err_buf);
+                       free(err_buf);
+                       return ret;
+               }
+       } else {
+               xmi->xattr_regex_value[n] = NULL;
+       }
+
+       return 0;
+
+err_out:
+       fprintf(stderr, "%s: %s\n", progname, strerror(ENOMEM));
+       return -ENOMEM;
+}
+
+void xattr_match_info_free(struct xattr_match_info *xmi)
+{
+       int i;
+
+       free(xmi->xattr_regex_exclude);
+       xmi->xattr_regex_exclude = NULL;
+
+       free(xmi->xattr_regex_matched);
+       xmi->xattr_regex_matched = NULL;
+
+       for (i = 0; i < xmi->xattr_regex_count; i++) {
+               if (xmi->xattr_regex_name[i]) {
+                       regfree(xmi->xattr_regex_name[i]);
+                       free(xmi->xattr_regex_name[i]);
+               }
+
+               if (xmi->xattr_regex_value[i]) {
+                       regfree(xmi->xattr_regex_value[i]);
+                       free(xmi->xattr_regex_value[i]);
+               }
+       }
+
+       xmi->xattr_regex_count = 0;
+
+       free(xmi->xattr_regex_name);
+       xmi->xattr_regex_name = NULL;
+
+       free(xmi->xattr_regex_value);
+       xmi->xattr_regex_value = NULL;
+
+       free(xmi->xattr_name_buf);
+       xmi->xattr_name_buf = NULL;
+
+       free(xmi->xattr_value_buf);
+       xmi->xattr_value_buf = NULL;
+}
+
+/**
+ * compile_xattr_match_regex() - Compile regexes for matching xattr names and
+ * values, returning an error if either fails to compile.
+ *
+ * The argument should be in the form "NAME=VALUE". The first '=' found
+ * is assumed to be the separator between the name regex and the value regex.
+ *
+ * VALUE may be empty. If it is empty, it is not compiled and left NULL.
+ * NAME must not be empty.
+ *
+ * Return: 0 if argument string is succesfully processed, nonzero if any
+ *         errors encountered.
+ */
+static int compile_xattr_match_regex(char *optarg, bool exclude,
+                                    struct find_param *param)
+{
+       char *sep;
+
+       sep = strchr(optarg, '=');
+       if (sep)
+               *sep = '\0';
+
+       /* error if no NAME pattern specified */
+       if (*optarg == '\0') {
+               fprintf(stderr, "%s: must specify xattr pattern\n", progname);
+               return CMD_HELP;
+       }
+
+       /* if first -xattr option seen */
+       if (param->fp_xattr_match_info == NULL) {
+               param->fp_xattr_match_info = calloc(1,
+                                       sizeof(struct xattr_match_info));
+               if (param->fp_xattr_match_info == NULL) {
+                       fprintf(stderr, "%s: %s\n", progname, strerror(ENOMEM));
+                       return -ENOMEM;
+               }
+       }
+
+       /*
+        * if '=' was not provided, or if there is no value after the '=',
+        * then pass NULL to xattr_match_info_append() so that no VALUE regex
+        * is compiled.
+        */
+       if (sep) {
+               sep++;
+               if (*sep == '\0')
+                       sep = NULL;
+       }
+
+       return xattr_match_info_append(param->fp_xattr_match_info, exclude,
+                                      optarg, sep);
+}
+
 static int parse_symbolic(const char *input, mode_t *outmode, const char **end)
 {
        int loop;
@@ -5204,6 +5400,8 @@ static int lfs_find(int argc, char **argv)
        { .val = 'U',   .name = "user",         .has_arg = required_argument },
 /* getstripe { .val = 'v', .name = "verbose",  .has_arg = no_argument }, */
 /* setstripe { .val = 'W', .name = "bandwidth",        .has_arg = required_argument }, */
+       { .val = LFS_XATTRS_MATCH_OPT,
+                       .name = "xattr",        .has_arg = required_argument },
        { .val = 'z',   .name = "extension-size",
                                                .has_arg = required_argument },
        { .val = 'z',   .name = "ext-size",     .has_arg = required_argument },
@@ -5969,6 +6167,12 @@ err_free:
                        param.fp_check_mdt_count = 1;
                        param.fp_exclude_mdt_count = !!neg_opt;
                        break;
+               case LFS_XATTRS_MATCH_OPT:
+                       ret = compile_xattr_match_regex(optarg, neg_opt,
+                                                       &param);
+                       if (ret)
+                               goto err;
+                       break;
                case 'z':
                        if (optarg[0] == '+') {
                                param.fp_ext_size_sign = -1;
@@ -6032,6 +6236,12 @@ err:
        if (param.fp_format_printf_str)
                free(param.fp_format_printf_str);
 
+       if (param.fp_xattr_match_info) {
+               xattr_match_info_free(param.fp_xattr_match_info);
+               free(param.fp_xattr_match_info);
+               param.fp_xattr_match_info = NULL;
+       }
+
        return ret;
 }
 
index f996a56..333d938 100644 (file)
@@ -4600,6 +4600,151 @@ static int find_check_attr_options(struct find_param *param)
        return 1;
 }
 
+/**
+ * xattr_reg_match() - return true if the supplied string matches the pattern.
+ *
+ * This requires the regex to match the entire supplied string, not just a
+ *     substring.
+ *
+ * str must be null-terminated. len should be passed in anyways to avoid an
+ *     extra call to strlen(str) when the length is already known.
+ */
+static bool xattr_reg_match(regex_t *pattern, const char *str, int len)
+{
+       regmatch_t pmatch;
+       int ret;
+
+       ret = regexec(pattern, str, 1, &pmatch, 0);
+       if (ret == 0 && pmatch.rm_so == 0 && pmatch.rm_eo == len)
+               return true;
+
+       return false;
+}
+
+/**
+ * xattr_done_matching() - return true if all supplied patterns have been
+ *     matched, allowing to skip checking any remaining xattrs on a file.
+ *
+ *     This is only allowed if there are no "exclude" patterns.
+ */
+static int xattr_done_matching(struct xattr_match_info *xmi)
+{
+       int i;
+
+       for (i = 0; i < xmi->xattr_regex_count; i++) {
+               /* if any pattern still undecided, need to keep going */
+               if (!xmi->xattr_regex_matched[i])
+                       return false;
+       }
+
+       return true;
+}
+
+static int find_check_xattrs(char *path, struct xattr_match_info *xmi)
+{
+       ssize_t list_len = 0;
+       ssize_t val_len = 0;
+       bool fetched_val;
+       char *p;
+       int i;
+
+       for (i = 0; i < xmi->xattr_regex_count; i++)
+               xmi->xattr_regex_matched[i] = false;
+
+       list_len = llistxattr(path, xmi->xattr_name_buf, XATTR_LIST_MAX);
+       if (list_len < 0) {
+               llapi_error(LLAPI_MSG_ERROR, errno,
+                           "error: listxattr: %s", path);
+               return -1;
+       }
+
+       /* loop over all xattr names on the file */
+       for (p = xmi->xattr_name_buf;
+            p - xmi->xattr_name_buf < list_len;
+            p = strchr(p, '\0'), p++) {
+               fetched_val = false;
+               /* loop over all regex patterns specified and check them */
+               for (i = 0; i < xmi->xattr_regex_count; i++) {
+                       if (xmi->xattr_regex_matched[i])
+                               continue;
+
+                       if (!xattr_reg_match(xmi->xattr_regex_name[i],
+                                            p, strlen(p)))
+                               continue;
+
+                       if (xmi->xattr_regex_value[i] == NULL)
+                               goto matched;
+
+                       /*
+                        * even if multiple patterns match the same xattr name,
+                        * don't call getxattr() more than once
+                        */
+                       if (!fetched_val) {
+                               val_len = lgetxattr(path, p,
+                                                   xmi->xattr_value_buf,
+                                                   XATTR_SIZE_MAX);
+                               fetched_val = true;
+                               if (val_len < 0) {
+                                       llapi_error(LLAPI_MSG_ERROR, errno,
+                                                   "error: getxattr: %s",
+                                                   path);
+                                       continue;
+                               }
+
+                               /*
+                                * the value returned by getxattr might or
+                                * might not be null terminated.
+                                * if it is, then decrement val_len so it
+                                * matches what strlen() would return.
+                                * if it is not, then add a null terminator
+                                * since regexec() expects that.
+                                */
+                               if (val_len > 0 &&
+                                   xmi->xattr_value_buf[val_len - 1] == '\0') {
+                                       val_len--;
+                               } else {
+                                       xmi->xattr_value_buf[val_len] = '\0';
+                               }
+                       }
+
+                       if (!xattr_reg_match(xmi->xattr_regex_value[i],
+                                            xmi->xattr_value_buf, val_len))
+                               continue;
+
+matched:
+                       /*
+                        * if exclude this xattr, we can exit early
+                        * with NO match
+                        */
+                       if (xmi->xattr_regex_exclude[i])
+                               return -1;
+
+                       xmi->xattr_regex_matched[i] = true;
+
+                       /*
+                        * if all "include" patterns have matched, and there are
+                        * no "exclude" patterns, we can exit early with match
+                        */
+                       if (xattr_done_matching(xmi) == 1)
+                               return 1;
+               }
+       }
+
+       /*
+        * finally, check that all supplied patterns either matched, or were
+        * "exclude" patterns if they did not match.
+        */
+       for (i = 0; i < xmi->xattr_regex_count; i++) {
+               if (!xmi->xattr_regex_matched[i]) {
+                       if (!xmi->xattr_regex_exclude[i]) {
+                               return -1;
+                       }
+               }
+       }
+
+       return 1;
+}
+
 static bool find_check_lmm_info(struct find_param *param)
 {
        return param->fp_check_pool || param->fp_check_stripe_count ||
@@ -5725,6 +5870,12 @@ obd_matches:
              (param->fp_lazy && flags & OBD_MD_FLLAZYBLOCKS)))
                decision = 0;
 
+       if (param->fp_xattr_match_info) {
+               decision = find_check_xattrs(path, param->fp_xattr_match_info);
+               if (decision == -1)
+                       goto decided;
+       }
+
        /*
         * When checking nlink, stat(2) is needed for multi-striped directories
         * because the nlink value retrieved from the MDS above comes from