manage_p4group.py #1

  • //
  • guest/
  • russell_jackson/
  • ansible-sdp/
  • roles/
  • perforce-sdp-install/
  • files/
  • manage_p4group.py
  • View
  • Commits
  • Open Download .zip Download (6 KB)
#!/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.