From 1e2e210f9990ed77018139395094e92366b3af60 Mon Sep 17 00:00:00 2001 From: Tarashish Mishra Date: Wed, 18 Sep 2024 20:13:49 +0530 Subject: [PATCH] initialize projects explicitly during start up --- get-quota-your-home/generate.py | 141 +++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 48 deletions(-) diff --git a/get-quota-your-home/generate.py b/get-quota-your-home/generate.py index 32f631f..ed0569a 100755 --- a/get-quota-your-home/generate.py +++ b/get-quota-your-home/generate.py @@ -37,7 +37,10 @@ import itertools # Line at beginning of projid / projects file stating ownership -OWNERSHIP_PREAMBLE = "# This file is generated by get-quota-your-home. Do not modify by hand\n" +OWNERSHIP_PREAMBLE = ( + "# This file is generated by get-quota-your-home. Do not modify by hand\n" +) + def parse_projids(path): """ @@ -47,36 +50,44 @@ def parse_projids(path): if os.path.exists(path): with open(path) as f: for line in f: - if line.strip().startswith('#'): + if line.strip().startswith("#"): continue - splits = line.split(':', 2) + splits = line.split(":", 2) projects[splits[0]] = int(splits[1]) return projects + def mountpoint_for(path): """ Return mount point containing file / directory in path xfs_quota wants to know which fs to operate on """ - return subprocess.check_output(['df', '--output=target', path]).decode().strip().split('\n')[-1].strip() + return ( + subprocess.check_output(["df", "--output=target", path]) + .decode() + .strip() + .split("\n")[-1] + .strip() + ) def get_quotas(): - output = subprocess.check_output([ - 'xfs_quota', '-x', '-c', - 'report -N -p' - ]).decode().strip() + output = ( + subprocess.check_output(["xfs_quota", "-x", "-c", "report -N -p"]) + .decode() + .strip() + ) quotas = {} - for line in output.split('\n'): + for line in output.split("\n"): path, used, soft, hard, warn, grace = line.split() # Everything here is in kb, since that's what xfs_quota reports things in quotas[path] = { - 'used': int(used), - 'soft': int(soft), - 'hard': int(hard), - 'warn': int(warn), - 'grace': grace + "used": int(used), + "soft": int(soft), + "hard": int(hard), + "warn": int(warn), + "grace": grace, } return quotas @@ -90,7 +101,9 @@ def reconcile_projfiles(paths, projects_file_path, projid_file_path, min_projid) """ # Fetch existing home directories # Sort to provide consistent ordering across runs - homedirs = sorted([ent.path for path in paths for ent in os.scandir(path) if ent.is_dir()]) + homedirs = sorted( + [ent.path for path in paths for ent in os.scandir(path) if ent.is_dir()] + ) print(homedirs) # Fetch list of projects in /etc/projid file, assumed to sync'd to /etc/projects file @@ -111,21 +124,36 @@ def reconcile_projfiles(paths, projects_file_path, projid_file_path, min_projid) if home not in projects: projects[home] = max(projects.values() or [min_projid]) + 1 projid_file_dirty = True - print(f'Found new project {home}') + print(f"Found new project {home}") # Remove projects that don't have corresponding homedirs projects = {k: v for k, v in projects.items() if k in homedirs} - # FIXME: make this an atomic write - with open(projects_file_path, 'w') as projects_file, open(projid_file_path, 'w') as projid_file: + with open(projects_file_path, "w") as projects_file, open( + projid_file_path, "w" + ) as projid_file: projects_file.write(OWNERSHIP_PREAMBLE) projid_file.write(OWNERSHIP_PREAMBLE) for path, id in projects.items(): - projid_file.write(f'{path}:{id}\n') - projects_file.write(f'{id}:{path}\n') + projid_file.write(f"{path}:{id}\n") + projects_file.write(f"{id}:{path}\n") + + print(f"Writing /etc/projid and /etc/projects") + + +def initialize_projects(projid_file_path): + """ + Set up xfs_quota projects for all projects in /etc/projid + """ + projects = parse_projids(projid_file_path) + for project in projects: + mountpoint = mountpoint_for(project) + subprocess.check_call( + ["xfs_quota", "-x", "-c", f"project -s {project}", mountpoint] + ) + print(f"Setting up xfs_quota project for {project}") - print(f'Writing /etc/projid and /etc/projects') def reconcile_quotas(projid_file_path, hard_quota_kb): """ @@ -138,55 +166,72 @@ def reconcile_quotas(projid_file_path, hard_quota_kb): quotas = get_quotas() # Check for projects that don't have any nor correct quota - changed_projects = [p for p in projects if quotas.get(p, {}) .get('hard') != hard_quota_kb] + changed_projects = [ + p for p in projects if quotas.get(p, {}).get("hard") != hard_quota_kb + ] # Make sure xfs_quotas is in sync if changed_projects: for project in changed_projects: mountpoint = mountpoint_for(project) if project not in quotas: - subprocess.check_call([ - 'xfs_quota', '-x', '-c', - f'project -s {project}', - mountpoint - ]) - print(f'Setting up xfs_quota project for {project}') - if project not in quotas or quotas[project]['hard'] != hard_quota_kb: - subprocess.check_call([ - 'xfs_quota', '-x', '-c', - f'limit -p bhard={hard_quota_kb}k {project}', - mountpoint - ]) - print(f'Setting limit for project {project} to {hard_quota_kb}k') - + subprocess.check_call( + ["xfs_quota", "-x", "-c", f"project -s {project}", mountpoint] + ) + print(f"Setting up xfs_quota project for {project}") + if project not in quotas or quotas[project]["hard"] != hard_quota_kb: + subprocess.check_call( + [ + "xfs_quota", + "-x", + "-c", + f"limit -p bhard={hard_quota_kb}k {project}", + mountpoint, + ] + ) + print(f"Setting limit for project {project} to {hard_quota_kb}k") def main(): argparser = argparse.ArgumentParser() - argparser.add_argument('paths', nargs='+', - help='Paths to scan for home directories' + argparser.add_argument( + "paths", nargs="+", help="Paths to scan for home directories" ) - argparser.add_argument('--projects-file', default='/etc/projects') - argparser.add_argument('--projid-file', default='/etc/projid') - argparser.add_argument('--min-projid', default=1000, type=int, - help="Project IDs will be generated starting from this number" + argparser.add_argument("--projects-file", default="/etc/projects") + argparser.add_argument("--projid-file", default="/etc/projid") + argparser.add_argument( + "--min-projid", + default=1000, + type=int, + help="Project IDs will be generated starting from this number", ) - argparser.add_argument('--wait-time', default=30, type=int, - help='Number of seconds to wait between runs' + argparser.add_argument( + "--wait-time", + default=30, + type=int, + help="Number of seconds to wait between runs", ) - argparser.add_argument('--hard-quota', default=10, type=float, - help='Hard quota limit (in GB) to set for all home directories' + argparser.add_argument( + "--hard-quota", + default=10, + type=float, + help="Hard quota limit (in GB) to set for all home directories", ) args = argparser.parse_args() # xfs_quota reports in kb hard_quota_kb = int(args.hard_quota * 1024 * 1024) + initialize_projects(args.projid_file) + while True: - reconcile_projfiles(args.paths, args.projects_file, args.projid_file, args.min_projid) + reconcile_projfiles( + args.paths, args.projects_file, args.projid_file, args.min_projid + ) reconcile_quotas(args.projid_file, hard_quota_kb) time.sleep(args.wait_time) -if __name__ == '__main__': + +if __name__ == "__main__": main()