#!/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 Swarm key values updated: p4 keys -e swarm-user-* p4 keys -e swarm-workflow-* Users/src/Model/Config.php: const KEY_PREFIX = 'swarm-user-'; swarm-user-jim = {"delayedComments":[],"follows":null,"user_notification_settings":null,"user_settings":null} swarm-user-john = {"delayedComments":[],"follows":null,"user_notification_settings":null,"user_settings":null} """ # 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())