From 03006174b7fbcbccee3fe1ced2143a57aae1afa1 Mon Sep 17 00:00:00 2001 From: Todd Seidelmann <18294602+seidelma@users.noreply.github.com> Date: Mon, 17 Jun 2024 00:41:22 -0700 Subject: [PATCH] Implement default user and group quotas This change adds the 'defaultuserquota' and 'defaultgroupquota' properties to ZFS datasets to apply a quota to users and groups that do not have a specific quota assigned. The default quota checking mechanism works alongside the existing 'userquota' and 'groupquota' checks, only taking effect if no quota is assigned for a particular user/group. This means that it's possible to exceed a default quota by quite a lot before the user/groupused property is updated and further writes are denied, which was already the case for user/groupquota. Default quotas are implemented similarly to 'normal' user quotas, but instead of being user properties that are preserved across snapshots, they default back to none. NB: this is different from the observed Solaris behavior, which is to preserve default quotas across snapshot/clone/promote. For instance, Solaris has: # zfs set defaultuserquota=100M tank/fs # zfs snap tank/fs@snap # zfs clone tank/fs@snap tank/fs-clone # zfs get -H defaultuserquota tank/fs-clone tank/fs-clone defaultuserquota 100M - Whereas this commit does: # zfs set defaultuserquota=100M tank/fs # zfs snap tank/fs@snap # zfs clone tank/fs@snap tank/fs-clone # zfs get -H defaultuserquota tank/fs-clone tank/fs-clone defaultuserquota none default It should also be possible to implement a default project quota using an analogous process, if doing so makes sense. Signed-off-by: Todd Seidelmann --- cmd/zfs/zfs_main.c | 4 ++ include/os/linux/zfs/sys/zfs_vfsops_os.h | 2 + include/sys/fs/zfs.h | 2 + include/sys/zfs_quota.h | 1 + lib/libzfs/libzfs_dataset.c | 2 + module/os/linux/zfs/zfs_vfsops.c | 16 +++++ module/zcommon/zfs_prop.c | 6 ++ module/zfs/zfs_ioctl.c | 19 +++++ module/zfs/zfs_quota.c | 89 ++++++++++++++++++++++-- 9 files changed, 136 insertions(+), 5 deletions(-) diff --git a/cmd/zfs/zfs_main.c b/cmd/zfs/zfs_main.c index 75c0e40b61..274fdad980 100644 --- a/cmd/zfs/zfs_main.c +++ b/cmd/zfs/zfs_main.c @@ -585,6 +585,10 @@ usage(boolean_t requested) (void) fprintf(fp, "YES NO | none\n"); (void) fprintf(fp, "\t%-15s ", "projectobjquota@..."); (void) fprintf(fp, "YES NO | none\n"); + (void) fprintf(fp, "\t%-15s ", "defaultuserquota"); + (void) fprintf(fp, "YES NO | none\n"); + (void) fprintf(fp, "\t%-15s ", "defaultgroupquota"); + (void) fprintf(fp, "YES NO | none\n"); (void) fprintf(fp, "\t%-15s ", "written@"); (void) fprintf(fp, " NO NO \n"); (void) fprintf(fp, "\t%-15s ", "written#"); diff --git a/include/os/linux/zfs/sys/zfs_vfsops_os.h b/include/os/linux/zfs/sys/zfs_vfsops_os.h index b4d5db21f5..6a8e100978 100644 --- a/include/os/linux/zfs/sys/zfs_vfsops_os.h +++ b/include/os/linux/zfs/sys/zfs_vfsops_os.h @@ -128,6 +128,8 @@ struct zfsvfs { uint64_t z_groupobjquota_obj; uint64_t z_projectquota_obj; uint64_t z_projectobjquota_obj; + uint64_t z_defaultuserquota_obj; + uint64_t z_defaultgroupquota_obj; uint64_t z_replay_eof; /* New end of file - replay only */ sa_attr_type_t *z_attr_table; /* SA attr mapping->id */ uint64_t z_hold_size; /* znode hold array size */ diff --git a/include/sys/fs/zfs.h b/include/sys/fs/zfs.h index c7e48d1edc..91323dd5d1 100644 --- a/include/sys/fs/zfs.h +++ b/include/sys/fs/zfs.h @@ -193,6 +193,8 @@ typedef enum { ZFS_PROP_SNAPSHOTS_CHANGED, ZFS_PROP_PREFETCH, ZFS_PROP_VOLTHREADING, + ZFS_PROP_DEFAULTUSERQUOTA, + ZFS_PROP_DEFAULTGROUPQUOTA, ZFS_NUM_PROPS } zfs_prop_t; diff --git a/include/sys/zfs_quota.h b/include/sys/zfs_quota.h index 4567cc651a..e184f89459 100644 --- a/include/sys/zfs_quota.h +++ b/include/sys/zfs_quota.h @@ -37,6 +37,7 @@ extern int zfs_userspace_many(struct zfsvfs *, zfs_userquota_prop_t, uint64_t *, void *, uint64_t *); extern int zfs_set_userquota(struct zfsvfs *, zfs_userquota_prop_t, const char *, uint64_t, uint64_t); +extern int zfs_set_defaultquota(struct zfsvfs *, int, uint64_t); extern boolean_t zfs_id_overobjquota(struct zfsvfs *, uint64_t, uint64_t); extern boolean_t zfs_id_overblockquota(struct zfsvfs *, uint64_t, uint64_t); diff --git a/lib/libzfs/libzfs_dataset.c b/lib/libzfs/libzfs_dataset.c index 231bbbd92d..e670379cf4 100644 --- a/lib/libzfs/libzfs_dataset.c +++ b/lib/libzfs/libzfs_dataset.c @@ -2811,6 +2811,8 @@ zfs_prop_get(zfs_handle_t *zhp, zfs_prop_t prop, char *propbuf, size_t proplen, case ZFS_PROP_REFQUOTA: case ZFS_PROP_RESERVATION: case ZFS_PROP_REFRESERVATION: + case ZFS_PROP_DEFAULTUSERQUOTA: + case ZFS_PROP_DEFAULTGROUPQUOTA: if (get_numeric_property(zhp, prop, src, &source, &val) != 0) return (-1); diff --git a/module/os/linux/zfs/zfs_vfsops.c b/module/os/linux/zfs/zfs_vfsops.c index 2015c20d73..e5161a04e3 100644 --- a/module/os/linux/zfs/zfs_vfsops.c +++ b/module/os/linux/zfs/zfs_vfsops.c @@ -743,6 +743,22 @@ zfsvfs_init(zfsvfs_t *zfsvfs, objset_t *os) else if (error != 0) return (error); + error = zap_lookup(os, MASTER_NODE_OBJ, + zfs_prop_to_name(ZFS_PROP_DEFAULTUSERQUOTA), + 8, 1, &zfsvfs->z_defaultuserquota_obj); + if (error == ENOENT) + zfsvfs->z_defaultuserquota_obj = 0; + else if (error != 0) + return (error); + + error = zap_lookup(os, MASTER_NODE_OBJ, + zfs_prop_to_name(ZFS_PROP_DEFAULTGROUPQUOTA), + 8, 1, &zfsvfs->z_defaultgroupquota_obj); + if (error == ENOENT) + zfsvfs->z_defaultgroupquota_obj = 0; + else if (error != 0) + return (error); + error = zap_lookup(os, MASTER_NODE_OBJ, ZFS_FUID_TABLES, 8, 1, &zfsvfs->z_fuid_obj); if (error == ENOENT) diff --git a/module/zcommon/zfs_prop.c b/module/zcommon/zfs_prop.c index 764993b45e..6a08d0a411 100644 --- a/module/zcommon/zfs_prop.c +++ b/module/zcommon/zfs_prop.c @@ -694,6 +694,12 @@ zfs_prop_init(void) zprop_register_number(ZFS_PROP_SNAPSHOT_LIMIT, "snapshot_limit", UINT64_MAX, PROP_DEFAULT, ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME, " | none", "SSLIMIT", B_FALSE, sfeatures); + zprop_register_number(ZFS_PROP_DEFAULTUSERQUOTA, "defaultuserquota", 0, + PROP_DEFAULT, ZFS_TYPE_FILESYSTEM, " | none", + "DEFAULTUSERQUOTA", B_FALSE, sfeatures); + zprop_register_number(ZFS_PROP_DEFAULTGROUPQUOTA, "defaultgroupquota", + 0, PROP_DEFAULT, ZFS_TYPE_FILESYSTEM, " | none", + "DEFAULTGROUPQUOTA", B_FALSE, sfeatures); /* inherit number properties */ zprop_register_number(ZFS_PROP_RECORDSIZE, "recordsize", diff --git a/module/zfs/zfs_ioctl.c b/module/zfs/zfs_ioctl.c index 897335dd4e..63c0f9efe4 100644 --- a/module/zfs/zfs_ioctl.c +++ b/module/zfs/zfs_ioctl.c @@ -2567,6 +2567,25 @@ zfs_prop_set_special(const char *dsname, zprop_source_t source, } break; } + case ZFS_PROP_DEFAULTUSERQUOTA: + case ZFS_PROP_DEFAULTGROUPQUOTA: + { + zfsvfs_t *zfsvfs; + + if ((err = zfsvfs_hold(dsname, FTAG, &zfsvfs, B_TRUE)) != 0) + break; + + err = zfs_set_defaultquota(zfsvfs, prop, intval); + zfsvfs_rele(zfsvfs, FTAG); + + /* + * Set err to -1 to force the zfs_set_prop_nvlist code down the + * default path to set the value in the nvlist. + */ + if (err == 0) + err = -1; + break; + } default: err = -1; } diff --git a/module/zfs/zfs_quota.c b/module/zfs/zfs_quota.c index 9b351eefc0..e1856c5d63 100644 --- a/module/zfs/zfs_quota.c +++ b/module/zfs/zfs_quota.c @@ -366,6 +366,59 @@ zfs_set_userquota(zfsvfs_t *zfsvfs, zfs_userquota_prop_t type, return (err); } +int +zfs_set_defaultquota(zfsvfs_t *zfsvfs, int type, uint64_t quota) +{ + int err; + dmu_tx_t *tx; + uint64_t *objp; + const char *name; + + if (type == ZFS_PROP_DEFAULTUSERQUOTA) { + objp = &zfsvfs->z_defaultuserquota_obj; + name = zfs_prop_to_name(ZFS_PROP_DEFAULTUSERQUOTA); + } else if (type == ZFS_PROP_DEFAULTGROUPQUOTA) { + objp = &zfsvfs->z_defaultgroupquota_obj; + name = zfs_prop_to_name(ZFS_PROP_DEFAULTGROUPQUOTA); + } else { + /* defaultprojectquota NYI (does it make sense to do so?) */ + return (SET_ERROR(EINVAL)); + } + + tx = dmu_tx_create(zfsvfs->z_os); + dmu_tx_hold_zap(tx, *objp ? *objp : DMU_NEW_OBJECT, B_TRUE, NULL); + if (*objp == 0) { + dmu_tx_hold_zap(tx, MASTER_NODE_OBJ, B_TRUE, name); + } + err = dmu_tx_assign(tx, TXG_WAIT); + if (err) { + dmu_tx_abort(tx); + return (err); + } + + mutex_enter(&zfsvfs->z_lock); + if (*objp == 0) { + *objp = zap_create(zfsvfs->z_os, DMU_OT_USERGROUP_QUOTA, + DMU_OT_NONE, 0, tx); + VERIFY(0 == zap_add(zfsvfs->z_os, MASTER_NODE_OBJ, name, + 8, 1, objp, tx)); + } + mutex_exit(&zfsvfs->z_lock); + + if (quota == 0) { + err = zap_remove(zfsvfs->z_os, *objp, name, tx); + if (err == ENOENT) + err = 0; + } else { + err = zap_update(zfsvfs->z_os, *objp, name, + 8, 1, "a, tx); + } + + ASSERT(err == 0); + dmu_tx_commit(tx); + return (err); +} + boolean_t zfs_id_overobjquota(zfsvfs_t *zfsvfs, uint64_t usedobj, uint64_t id) { @@ -423,8 +476,9 @@ boolean_t zfs_id_overblockquota(zfsvfs_t *zfsvfs, uint64_t usedobj, uint64_t id) { char buf[20]; - uint64_t used, quota, quotaobj; - int err; + uint64_t used, quota, quotaobj, defquota, defquotaobj; + int err, uerr, derr; + const char *name; if (usedobj == DMU_PROJECTUSED_OBJECT) { if (!dmu_objset_projectquota_present(zfsvfs->z_os)) { @@ -440,22 +494,46 @@ zfs_id_overblockquota(zfsvfs_t *zfsvfs, uint64_t usedobj, uint64_t id) quotaobj = zfsvfs->z_projectquota_obj; } else if (usedobj == DMU_USERUSED_OBJECT) { quotaobj = zfsvfs->z_userquota_obj; + defquotaobj = zfsvfs->z_defaultuserquota_obj; + name = zfs_prop_to_name(ZFS_PROP_DEFAULTUSERQUOTA); } else if (usedobj == DMU_GROUPUSED_OBJECT) { quotaobj = zfsvfs->z_groupquota_obj; + defquotaobj = zfsvfs->z_defaultgroupquota_obj; + name = zfs_prop_to_name(ZFS_PROP_DEFAULTGROUPQUOTA); } else { return (B_FALSE); } - if (quotaobj == 0 || zfsvfs->z_replay) + + /* no quota assigned */ + if (quotaobj == 0 && defquotaobj == 0) { return (B_FALSE); + } + if (zfsvfs->z_replay) { + return (B_FALSE); + } (void) snprintf(buf, sizeof (buf), "%llx", (longlong_t)id); - err = zap_lookup(zfsvfs->z_os, quotaobj, buf, 8, 1, "a); - if (err != 0) + derr = zap_lookup(zfsvfs->z_os, defquotaobj, name, + 8, 1, &defquota); + uerr = zap_lookup(zfsvfs->z_os, quotaobj, buf, 8, 1, "a); + + /* bail if both id-specific && default lookups failed */ + if (uerr != 0 && derr != 0) return (B_FALSE); err = zap_lookup(zfsvfs->z_os, usedobj, buf, 8, 1, &used); if (err != 0) return (B_FALSE); + + /* + * if a user/group has a specific quota assigned, use that. + * if neither quota...we've already returned false (hopefully). + * if a default quota is set, but no user quota is, + * use the default. + */ + if (uerr != 0 && derr == 0 && defquota) + quota = defquota; + return (used >= quota); } @@ -470,6 +548,7 @@ EXPORT_SYMBOL(zpl_get_file_info); EXPORT_SYMBOL(zfs_userspace_one); EXPORT_SYMBOL(zfs_userspace_many); EXPORT_SYMBOL(zfs_set_userquota); +EXPORT_SYMBOL(zfs_set_defaultquota); EXPORT_SYMBOL(zfs_id_overblockquota); EXPORT_SYMBOL(zfs_id_overobjquota); EXPORT_SYMBOL(zfs_id_overquota);