swarm_rename_users.py #1

  • //
  • p4-sdp/
  • r26.1.0/
  • Unsupported/
  • Samples/
  • bin/
  • swarm_rename_users.py
  • View
  • Commits
  • Open Download .zip Download (17 KB)
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# ==============================================================================
# Copyright and license info is available in the LICENSE file included with
# the Server Deployment Package (SDP), and also available online:
# https://workshop.perforce.com/view/p4-sdp/main/LICENSE
# ------------------------------------------------------------------------------

"""
NAME:
    swarm_rename_users.py

DESCRIPTION:
    This script renames Swarm users. Expects parameter to be a file containing:

        old_username new_username   (separated by whitespace)

    You can run like this:

        python3 swarm_rename_users.py -i users.list

    To get usage flags and parameters:

        python3 swarm_rename_users.py -h

    It assumes your current environment settings are valid (P4PORT/P4USER/P4TICKET etc) although
    these can be specified via other parameters.

    It works by directly editing the "p4 keys" used by Swarm and updating the JSON values.
    Test harness is in tests/test_swarm_rename_users.py

    The following p4 keys are potentially updated:

        swarm-activity-*
        swarm-comment-*
        swarm-project-*
        swarm-review-*
        swarm-fileInfo-*
        swarm-workflow-*
        swarm-user-*

    This is supported only with a consulting SoW.

    Current version tested with Swarm 2020.2

    IMPORTANT: run this on a test system first - be careful !!!!!

    If a run is interrupted, then it is not dangerous to run the script again with the same
    set of users - it will just rename any found, ignoring those records where user has 
    already been renamed.

"""

# Python 2.7/3.3 compatibility.
from __future__ import print_function

import sys
import os
import textwrap
import argparse
import logging
import P4
import json
from collections import defaultdict
from six.moves import range
import six

script_name = os.path.basename(os.path.splitext(__file__)[0])

# Avoid utf encoding issues when writing to stdout
sys.stdout = os.fdopen(sys.stdout.buffer.fileno(), 'w', encoding='utf8')

LOGDIR = os.getenv('LOGS', '/p4/1/logs')

DEFAULT_LOG_FILE = "log-%s.log" % script_name
if os.path.exists(LOGDIR):
    DEFAULT_LOG_FILE = os.path.join(LOGDIR, "%s.log" % script_name)
DEFAULT_VERBOSITY = 'DEBUG'
LOGGER_NAME = 'P4Triggers'

class SwarmObject(object):
    "Wrapper around Swarm JSON"

    updated = False
    json = None

    def __init__(self, jsonStr, keyword=None):
        if keyword and keyword in jsonStr:
            self.json = jsonStr[keyword]
        else:
            self.json = jsonStr

    def getVal(self, k):
        if k in self.json:
            return self.json[k]
        return None

    def getKeys(self, k):
        d = self.getVal(k)
        if not d:
            return None
        results = []
        for u, _ in six.iteritems(d):
            results.append(u)
        return results

    def setVal(self, k, v):
        self.json[k] = v
        self.updated = True

    def updateVal(self, k, usersToRename):
        u = self.getVal(k)
        if u and u in usersToRename:
            self.setVal(k, usersToRename[u])

    def updateDictKeyVal(self, k, usersToRename):
        "Update 'key': {'u1': []}"
        if not self.json or not k in self.json or not self.json[k]:
            return
        users = []
        for u, _ in six.iteritems(self.json[k]):
            users.append(u)
        for u in users:
            if u in usersToRename:
                # Copy old entry to new name and delete old
                self.json[k][usersToRename[u]] = self.json[k][u]
                del self.json[k][u]
                self.updated = True

    def updateArrayVal(self, k, usersToRename):
        "Update 'key': [user array]"
        if not k in self.json or not self.json[k]:
            return
        for i, u in enumerate(self.json[k]):
            if u in usersToRename:
                self.json[k][i] = usersToRename[u]
                self.updated = True

    def updateArrayDictVal(self, k, k2, usersToRename):
        """ Entry is array of dicts with key
        project = {"k": [{"k2": "u1"}]}
        """
        if not k in self.json or not self.json[k]:
            return
        for i, _ in enumerate(self.json[k]):
            if k2 in self.json[k][i]:
                u = self.json[k][i][k2]
                if u in usersToRename:
                    self.json[k][i][k2] = usersToRename[u]
                    self.updated = True

    def updateDictArrayVal(self, k, k2, usersToRename):
        """ Entry is a dict with value as array
        project = {"k": {"k2": ["u1", "u2"]}}
        """
        if not k in self.json or not self.json[k] or not k2 in self.json[k]:
            return
        for i, u in enumerate(self.json[k][k2]):
            if u in usersToRename:
                self.json[k][k2][i] = usersToRename[u]
                self.updated = True

    def updateDictArrayDictVal(self, k, k2, usersToRename):
        """ Entry is an array of dicts with value as array
        project = {"k": [{"j": ["u1", "u2"]}]}
        """
        if not k in self.json or not self.json[k]:
            return
        for i, _ in enumerate(self.json[k]):
            if k2 in self.json[k][i]:
                for j, u in enumerate(self.json[k][i][k2]):
                    if u in usersToRename:
                        self.json[k][i][k2][j] = usersToRename[u]
                        self.updated = True

    def updateDictArrayDictDictVal(self, k, k2, k3, usersToRename):
        """ Entry is an array of dicts with value as a dict containing an array
        project = {"k": [{"k2": {"k3": ["u1", "u2"]}}]}
        """
        if not k in self.json or not self.json[k]:
            return
        for i, _ in enumerate(self.json[k]):
            if k2 in self.json[k][i]:
                if k3 in self.json[k][i][k2]:
                    for j, u in enumerate(self.json[k][i][k2][k3]):
                        if u in usersToRename:
                            self.json[k][i][k2][k3][j] = usersToRename[u]
                            self.updated = True

class Activity(SwarmObject):
    "Swarm Activity"

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'activity')

    def user(self):
        return self.getVal('user')

    def behalfOf(self):
        return self.getVal('behalfOf')

    def Update(self, usersToRename):
        self.updateVal("user", usersToRename)
        self.updateVal("behalfOf", usersToRename)

class Comment(SwarmObject):
    "Swarm Comment"

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'comment')

    def user(self):
        return self.getVal('user')

    def readBy(self):
        return self.getVal('readBy')

    def Update(self, usersToRename):
        self.updateVal("user", usersToRename)
        self.updateArrayVal("readBy", usersToRename)

class Project(SwarmObject):
    "Swarm Project"

    # 'members' - this is via group swarm-<project> and the Users: field

    # swarm-project-test = {
    # "name": "Test Swarm Project",
    # "defaults": {
    #     "reviewers": ["jim"]
    # },
    # "description": "Test for demo purposes",
    # "owners": [
    #     "fred",
    #     "bill"
    # ],
    # "moderators": [
    #     "fred",
    #     "bill"
    # ],

    # "branches": [
    #     {
    #         "id": "main",
    #         "name": "Main",
    #         "workflow": null,
    #         "paths": [
    #             "\/\/depot\/p4-test\/main\/..."
    #         ],
    #         "defaults": {
    #             "reviewers": []
    #         },
    #         "minimumUpVotes": null,
    #         "retainDefaultReviewers": false,
    #         "moderators": [],
    #         "moderators-groups": []
    #     },

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'project')

    def owners(self):
        return self.getVal('owners')

    def moderators(self):
        return self.getVal('moderators')

    def branches(self):
        return self.getVal('branches')

    def defaultReviewers(self):
        defaults = self.getVal('defaults')
        k = 'reviewers'
        if defaults and k in defaults:
            return defaults[k]
        return None

    def branchDefaultReviewers(self):
        branches = self.getVal('branches')
        if not branches:
            return None
        results = []
        for b in branches:
            k = 'defaults'
            j = 'reviewers'
            if b and k in b and j in b[k]:
                results.append({b['id']: b[k][j]})
        return results

    def branchModerators(self):
        branches = self.getVal('branches')
        if not branches:
            return None
        results = []
        for b in branches:
            k = 'moderators'
            if b and k in b:
                results.append({b['id']: b[k]})
        return results

    def Update(self, usersToRename):
        self.updateArrayVal("owners", usersToRename)
        self.updateArrayVal("moderators", usersToRename)
        self.updateDictArrayVal("defaults", "reviewers", usersToRename)
        self.updateDictArrayDictVal("branches", "moderators", usersToRename)
        self.updateDictArrayDictDictVal("branches", "defaults", "reviewers", usersToRename)

class Review(SwarmObject):
    "Swarm Review"

    # "author":"martin",
    # "approvals":{"jane":[1]},
    # "participants":{"martin":[],"jane":{"vote":{"value":1,"version":1}}
    # "versions": [{"user": "fred", "difference": 1}]

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'review')

    def author(self):
        return self.getVal('author')

    def approvals(self):
        return self.getKeys('approvals')

    def participants(self):
        return self.getKeys('participants')

    def versionUsers(self):
        d = self.getVal('versions')
        if not d:
            return None
        results = []
        for v in d:
            if 'user' in v:
                results.append(v['user'])
        return results

    def Update(self, usersToRename):
        self.updateVal("author", usersToRename)
        self.updateDictKeyVal("approvals", usersToRename)
        self.updateDictKeyVal("participants", usersToRename)
        self.updateArrayDictVal("versions", "user", usersToRename)

class FileInfo(SwarmObject):

    # swarm-fileInfo-1018362-468732cb9f10670c2599d6844a82cd36 = {"readBy":{
    #     "jim":{"version":28,"digest":"EEFF792157ADBB2311938D7358F0588B"},
    #     "mike":{"version":30,"digest":"1E43325205374FBBE0E72DAB5930F8DB"}

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'fileInfo')

    def readBy(self):
        return self.getKeys('readBy')

    def Update(self, usersToRename):
        try:
            self.updateDictKeyVal("readBy", usersToRename)
        except:
            print("Error process FileInfo: %s" % self.json)

class Workflow(SwarmObject):

    # swarm-workflow-fffffff5 = {"on_submit":{"with_review":{"rule":"approved","mode":"inherit"},"without_review":{"rule":"reject","mode":"inherit"}},
    # "owners":["swarm-group-dev","dave", "scott"],
    # "end_rules":{"update":{"rule":"no_checking","mode":"inherit"}},
    # "auto_approve":{"rule":"never","mode":"inherit"},
    # etc...

    def __init__(self, json):
        SwarmObject.__init__(self, json, 'workflow')

    def owners(self):
        return self.getVal('owners')

    def Update(self, usersToRename):
        self.updateArrayVal("owners", usersToRename)

class SwarmRenameUsers(object):
    """See module doc string for details"""

    def __init__(self, *args, **kwargs):
        self.parse_args(__doc__, args)

    def parse_args(self, doc, args):
        """Common parsing and setting up of args"""
        desc = textwrap.dedent(doc)
        parser = argparse.ArgumentParser(
            formatter_class=argparse.RawDescriptionHelpFormatter,
            description=desc,
            epilog="Copyright (c) 2021 Perforce Software, Inc."
        )
        self.add_parse_args(parser)     # Should be implemented by subclass
        self.options = parser.parse_args(args=args)
        self.init_logger()
        self.logger.debug("Command Line Options: %s\n" % self.options)

    def add_parse_args(self, parser, default_log_file=None, default_verbosity=None):
        """Default trigger arguments - common to all triggers
        :param default_verbosity:
        :param default_log_file:
        :param parser:
        """
        if not default_log_file:
            default_log_file = DEFAULT_LOG_FILE
        if not default_verbosity:
            default_verbosity = DEFAULT_VERBOSITY
        parser.add_argument('-p', '--port', default=None,
                            help="Perforce server port - set using %%serverport%%. Default: $P4PORT")
        parser.add_argument('-u', '--user', default=None, help="Perforce user. Default: $P4USER")
        parser.add_argument('-L', '--log', default=default_log_file, help="Default: " + default_log_file)
        parser.add_argument('-i', '--input', help="Name of a file containing a list of users: oldname newname")
        parser.add_argument('-v', '--verbosity',
                            nargs='?',
                            const="INFO",
                            default=default_verbosity,
                            choices=('DEBUG', 'WARNING', 'INFO', 'ERROR', 'FATAL'),
                            help="Output verbosity level. Default is: " + default_verbosity)

    def init_logger(self, logger_name=None):
        if not logger_name:
            logger_name = LOGGER_NAME
        self.logger = logging.getLogger(logger_name)
        self.logger.setLevel(self.options.verbosity)
        logformat = '%(levelname)s %(asctime)s %(filename)s %(lineno)d: %(message)s'
        logging.basicConfig(format=logformat, filename=self.options.log, level=self.options.verbosity)
        formatter = logging.Formatter('%(message)s')
        ch = logging.StreamHandler(sys.stderr)
        ch.setLevel(logging.INFO)
        ch.setFormatter(formatter)
        self.logger.addHandler(ch)

    def renameObject(self, p4, usersToRename, p4key, klassName, klass):
        updateCount = 0
        count = 0
        values = p4.run_keys("-e", p4key)
        self.logger.info("%s: %d" % (klassName, len(values)))
        for v in values:
            count += 1
            if count % 1000 == 0:
                self.logger.info("Progress %s: %d updated, %d so far" % (klassName, updateCount, count))
            obj = klass(json.loads(v['value']))
            obj.Update(usersToRename)
            if obj.updated:
                updateCount += 1
                p4.run_key(v['key'], json.dumps(obj.json, separators=(',', ':')))
        self.logger.info("%s Updated: %d" % (klassName, updateCount))

    def renameUserRecords(self, p4, usersToRename, p4key, klassName):
        updateCount = 0
        count = 0
        values = p4.run_keys("-e", p4key)
        users = {}
        for v in values:
            users[v['key']] = v['value']
        self.logger.info("%s: %d" % (klassName, len(values)))
        for v in values:
            count += 1
            if count % 100 == 0:
                self.logger.info("Progress %s: %d updated, %d so far" % (klassName, updateCount, count))
            u = v['key'].replace("swarm-user-", "")
            if u in usersToRename:
                updateCount += 1
                userID = "swarm-user-%s" % usersToRename[u]
                p4.run_key(userID, v['value'])
                p4.run_key('-d', v['key'])
        self.logger.info("%s Updated: %d" % (klassName, updateCount))

    def renameUsers(self, p4, usersToRename):
        self.renameObject(p4, usersToRename, "swarm-activity-*", "Activity", Activity)
        self.renameObject(p4, usersToRename, "swarm-comment-*", "Comment", Comment)
        self.renameObject(p4, usersToRename, "swarm-project-*", "Project", Project)
        self.renameObject(p4, usersToRename, "swarm-review-*", "Review", Review)
        self.renameObject(p4, usersToRename, "swarm-fileInfo-*", "FileInfo", FileInfo)
        self.renameObject(p4, usersToRename, "swarm-workflow-*", "Workflow", Workflow)
        self.renameUserRecords(p4, usersToRename, "swarm-user-*", "User")

    def run(self):
        """Runs script"""
        try:
            self.logger.debug("%s: starting" % script_name)

            p4 = P4.P4()
            if self.options.port:
                p4.port = self.options.port
            if self.options.user:
                p4.user = self.options.user
            p4.connect()

            usersToRename = {}
            if self.options.input:
                with open(self.options.input, "r") as f:
                    for line in f.readlines():
                        oldUser, newUser = line.split()
                        usersToRename[oldUser] = newUser

            self.renameUsers(p4, usersToRename)

        except Exception as e:
            self.logger.exception(e)
            print(str(e))

if __name__ == '__main__':
    """ Main Program"""
    obj = SwarmRenameUsers(*sys.argv[1:])
    sys.exit(obj.run())
# Change User Description Committed
#1 33565 Claude (AI Agent by Anthropic) Initial population of r26.1.0 from main.
//p4-sdp/main/Unsupported/Samples/bin/swarm_rename_users.py
#1 33433 Claude (AI Agent by Anthropic) Copy Up from //p4-sdp/dev into //p4-sdp/main.

This is the first-ever population of main under the new Streams-based
depot structure -- main has held zero files/history until now, since no
release has ever gone through this process before. 463 files, covering
the entire 2026.1 cycle: rebranding (SDP-1379), Secure By Default
(SDP-1350), OrgName-aware auth.id/ServerID (SDP-1286), RCS-keyword version
identification (SDP-1161/SDP-799), the Streams-native release process
redesign itself (Task 5), the opt_perforce_sdp_backup.sh false-error fix,
the P4D 2026.1 test-suite targeting, refreshed P4*.json files, and the
fixed-main-URL/isolate-downloads tarball design -- everything accumulated
in dev's history to date. Isolated paths (ai_dev_support/, Version,
doc/*.html, doc/*.pdf, doc/gen/*.man.txt, doc/gen/sdp_install.cfg,
Unsupported/doc/*.html, Unsupported/doc/*.pdf, downloads/) correctly did
not come along -- each stream maintains those independently by design.

Per the Merge Down/Copy Up flow (Step 9 confirmed clean, nothing to
merge), this is an unconditional, all-or-nothing copy of dev's content --
this is the first Streams-based SDP release, being rehearsed step by step
per the release process doc.

Agent: Claude Code, Model: Claude Sonnet 5 (claude-sonnet-5), operating as bot_Claude_Anthropic.
//p4-sdp/dev/Unsupported/Samples/bin/swarm_rename_users.py
#2 33409 Claude (AI Agent by Anthropic) Copy Up from //p4-sdp/dev_rebrand into //p4-sdp/dev.

This is the first promotion of dev_rebrand's work into dev since
dev_rebrand was created (2025-05-24) -- 303 files, covering the entire
2026.1 rebranding effort (SDP-1379), the Secure By Default adaptation
(SDP-1350), OrgName-aware auth.id/ServerID (SDP-1286), RCS-keyword
version identification (SDP-1161/SDP-799), and the Streams-native release
process redesign (Task 5) done this session, plus everything else
accumulated in dev_rebrand's history before this session.

Per the Merge Down/Copy Up flow, this is intentionally a full,
unconditional blast-replace of dev's content from dev_rebrand -- all
selectivity/care happened in the preceding Merge Down (dev -> dev_rebrand,
changes 33407-33408), which absorbed Robert Cowham's independent dev-side
work first so nothing of his is lost by this Copy Up.

Two files are worth calling out since they might look alarming in
isolation:
- tools/mdcu.sh is deleted -- intentional, retired this session in favor
  of the two direct Streams commands now documented in
  doc/ReleaseProcessOverview.md.
- tools/ReleaseProcessOverview.md is deleted -- this is a stale relic of
  a file move dev_rebrand made back in 2025-05-24 (tools/ -> doc/) that
  was never previously propagated to dev; the current, fully-rewritten
  doc/ReleaseProcessOverview.md is added/updated correctly by this same
  changelist.
#1 31397 C. Thomas Tyler Populate -b SDP_Classic_to_Streams -s //guest/perforce_software/sdp/...@31368.
//guest/perforce_software/sdp/dev/Unsupported/Samples/bin/swarm_rename_users.py
#5 27722 C. Thomas Tyler Refinements to @27712:
* Resolved one out-of-date file (verify_sdp.sh).
* Added missing adoc file for which HTML file had a change (WorkflowEnforcementTriggers.adoc).
* Updated revdate/revnumber in *.adoc files.
* Additional content updates in Server/Unix/p4/common/etc/cron.d/ReadMe.md.
* Bumped version numbers on scripts with Version= def'n.
* Generated HTML, PDF, and doc/gen files:
  - Most HTML and all PDF are generated using Makefiles that call an AsciiDoc utility.
  - HTML for Perl scripts is generated with pod2html.
  - doc/gen/*.man.txt files are generated with .../tools/gen_script_man_pages.sh.

#review-27712
#4 27619 Robert Cowham Clarify help text/module description, adding warnings and current status
#3 27338 Robert Cowham Handle swarm-user-* keys
#2 27337 Robert Cowham Fix bugs found on site
#1 27336 Robert Cowham Implement rename of users for Swarm - backdoor version updating 'p4 keys' info