Mini Shell
"""Filesystem quota functions"""
import os
import errno
import pwd
from typing import Union
import ctypes
import enum
import psutil
class QuotaError(OSError):
"""Base exception for errors raised running quotactl.
Subclasses read errno as defined in:
https://man7.org/linux/man-pages/man2/quotactl.2.html"""
__module__ = 'rads'
class QuotasDisabled(QuotaError, ProcessLookupError):
# ProcessLookupError is the usual Python OSError on errno.ESRCH
"""No disk quota is found for the indicated user. Quotas have
not been turned on for this filesystem"""
__module__ = 'rads'
class QuotaNoUser(QuotaError, KeyError):
"""Raised when trying to fetch quota for a non-existing user"""
__module__ = 'rads'
class VFSDqblk(ctypes.Structure):
"""dqblk struct for VFS quota data, as defined in sys/quota.h"""
_fields_ = [
("bhardlimit", ctypes.c_uint64), # Absolute limit on disk blocks
("bsoftlimit", ctypes.c_uint64), # Preferred limit on disk blocks
("curspace", ctypes.c_uint64), # Current occupied space (bytes)
("ihardlimit", ctypes.c_uint64), # Maximum number of inodes
("isoftlimit", ctypes.c_uint64), # Preferred inode limit
("curinodes", ctypes.c_uint64), # Current number of inodes
("btime", ctypes.c_uint64), # Time limit for excessive disk use
("itime", ctypes.c_uint64), # Time limit for excessive files
("valid", ctypes.c_uint32), # Bit mask of QIF constants
]
class XfsDiskQuota(ctypes.Structure):
"""fs_disk_quota struct for XFS quota data, as defined in xfs/xqm.h"""
# All the blk units are in BBs (Basic Blocks) of 512 bytes
_fields_ = [
("version", ctypes.c_int8), # Version of this structure
("flags", ctypes.c_int8), # XFS_{USER,PROJ,GROUP}_QUOTA
("fieldmask", ctypes.c_uint16), # Field specifier
("id", ctypes.c_uint32), # User, project, or group ID
("blk_hardlimit", ctypes.c_uint64), # Absolute limit on disk blocks
("blk_softlimit", ctypes.c_uint64), # Preferred limit on disk blocks
("ino_hardlimit", ctypes.c_uint64), # Maximum allocates inodes
("ino_softlimit", ctypes.c_uint64), # Preferred inode limit
("bcount", ctypes.c_uint64), # disk blocks owned by the user
("icount", ctypes.c_uint64), # inodes owned by the user
("itimer", ctypes.c_int32), # Zero if within inode limits
("btimer", ctypes.c_int32), # Similar to above, for disk blocks
("iwarns", ctypes.c_uint16), # warnings issued for inode count
("bwarns", ctypes.c_uint16), # warnings issued for block count
("padding2", ctypes.c_int32), # for future use
("rtb_hardlimit", ctypes.c_uint64), # hard limit on Realtime(RT) blocks
("rtb_softlimit", ctypes.c_uint64), # soft limit on RT blocks
("rtbcount", ctypes.c_uint64), # realtime blocks owned
("rtbtimer", ctypes.c_int32), # similar to above; for RT blocks
("rtbwarns", ctypes.c_uint16), # warnings for RT blocks
("padding3", ctypes.c_int16), # for future use
("padding4", ctypes.c_char * 8), # for future use
]
def xqm_cmd(cmd: int) -> int:
"""Mimics the XQM_CMD macro from quotaio_xfs.h:
#define XQM_CMD(cmd) ( ('X'<<8)+(cmd) )"""
return (ord('X') << 8) + cmd
class XfsCommands(enum.Enum):
"""Quotactl commands for XFS"""
Q_XQUOTAON = xqm_cmd(0x1) # enable quota accounting/enforcement
Q_XQUOTAOFF = xqm_cmd(0x2) # disable quota accounting/enforcement
Q_XGETQUOTA = xqm_cmd(0x3) # get disk limits & usage
Q_XSETQLIM = xqm_cmd(0x4) # set disk limits only
Q_XGETQSTAT = xqm_cmd(0x5) # returns fs_quota_stat_t struct
Q_XQUOTARM = xqm_cmd(0x6) # free quota files' space
Q_XGETNEXTQUOTA = xqm_cmd(0x9) # get disk limits and usage >= ID
class VfsCommands(enum.Enum):
"""Quotactl commands for VFS"""
USRQUOTA = 0
Q_SYNC = 0x800001 # sync disk copy of a filesystems quotas
Q_QUOTAON = 0x800002 # turn quotas on
Q_QUOTAOFF = 0x800003 # turn quotas off
Q_GETFMT = 0x800004 # get quota format used on given filesystem
Q_GETINFO = 0x800005 # get information about quota files
Q_SETINFO = 0x800006 # set information about quota files
Q_GETQUOTA = 0x800007 # get user quota structure
Q_SETQUOTA = 0x800008 # set user quota structure
Q_GETNEXTQUOTA = 0x800009 # get disk limits and usage >= ID
class QTypes(enum.Enum):
"""Quota type constants"""
# The dqblk_xfs.h header file defines its own XQM_USRQUOTA, XQM_GRPQUOTA,
# and XQM_PRJQUOTA constants for XFS, but they have the same value as below
USRQUOTA = 0
GRPQUOTA = 1
PRJQUOTA = 2
def qcmd(cmd: Union[VfsCommands, XfsCommands], qtype: QTypes):
"""Mimics the QCMD macro from quota.h:
#define QCMD(cmd, type) (((cmd) << SUBCMDSHIFT) | ((type) & SUBCMDMASK))"""
return (cmd.value << 8) | (qtype.value & 0x00FF)
class XfsQuotaFlags(enum.Enum):
"""Flags for XfsDiskQuota.flags"""
XFS_USER_QUOTA = 1 # User quota type
XFS_PROJ_QUOTA = 2 # Project quota type
XFS_GROUP_QUOTA = 4 # Group quota type
def blkdev(path: str):
"""Get the block device for the mount for a path
Args:
path: folder to get the block device for
Returns:
psutil._common.sdiskpart: device info
"""
path = os.path.realpath(path)
# /dev/vzfs will be filtered out on ovz if disk_partitions(all=False)
mounts = {
x.mountpoint: x
for x in psutil.disk_partitions(all=True)
if x.device.startswith('/')
}
while path != '/':
if path in mounts:
return mounts[path]
path = os.path.dirname(path)
return mounts[path]
blkdev.__module__ = 'rads'
class QuotaCtl:
"""Allows using the quotactl() syscall"""
__module__ = 'rads'
def __init__(self):
self.quotactl = ctypes.CDLL('libc.so.6', use_errno=True).quotactl
self.devices = {}
def getquota(self, user: Union[str, pwd.struct_passwd]) -> int:
"""Get the current usage of a user from the quotactl() syscall in bytes
Args:
user: username (or pwd struct) to fetch filesystem quota for
Raises:
QuotaNoUser: if you pass user as a str and the user does not exist
QuotasDisabled: if filesystem quotas are disabled
QuotaError: base exception for any other error
Returns:
disk usage of a user in bytes
"""
if not isinstance(user, pwd.struct_passwd):
try:
user = pwd.getpwnam(user)
except KeyError as exc:
raise QuotaNoUser(exc) from exc
if user.pw_dir in self.devices:
device_info = self.devices[user.pw_dir]
else:
device_info = blkdev(user.pw_dir)
self.devices[user.pw_dir] = device_info
dev = bytes(device_info.device, 'ascii')
if device_info.fstype == 'xfs':
mem = XfsDiskQuota()
cmd = qcmd(XfsCommands.Q_XGETQUOTA, QTypes.USRQUOTA)
else:
mem = VFSDqblk()
cmd = qcmd(VfsCommands.Q_GETQUOTA, QTypes.USRQUOTA)
if self.quotactl(cmd, dev, user.pw_uid, ctypes.byref(mem)) == -1:
# quotactl returned an error
err = ctypes.get_errno()
if err == errno.ESRCH:
raise QuotasDisabled('Filesystem quotas are not enabled')
try:
error = errno.errorcode[38]
except KeyError:
msg = f'quotactl failed for {user.pw_name} with errno {err}'
else:
msg = (
f'quotactl failed for {user.pw_name} with error {error}. '
'See https://man7.org/linux/man-pages/man2/quotactl.2.html'
)
raise QuotaError(err, msg)
if device_info.fstype == 'xfs':
return mem.bcount * 512
return mem.curspace
Zerion Mini Shell 1.0