Skip to content
Snippets Groups Projects
quota.py 4.99 KiB
"""mpsd-quota command line tool.

Provides the current use of storage for a user on /home and /scratch .

Options are available to

- display this for another user
- display in units of bytes (for debugging)
- show the version of the mpsd_hpc_tools package
- show some additional output

"""


import argparse
import os
import pathlib
import subprocess
import sys
import textwrap
from typing import Tuple
from mpsd_hpc_tools.lib import humanise_size
from mpsd_hpc_tools import __version__


def get_ceph_attribute(path: str, attribute: str) -> int:
    """Return output of 'getfattr' command.

    Parameters
    ----------
    path : str
        path to ceph managed volume, for example '/scratch/fangohr'
    attribute : str
        getfattrs attribute, for example 'ceph.dir.rbytes' or
        'ceph.quota.max_bytes'

    Returns
    -------
    value : int
        value returned by getfattrs command (only integers support)
    """
    # getfattr command should return a single integer number
    cmd = ["getfattr", "-n", attribute, "--only-values", "--absolute-names", path]
    output = subprocess.check_output(cmd).decode()
    return int(output)


def df_ceph(path: pathlib.Path) -> Tuple[int, int, int]:
    """
    Given a path (such as '/scratch/fangohr') return the number of bytes
    (size, used, available).

    Assumes that path_obj is provided by ceph.

    Example:

    >>> df_ceph("/scratch/fangohr")
    (25000000000000, 1780686359, 24998219313641)
    """
    try:
        used = get_ceph_attribute(path, "ceph.dir.rbytes")
        size = get_ceph_attribute(path, "ceph.quota.max_bytes")
    except Exception:
        used, size, avail = None, None, None
    else:
        avail = size - used
    return size, used, avail


def df_mountpoint(path: pathlib.Path) -> Tuple[int, int, int]:
    """
    Given a path (such as '/home/fangohr') return the number of bytes
    (size, used, available).

    Assumes that path_obj is a distinct mountpoint.

    Example:

    >>> df("/home/fangohr")
    (25000000000000, 1780686359, 24998219313641)
    """
    size, used, avail = None, None, None
    cmd = ["df", "--block-size=1", path]
    try:
        output = subprocess.check_output(cmd).decode()
    except Exception:
        return None, None, None
    for line in output.splitlines():
        df_fields = line.split()
        if df_fields[0] == "Filesystem":
            continue
        (size, used, avail) = [int(df_field) for df_field in df_fields[1:4]]
    return size, used, avail


def print_quota_line(location, size, used, avail, rel=None, raw=False):
    format_string = "{:20} {:>14} {:>14} {:>14} {:>14.4}%\n"
    if size is None:
        return
    if rel is None:
        rel = 100.0 * used / size
    if not raw:
        size, used, avail = map(humanise_size, [size, used, avail])
    sys.stdout.write(format_string.format(str(location), used, avail, size, rel))


def main(argv=None):
    """Main function for mpsd-quota command.

    Parameters
    ----------

    argv : List[str] or None
        Can provide list of arguments (reflecting sys.argv) for testing purposes
        The default value (None) will result in `sys.argv` being used.
    """
    parser = argparse.ArgumentParser(
        prog="mpsd-quota",
        description="Shows current usage of /home and /scratch",
        epilog=textwrap.dedent(
            """
            Output of mpsd-quota is in multiples of 1000 (i.e. decimal
            prefixes) and not 1024 (i.e. binary prefixes). If you want to
            compare output from mpsd-quota with output from "df -h", then
            use "df -h --si" to enforce use of decimal prefixes by df.
            """
        ),
    )

    # if executed on CI, "USER" may not be defined in environment. Then use
    # "UNKNOWN":
    userdefault = os.environ.get("USER", "UNKNOWN")

    parser.add_argument(
        type=str,
        default=userdefault,
        dest="user",
        nargs="?",
        help=f'quota for which user (by default "{userdefault}")',
    )
    parser.add_argument(
        "--version", "-V", help="display version and exit", action="store_true"
    )
    parser.add_argument(
        "--bytes",
        help="display storage in bytes (default: human readable)",
        action="store_true",
    )

    # if argv==None, then parse_args will use sys.argv (normal scenario) if we
    # are testing `main(argv)` we can parse custom arguments into `argv` (for
    # testing)
    args = parser.parse_args(argv)

    if args.version:
        print(__version__)
        sys.exit(0)

    # by default the user executing the command, or user provided on command line
    user = args.user

    # check user exists
    # print header
    print_quota_line("#location", "quota", "used", "avail", "use", raw=True)
    # print home quota
    homedir = pathlib.Path("/home") / user
    print_quota_line(homedir, *df_mountpoint(homedir), raw=args.bytes)

    # print scratch quota
    scratchdir = pathlib.Path("/scratch") / user
    print_quota_line(scratchdir, *df_ceph(scratchdir), raw=args.bytes)


if __name__ == "__main__":
    main()