#!/usr/bin/env python3 """ Remove one or more Perforce users and their associated clients and shelves using the P4Python API. This script accepts either a single username or a file containing a list of usernames (one per line). For each user, it uses the P4Python API to run ``p4 user -FDy``, which deletes the user along with any clients and shelves they own. The script respects a list of service accounts that should never be removed. """ import argparse import logging import sys from pathlib import Path from typing import Iterable, List from P4 import P4, P4Exception # type: ignore import sdputils # type: ignore # Users that should never be deleted. SKIP_USERS = {"p4admin", "perforce", "swarm"} def parse_arguments(argv: Iterable[str]) -> argparse.Namespace: parser = argparse.ArgumentParser( description="Remove Perforce users and their associated clients/shelves via P4Python", ) parser.add_argument( "instance", nargs="?", default="1", help="SDP instance identifier (defaults to '1')", ) parser.add_argument( "user_or_file", help="Username to remove or path to a file containing one username per line", ) parser.add_argument( "--simulate", action="store_true", help="Simulation mode: print actions without deleting users", ) return parser.parse_args(list(argv)) class P4UserRemoverAPI: """Helper to manage user deletion using the P4Python API.""" def __init__(self, instance: str) -> None: self.instance = instance # Reuse the maintenance.cfg parsing from sdputils utils = sdputils.SDPUtils(instance) self.port = utils.server self.user = utils.p4user if hasattr(utils, "p4user") else utils.p4.split()[utils.p4.split().index("-u") + 1] # Create the P4 object self.p4 = P4() self.p4.port = self.port self.p4.user = self.user # Configure password if available passwd_file = Path(f"/p4/common/config/.p4passwd.p4_{instance}.admin") self.passwd: str | None = None if passwd_file.is_file(): with passwd_file.open() as pf: self.passwd = pf.read().strip() self.login_done = False def connect_and_login(self) -> None: try: self.p4.connect() if self.passwd: self.p4.password = self.passwd self.p4.run_login() self.login_done = True except P4Exception as exc: logging.error("Failed to connect/login to %s: %s", self.port, exc) raise def delete_user(self, user: str, simulate: bool = False) -> None: if user.lower() in SKIP_USERS: logging.info("Skipping protected user %s", user) return logging.info("Deleting user: %s", user) if simulate: logging.info("Simulation mode: would run 'user -FDy %s'", user) return try: # run('user', '-FDy', user) deletes the user and associated metadata self.p4.run("user", "-FDy", user) logging.debug("Successfully deleted user %s", user) except P4Exception as exc: logging.error("Failed to delete user %s: %s", user, exc) def read_user_list(path: Path) -> List[str]: users: List[str] = [] with path.open() as infile: for line in infile: username = line.strip() if username and not username.startswith("#"): users.append(username) return users def main(argv: Iterable[str] | None = None) -> int: args = parse_arguments(argv or sys.argv[1:]) logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") remover = P4UserRemoverAPI(args.instance) # Connect and login once at the beginning try: remover.connect_and_login() except P4Exception: return 1 # Determine user list user_or_file = Path(args.user_or_file) try: if user_or_file.is_file(): user_list = read_user_list(user_or_file) else: user_list = [args.user_or_file] except FileNotFoundError: logging.error("File '%s' not found; treating argument as a single username.", user_or_file) user_list = [args.user_or_file] # Delete each user for user in user_list: remover.delete_user(user, simulate=args.simulate) return 0 if __name__ == "__main__": sys.exit(main())