[[\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
.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
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
.BR lfs-migrate (1),
.BR lfs_migrate (1),
.BR lustre (7),
+.BR regex (7),
.BR xargs (1)
*/
#include <glob.h>
+#include <regex.h>
#include <stdarg.h>
#include <stdint.h>
#include <time.h>
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
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);
}
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
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
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;
{ .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 },
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,
+ ¶m);
+ if (ret)
+ goto err;
+ break;
case 'z':
if (optarg[0] == '+') {
param.fp_ext_size_sign = -1;
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;
}
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 ||
(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