#!/usr/bin/env python3 #============================================================================== # Idempotently set the resource-limit fields (and ensure an owner) on a Perforce # group, preserving every other field (Users, Subgroups, existing Owners). # # Reads the group spec via the SDP wrapper, changes only the requested fields, # and runs "group -i" ONLY when something actually differs. Prints "changed" or # "unchanged" on stdout so Ansible can drive changed_when from it. Membership of # the limits group is handled separately by the SDP's update_limits.py. # # Usage: # manage_p4group.py set-limits [--owner USER] \ # --set Field=Value [--set Field=Value ...] # # e.g. manage_p4group.py set-limits 1 limits --owner p4admin \ # --set MaxResults=500000 --set MaxScanRows=4500000 \ # --set MaxLockTime=90000 --set MaxMemory=4096 --set MaxOpenFiles=50000 # # Pass one --set per limit field; the Ansible task generates a --set for every # field in the group's host_vars map, so all of them are applied. A value of # "unlimited" (or "unset") is passed through verbatim, so the same command sets # an unlimited override group. #============================================================================== import argparse import re import subprocess import sys # Single-valued limit fields this tool manages. Anything else in the spec # (Users, Subgroups, Owners, Description, ...) is left untouched. LIMIT_FIELDS = ( "MaxResults", "MaxScanRows", "MaxLockTime", "MaxMemory", "MaxOpenFiles", "Timeout", "PasswordTimeout", ) # List fields, in the order p4 emits them; a new single field is inserted before # the first of these so the form stays well-formed. LIST_FIELDS = ("Subgroups", "Owners", "Users") def p4(instance, args, stdin=None): """Run one p4 command through the SDP wrappers, as update_limits.py does.""" cmd = ["/p4/common/bin/p4master_run", instance, "/p4/{0}/bin/p4_{0}".format(instance)] + args proc = subprocess.run(cmd, input=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) if proc.returncode != 0: sys.stderr.write(proc.stderr) sys.exit(proc.returncode or 1) return proc.stdout def parse_spec(spec): """Return (single_field_values, owners_list) from a p4 group form.""" values = {} owners = [] in_owners = False for line in spec.splitlines(): m = re.match(r"^(\w+):\s*(.*)$", line) if m: in_owners = (m.group(1) == "Owners") val = m.group(2).strip() if val: values[m.group(1)] = val elif in_owners and line[:1] in ("\t", " ") and line.strip(): owners.append(line.strip()) return values, owners def set_field(lines, field, value): """Replace (or insert) a single-valued field line.""" new_line = "{0}:\t{1}".format(field, value) for i, line in enumerate(lines): if re.match(r"^{0}:".format(re.escape(field)), line): lines[i] = new_line return insert_at = len(lines) for i, line in enumerate(lines): if re.match(r"^({0}):".format("|".join(LIST_FIELDS)), line): insert_at = i break lines.insert(insert_at, new_line) lines.insert(insert_at + 1, "") def ensure_owner(lines, owner): """Ensure `owner` is listed under Owners, creating the section if needed.""" for i, line in enumerate(lines): if re.match(r"^Owners:", line): j = i + 1 existing = [] while j < len(lines) and lines[j][:1] in ("\t", " ") and lines[j].strip(): existing.append(lines[j].strip()) j += 1 if owner not in existing: lines.insert(j, "\t{0}".format(owner)) return insert_at = len(lines) for i, line in enumerate(lines): if re.match(r"^Users:", line): insert_at = i break lines.insert(insert_at, "Owners:") lines.insert(insert_at + 1, "\t{0}".format(owner)) lines.insert(insert_at + 2, "") def cmd_set_limits(args): desired = {} for pair in args.set or []: if "=" not in pair: sys.stderr.write("Error: --set expects Field=Value, got '%s'\n" % pair) sys.exit(2) field, value = pair.split("=", 1) if field not in LIMIT_FIELDS: sys.stderr.write("Error: '%s' is not a managed limit field %s\n" % (field, LIMIT_FIELDS)) sys.exit(2) desired[field] = value.strip() spec = p4(args.instance, ["group", "-o", args.group]) current, owners = parse_spec(spec) changed = any(current.get(f) != v for f, v in desired.items()) if args.owner and args.owner not in owners: changed = True if not changed: print("unchanged") return lines = spec.splitlines() for field, value in desired.items(): set_field(lines, field, value) if args.owner: ensure_owner(lines, args.owner) p4(args.instance, ["group", "-i"], stdin="\n".join(lines) + "\n") print("changed") def main(): parser = argparse.ArgumentParser(description=__doc__) sub = parser.add_subparsers(dest="command", required=True) sl = sub.add_parser("set-limits", help="set limit fields on a group") sl.add_argument("instance") sl.add_argument("group") sl.add_argument("--owner", help="ensure this user is a group owner") sl.add_argument("--set", action="append", metavar="Field=Value", help="limit field to set (repeatable)") sl.set_defaults(func=cmd_set_limits) args = parser.parse_args() args.func(args) if __name__ == "__main__": main()