diff --git a/include/os/linux/zfs/sys/zfs_ctldir.h b/include/os/linux/zfs/sys/zfs_ctldir.h index ad16ab5e4444..e95e5d9ebe3a 100644 --- a/include/os/linux/zfs/sys/zfs_ctldir.h +++ b/include/os/linux/zfs/sys/zfs_ctldir.h @@ -39,6 +39,9 @@ #define ZFS_CTLDIR_NAME ".zfs" #define ZFS_SNAPDIR_NAME "snapshot" #define ZFS_SHAREDIR_NAME "shares" +#define ZFS_USERSPACEFILE_NAME "userspace" +#define ZFS_GROUPSPACEFILE_NAME "groupspace" +#define ZFS_PROJSPACEFILE_NAME "projectspace" #define zfs_has_ctldir(zdp) \ ((zdp)->z_id == ZTOZSB(zdp)->z_root && \ @@ -48,6 +51,7 @@ (ZTOZSB(zdp)->z_show_ctldir)) extern int zfs_expire_snapshot; +extern int zfs_ctldir_spacefiles; /* zfsctl generic functions */ extern int zfsctl_create(zfsvfs_t *); @@ -95,8 +99,12 @@ extern int zfsctl_shares_lookup(struct inode *dip, char *name, */ #define ZFSCTL_INO_ROOT 0x0000FFFFFFFFFFFFULL #define ZFSCTL_INO_SHARES 0x0000FFFFFFFFFFFEULL -#define ZFSCTL_INO_SNAPDIR 0x0000FFFFFFFFFFFDULL -#define ZFSCTL_INO_SNAPDIRS 0x0000FFFFFFFFFFFCULL +#define ZFSCTL_INO_USERSPACE 0x0000FFFFFFFFFFFDULL +#define ZFSCTL_INO_GROUPSPACE 0x0000FFFFFFFFFFFCULL +#define ZFSCTL_INO_PROJSPACE 0x0000FFFFFFFFFFFBULL +#define ZFSCTL_INO_SNAPDIR 0x0000FFFFFFFFFFFAULL +#define ZFSCTL_INO_SNAPDIRS 0x0000FFFFFFFFFFF9ULL + #define ZFSCTL_EXPIRE_SNAPSHOT 300 diff --git a/include/os/linux/zfs/sys/zpl.h b/include/os/linux/zfs/sys/zpl.h index 91a4751fffb0..5e4f8b91fce1 100644 --- a/include/os/linux/zfs/sys/zpl.h +++ b/include/os/linux/zfs/sys/zpl.h @@ -118,6 +118,15 @@ extern const struct inode_operations zpl_ops_snapdir; extern const struct file_operations zpl_fops_shares; extern const struct inode_operations zpl_ops_shares; +extern const struct file_operations zpl_fops_userspacefile; +extern const struct inode_operations zpl_ops_userspace_file; + +extern const struct file_operations zpl_fops_groupspacefile; +extern const struct inode_operations zpl_ops_groupspace_file; + +extern const struct file_operations zpl_fops_projectspacefile; +extern const struct inode_operations zpl_ops_projectspace_file; + #if defined(HAVE_VFS_ITERATE) || defined(HAVE_VFS_ITERATE_SHARED) #define ZPL_DIR_CONTEXT_INIT(_dirent, _actor, _pos) { \ diff --git a/man/man4/zfs.4 b/man/man4/zfs.4 index 47471a805907..ccfc2196cdbe 100644 --- a/man/man4/zfs.4 +++ b/man/man4/zfs.4 @@ -1366,6 +1366,25 @@ which have the .Em no_root_squash option set. . +.It Sy zfs_ctldir_spacefiles Ns = Ns Sy 0 Ns | Ns 1 Pq int +Enable the existence of three files in the +.Sy .zfs +directory: +.Sy userspace , +.Sy groupspace , +and +.Sy projectspace . +These files can be used in lieu of the +.Sy zfs-userspace(8) , +.Sy zfs-groupspace(8) , +and +.Sy zfs-projectspace(8) +tools, respectively, to determine space and quota utilization for a +given dataset. +These would normally be used when the dataset is being accessed over +a network, e.g. using NFS, where ZFS command line tools, libzfs, +etc. are not available. +. .It Sy zfs_flags Ns = Ns Sy 0 Pq int Set additional debugging flags. The following flags may be bitwise-ored together: diff --git a/module/os/linux/zfs/zfs_ctldir.c b/module/os/linux/zfs/zfs_ctldir.c index 54ed70d0394f..252f4fe8688e 100644 --- a/module/os/linux/zfs/zfs_ctldir.c +++ b/module/os/linux/zfs/zfs_ctldir.c @@ -111,6 +111,7 @@ static krwlock_t zfs_snapshot_lock; */ int zfs_expire_snapshot = ZFSCTL_EXPIRE_SNAPSHOT; static int zfs_admin_snapshot = 0; +int zfs_ctldir_spacefiles = 0; typedef struct { char *se_name; /* full snapshot name */ @@ -818,6 +819,21 @@ zfsctl_root_lookup(struct inode *dip, const char *name, struct inode **ipp, } else if (strcmp(name, ZFS_SHAREDIR_NAME) == 0) { *ipp = zfsctl_inode_lookup(zfsvfs, ZFSCTL_INO_SHARES, &zpl_fops_shares, &zpl_ops_shares); + } else if (strcmp(name, ZFS_USERSPACEFILE_NAME) == 0 && + zfs_ctldir_spacefiles) { + *ipp = zfsctl_inode_lookup(zfsvfs, ZFSCTL_INO_USERSPACE, + &zpl_fops_userspacefile, &zpl_ops_userspace_file); + (*ipp)->i_mode = (S_IFREG|S_IRUGO); + } else if (strcmp(name, ZFS_GROUPSPACEFILE_NAME) == 0 && + zfs_ctldir_spacefiles) { + *ipp = zfsctl_inode_lookup(zfsvfs, ZFSCTL_INO_GROUPSPACE, + &zpl_fops_groupspacefile, &zpl_ops_groupspace_file); + (*ipp)->i_mode = (S_IFREG|S_IRUGO); + } else if (strcmp(name, ZFS_PROJSPACEFILE_NAME) == 0 && + zfs_ctldir_spacefiles) { + *ipp = zfsctl_inode_lookup(zfsvfs, ZFSCTL_INO_PROJSPACE, + &zpl_fops_projectspacefile, &zpl_ops_projectspace_file); + (*ipp)->i_mode = (S_IFREG|S_IRUGO); } else { *ipp = NULL; } @@ -1315,3 +1331,7 @@ MODULE_PARM_DESC(zfs_admin_snapshot, "Enable mkdir/rmdir/mv in .zfs/snapshot"); module_param(zfs_expire_snapshot, int, 0644); MODULE_PARM_DESC(zfs_expire_snapshot, "Seconds to expire .zfs/snapshot"); + +module_param(zfs_ctldir_spacefiles, int, 0644); +MODULE_PARM_DESC(zfs_ctldir_spacefiles, + "Enable user/group/projectspace files in .zfs/"); diff --git a/module/os/linux/zfs/zpl_ctldir.c b/module/os/linux/zfs/zpl_ctldir.c index 8ee7fcecc7b7..303d1231983c 100644 --- a/module/os/linux/zfs/zpl_ctldir.c +++ b/module/os/linux/zfs/zpl_ctldir.c @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -78,6 +79,33 @@ zpl_root_iterate(struct file *filp, zpl_dir_context_t *ctx) ctx->pos++; } + + if (ctx->pos == 4 && zfs_ctldir_spacefiles) { + if (!zpl_dir_emit(ctx, ZFS_USERSPACEFILE_NAME, + strlen(ZFS_USERSPACEFILE_NAME), + ZFSCTL_INO_USERSPACE, DT_REG)) + goto out; + + ctx->pos++; + } + + if (ctx->pos == 5 && zfs_ctldir_spacefiles) { + if (!zpl_dir_emit(ctx, ZFS_GROUPSPACEFILE_NAME, + strlen(ZFS_GROUPSPACEFILE_NAME), + ZFSCTL_INO_GROUPSPACE, DT_REG)) + goto out; + + ctx->pos++; + } + + if (ctx->pos == 6 && zfs_ctldir_spacefiles) { + if (!zpl_dir_emit(ctx, ZFS_PROJSPACEFILE_NAME, + strlen(ZFS_PROJSPACEFILE_NAME), + ZFSCTL_INO_PROJSPACE, DT_REG)) + goto out; + + ctx->pos++; + } out: zpl_exit(zfsvfs, FTAG); @@ -671,3 +699,163 @@ const struct inode_operations zpl_ops_shares = { .lookup = zpl_shares_lookup, .getattr = zpl_shares_getattr, }; + +/* + * Helpers for: + * .zfs/userspace + * .zfs/groupspace + * .zfs/projectspace + */ +static int foreach_zfs_useracct(zfsvfs_t *zfsvfs, + zfs_userquota_prop_t type, uint64_t cookie, + int (*fn)(zfs_useracct_t *zua, zfs_userquota_prop_t type, void *v), + void *fn_arg) +{ + uint64_t cbufsize, bufsize = 100 * sizeof (zfs_useracct_t); + zfs_useracct_t *buf = (zfs_useracct_t *)vmem_alloc(bufsize, KM_SLEEP); + int err = 0; + for (;;) { + cbufsize = bufsize; + if (zfs_userspace_many(zfsvfs, type, &cookie, + buf, &cbufsize)) { + err = 1; + break; + }; + if (cbufsize == 0) { + break; + } + zfs_useracct_t *zua = buf; + while (cbufsize > 0) { + if (fn(zua, type, fn_arg)) { + err = 1; + } + zua++; + cbufsize -= sizeof (zfs_useracct_t); + } + } + vmem_free(buf, bufsize); + return (err); +} + +static int zua_nvlist_add(zfs_useracct_t *zua, + zfs_userquota_prop_t type, void *v) +{ + nvlist_t *ids = (nvlist_t *)v; + char name[MAXNAMELEN]; + (void) snprintf(name, sizeof (name), "%u", zua->zu_rid); + nvlist_t *spacelist; + if (nvlist_lookup_nvlist(ids, name, &spacelist) || + spacelist == NULL) { + VERIFY0(nvlist_alloc(&spacelist, NV_UNIQUE_NAME, KM_NOSLEEP)); + VERIFY0(nvlist_add_nvlist(ids, name, spacelist)); + /* lookup again because nvlist_add_nvlist does a deep-copy */ + VERIFY0(nvlist_lookup_nvlist(ids, name, &spacelist)); + } + VERIFY0(nvlist_add_uint64(spacelist, + zfs_userquota_prop_prefixes[type], zua->zu_space)); + return (0); +} + +static void seq_print_spaceval(struct seq_file *seq, nvlist_t *spacelist, + zfs_userquota_prop_t type) +{ + uint64_t spaceval; + seq_printf(seq, ","); + if (!nvlist_lookup_uint64(spacelist, + zfs_userquota_prop_prefixes[type], + &spaceval)) { + seq_printf(seq, "%llu", spaceval); + } +} + +static int zpl_userspace_show(struct seq_file *seq, void *v) +{ + zfs_userquota_prop_t *props = (zfs_userquota_prop_t *)seq->private; + seq_printf(seq, "id,used,quota,objused,objquota\n"); + zfsvfs_t *zfsvfs = ITOZSB(file_inode(seq->file)); + nvlist_t *ids; + unsigned int i; + VERIFY0(nvlist_alloc(&ids, NV_UNIQUE_NAME, 0)); + for (i = 0; i < 4; ++i) + VERIFY0(foreach_zfs_useracct(zfsvfs, props[i], 0, + zua_nvlist_add, ids)); + for (nvpair_t *idpair = nvlist_next_nvpair(ids, NULL); + idpair != NULL; idpair = nvlist_next_nvpair(ids, idpair)) { + const char *id = nvpair_name(idpair); + seq_printf(seq, "%s", id); + nvlist_t *spacelist; + VERIFY0(nvpair_value_nvlist(idpair, &spacelist)); + for (i = 0; i < 4; ++i) + seq_print_spaceval(seq, spacelist, props[i]); + seq_putc(seq, '\n'); + } + nvlist_free(ids); + return (0); +} + +static zfs_userquota_prop_t userspace_props[4] = { + ZFS_PROP_USERUSED, + ZFS_PROP_USERQUOTA, + ZFS_PROP_USEROBJUSED, + ZFS_PROP_USEROBJQUOTA +}; + +static zfs_userquota_prop_t groupspace_props[4] = { + ZFS_PROP_GROUPUSED, + ZFS_PROP_GROUPQUOTA, + ZFS_PROP_GROUPOBJUSED, + ZFS_PROP_GROUPOBJQUOTA +}; + +static zfs_userquota_prop_t projectspace_props[4] = { + ZFS_PROP_PROJECTUSED, + ZFS_PROP_PROJECTQUOTA, + ZFS_PROP_PROJECTOBJUSED, + ZFS_PROP_PROJECTOBJQUOTA +}; + +static int zpl_fops_userspace_open(struct inode *inode, struct file *file) +{ + return single_open(file, zpl_userspace_show, + (void *)userspace_props); +} + +static int zpl_fops_groupspace_open(struct inode *inode, struct file *file) +{ + return single_open(file, zpl_userspace_show, + (void *)groupspace_props); +} + +static int zpl_fops_projectspace_open(struct inode *inode, struct file *file) +{ + return single_open(file, zpl_userspace_show, + (void *)projectspace_props); +} + +/* .zfs/userspace */ +const struct file_operations zpl_fops_userspacefile = { + .open = zpl_fops_userspace_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release +}; + +/* .zfs/groupspace */ +const struct file_operations zpl_fops_groupspacefile = { + .open = zpl_fops_groupspace_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release +}; + +/* .zfs/projectspace */ +const struct file_operations zpl_fops_projectspacefile = { + .open = zpl_fops_projectspace_open, + .read = seq_read, + .llseek = seq_lseek, + .release = single_release +}; + +const struct inode_operations zpl_ops_userspace_file = {}; +const struct inode_operations zpl_ops_groupspace_file = {}; +const struct inode_operations zpl_ops_projectspace_file = {}; diff --git a/tests/runfiles/linux.run b/tests/runfiles/linux.run index 6a4cd3fe691c..67f08edff563 100644 --- a/tests/runfiles/linux.run +++ b/tests/runfiles/linux.run @@ -210,7 +210,7 @@ tags = ['functional', 'user_namespace'] [tests/functional/userquota:Linux] tests = ['groupspace_001_pos', 'groupspace_002_pos', 'groupspace_003_pos', - 'userquota_013_pos', 'userspace_003_pos'] + 'userquota_013_pos', 'userspace_003_pos', 'userspace_ctldir_files_001'] tags = ['functional', 'userquota'] [tests/functional/zvol/zvol_misc:Linux] diff --git a/tests/zfs-tests/tests/Makefile.am b/tests/zfs-tests/tests/Makefile.am index 4040e60434a7..bfa004b025ea 100644 --- a/tests/zfs-tests/tests/Makefile.am +++ b/tests/zfs-tests/tests/Makefile.am @@ -2035,6 +2035,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \ functional/userquota/userspace_encrypted.ksh \ functional/userquota/userspace_send_encrypted.ksh \ functional/userquota/userspace_encrypted_13709.ksh \ + functional/userquota/userspace_ctldir_files_001.ksh \ functional/vdev_zaps/cleanup.ksh \ functional/vdev_zaps/setup.ksh \ functional/vdev_zaps/vdev_zaps_001_pos.ksh \ diff --git a/tests/zfs-tests/tests/functional/userquota/userspace_ctldir_files_001.ksh b/tests/zfs-tests/tests/functional/userquota/userspace_ctldir_files_001.ksh new file mode 100755 index 000000000000..91bbc7a50fb1 --- /dev/null +++ b/tests/zfs-tests/tests/functional/userquota/userspace_ctldir_files_001.ksh @@ -0,0 +1,69 @@ +#!/bin/ksh -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or https://opensource.org/licenses/CDDL-1.0. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +. $STF_SUITE/include/libtest.shlib +. $STF_SUITE/tests/functional/userquota/userquota_common.kshlib + +# +# DESCRIPTION: +# Check the zfs space files in .zfs directory +# +# +# STRATEGY: +# 1. set zfs userquota to a fs +# 2. write some data to the fs with specified user and group +# 3. use zfs space files in .zfs to check the result +# + +function cleanup +{ + log_must cleanup_quota + echo 0 >$SPACEFILES_ENABLED_PARAM || log_fail +} + +log_onexit cleanup + +log_assert "Check the .zfs space files" + +typeset userquota=104857600 +typeset groupquota=524288000 + +log_must zfs set userquota@$QUSER1=$userquota $QFS +log_must zfs set groupquota@$QGROUP=$groupquota $QFS +mkmount_writable $QFS +log_must user_run $QUSER1 mkfile 50m $QFILE + +typeset SPACEFILES_ENABLED_PARAM=/sys/module/zfs/parameters/zfs_ctldir_spacefiles +echo 1 >$SPACEFILES_ENABLED_PARAM || log_fail +typeset mntp=$(get_prop mountpoint $QFS) +typeset user_id=$(id -u $QUSER1) || log_fail +typeset group_id=$(id -g $QUSER1) || log_fail + +sync_all_pools + +log_must eval "grep \"^$user_id\" $mntp/.zfs/userspace" +log_must eval "grep \"^$user_id,[[:digit:]]*,$userquota\" $mntp/.zfs/userspace" +log_must eval "grep \"^$group_id\" $mntp/.zfs/groupspace" +log_must eval "grep \"^$group_id,[[:digit:]]*,$groupquota\" $mntp/.zfs/groupspace" + +log_pass "Check the .zfs space files" +