file_inventory.py #1

  • //
  • guest/
  • russell_jackson/
  • sdp/
  • Maintenance/
  • file_inventory.py
  • View
  • Commits
  • Open Download .zip Download (6 KB)
#!/usr/bin/env python3
"""Stream a complete depot inventory and atomically publish its pathname list.

Uses the p4 CLI's marshalled output; P4Python is not required. One query covers
files directly at each depot root, then one query per top-level directory.
The output retains the existing format: one depotFile pathname per line,
including deleted head revisions reported by `p4 files`.
"""

import argparse
import logging
import marshal
import os
from pathlib import Path
import stat
import subprocess
import sys
import tempfile


class InventoryError(RuntimeError):
    def __init__(self, message, returncode=1):
        super().__init__(message)
        self.returncode = returncode if returncode > 0 else 1


def _text(value):
    return value.decode("utf-8", "surrogateescape") if isinstance(value, bytes) else str(value)


def records(binary, args, allow_empty=False):
    """Yield stat records while distinguishing empty queries from failures."""
    with tempfile.TemporaryFile() as diagnostic:
        process = subprocess.Popen([binary, "-G", "-ztag", *args],
                                   stdout=subprocess.PIPE, stderr=diagnostic)
        errors = []
        empty = False
        try:
            # peek distinguishes a clean EOF from a truncated marshalled record.
            while process.stdout.peek(1):
                try:
                    raw = marshal.load(process.stdout)
                except (EOFError, ValueError, TypeError) as exc:
                    raise InventoryError("Invalid or truncated p4 output for " + args[0]) from exc
                if not isinstance(raw, dict):
                    raise InventoryError("Expected p4 dictionary output for " + args[0])
                rec = {_text(key): value for key, value in raw.items()}
                code = _text(rec.get("code", ""))
                if code == "stat":
                    yield rec
                elif code == "error":
                    generic = int(rec.get("generic", 0))
                    severity = int(rec.get("severity", 3))
                    # Perforce EV_EMPTY (0x11) means the query had no matches.
                    # Permission, connection and resource-limit errors never qualify.
                    if allow_empty and generic == 17 and severity <= 2:
                        empty = True
                    else:
                        errors.append(_text(rec.get("data", "p4 query failed")).strip())
                elif code in ("info", "text"):
                    detail = _text(rec.get("data", "")).strip()
                    if detail:
                        logging.info("%s", detail)
                else:
                    raise InventoryError("Unexpected p4 record type: " + code)
            returncode = process.wait()
            diagnostic.seek(0)
            stderr = diagnostic.read().decode("utf-8", "replace").strip()
            if stderr:
                logging.warning("%s", stderr)
            if errors or (returncode != 0 and not (empty and returncode == 1)):
                detail = "; ".join(errors) or stderr or "p4 query failed"
                raise InventoryError(detail, returncode)
        finally:
            process.stdout.close()
            if process.poll() is None:
                process.terminate()
                process.wait()


def _field(record, name):
    if name not in record:
        raise InventoryError("p4 inventory record is missing " + name)
    value = _text(record[name])
    if not value or "\n" in value or "\r" in value:
        raise InventoryError("Inventory names must fit on one line")
    return value


def create_inventory(binary, output):
    output = Path(output)
    temporary = None
    count = 0
    try:
        with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", errors="surrogateescape",
                                         newline="\n", dir=str(output.parent),
                                         prefix="." + output.name + ".", delete=False) as stream:
            temporary = Path(stream.name)
            for depot in records(binary, ["depots"], allow_empty=True):
                depot_path = "//" + _field(depot, "name")
                logging.info("Scanning %s", depot_path)
                # '*' never crosses '/', so these cannot overlap child scans.
                for rec in records(binary, ["files", depot_path + "/*"], allow_empty=True):
                    stream.write(_field(rec, "depotFile") + "\n")
                    count += 1
                for directory in records(binary, ["dirs", "-D", depot_path + "/*"], allow_empty=True):
                    directory_path = _field(directory, "dir").rstrip("/")
                    for rec in records(binary, ["files", directory_path + "/..."], allow_empty=True):
                        stream.write(_field(rec, "depotFile") + "\n")
                        count += 1
            stream.flush()
            os.fsync(stream.fileno())
        if output.exists():
            temporary.chmod(stat.S_IMODE(output.stat().st_mode))
        os.replace(temporary, output)
        temporary = None
    finally:
        if temporary is not None:
            temporary.unlink()
    logging.info("Published %d paths to %s", count, output)
    return count


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--p4", required=True, help="Configured SDP p4 executable")
    parser.add_argument("--output", default="p4_file_list.txt")
    args = parser.parse_args(argv)
    logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
    try:
        create_inventory(args.p4, args.output)
    except InventoryError as exc:
        logging.error("%s", exc)
        return exc.returncode
    except (OSError, ValueError) as exc:
        logging.error("Unable to create file inventory: %s", exc)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
# Change User Description Committed
#1 34004 Russell C. Jackson (Rusty) Improve SDP backup/recovery, Maintenance, logging and cloud operation.

Add owned checkpoint locks, recovery-aware retention, preserved database switches,
bounded cleanup and coordinated AWS/Azure snapshots. Fix service-user password
initialization, edge staging, standby promotion state parsing, proxy cache setup,
typed depot verification and historical-server configuration/error handling.
Remove unused OTP programs; keep Windows removed, server.locks cleanup and the
journal-count condition unchanged. Add no global shell strict modes.

Update operational documentation and add reproducible mkdirs.sh/mkdirs.cfg
container assurance. Validate Ubuntu 26.04 and UBI 8.10/9.8/10.2 across both Python
runtimes: 44 stages; 428 offline tests per runtime/OS, zero offline skips;
19 deployed-archive comparisons per combination. Three six-node topology runs
pass all 30 recovery/failure scenarios. P4D 2023.1 through 2026.1 on UBI 8/9
pass all 14 cells and 98 scenarios. Static, documentation and provenance checks
pass. Containers do not certify every fleet kernel, patch build or integration.

Tested source manifest SHA-256:
e1a23e17b8ced8c1148d9a250589977c6f6e98606bf2a81dc3ff5d7c909e3695
Deployed comparison archive SHA-256:
1bd23217356ee3808e040d39aea5be9519bda7c8f2b4924656a70aff8ad7293a