Skip to content

Commit

Permalink
initialize projects explicitly during start up
Browse files Browse the repository at this point in the history
  • Loading branch information
sunu committed Sep 18, 2024
1 parent 9b8ef3a commit 1e2e210
Showing 1 changed file with 93 additions and 48 deletions.
141 changes: 93 additions & 48 deletions get-quota-your-home/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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):
"""
Expand All @@ -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()

0 comments on commit 1e2e210

Please sign in to comment.