#!/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://swarm.workshop.perforce.com/projects/perforce-software-sdp/view/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- 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())