#!/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 <instance> <group> [--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()
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #1 | 32894 | Russell C. Jackson (Rusty) |
Add group resource guardrails to perforce-sdp-install; inventory cleanup Guardrails (gated on guardrails_enabled, commit server): - New group_limits phase (thin orchestrator + group_limits/ subdir: deploy_scripts, set_limits, populate) included from main.yml. Templates the SDP keep_group_unset.py trigger and update_limits.py from inventory so no group/user name is hardcoded; sets Max* on each managed group via the new idempotent files/manage_p4group.py helper; populates the limits group once (marker-guarded). No new cron -- the SDP's hourly master cron maintains membership. - Every group name is its own variable (p4_baseline_group, p4_integrators_group, p4_unlimited_group, p4_admins_group, p4_replicas_group); p4_group_limits is a list of {name, limits}. Naming convention in group_vars/main; per-environment values + enable flag in each commit server's host_vars; safe fallbacks in defaults. - Molecule: install scenario now runs the guardrails phase after configure_new_server and verifies the scripts are deployed and the limits/p4-unlimited groups carry the expected values. README + docs updated. Inventory cleanup: - Removed the 2kgla1tst2 and la1v-rjackson commit hosts (host_vars + inventory). - Renamed the top-level inventory group central -> main (group_vars dir moved, references updated across inventory, molecule, README, CLAUDE.md). ansible-lint production-clean; playbooks syntax-check; helper/templates compile. |