#!/p4/common/python/bin/python3
# -*- coding: utf-8 -*-
"""@SSTemplateUpdate.py
|------------------------------------------------------------------------------|
| Copyright (c) 2008-2015 Perforce Software, Inc. |
|------------------------------------------------------------------------------|
SSTemplateUpdate.py - Stream Spec Template Update, a part of the CBD system.
Environment Dependencies:
This script assumes a complete Perforce environment including a valid,
non-expiring ticket in the P4TICKETS file. This script is called by
the Perforce server, so the environment should be reliable.
"""
# Python 2.7/3.3 compatibility.
from __future__ import print_function
import sys
# Python 2.7/3.3 compatibility.
if sys.version_info[0] >= 3:
from configparser import ConfigParser
else:
from ConfigParser import ConfigParser
import argparse
import textwrap
import os
import re
from datetime import datetime
import logging
from P4 import P4, P4Exception
# If working on a server with the SDP, the 'LOGS' environment variable contains
# the path the standard loggin directory. The '-L <logfile>' argument shoudl be
# specified in non-SDP environments.
LOGDIR = os.getenv ('LOGS', '/p4/1/logs')
P4PORT = os.getenv ('P4PORT', 'UNDEFINED_P4PORT_VALUE')
P4USER = os.getenv ('P4USER', 'UNDEFINED_P4USER_VALUE')
P4CLIENT = os.getenv ('P4CLIENT', 'UNDEFINED_P4CLIENT_VALUE')
del os.environ ['P4CONFIG']
DEFAULT_LOG_FILE = '%s/SSTemplateUpdate.log' % LOGDIR
DEFAULT_VERBOSITY = 'DEBUG'
LOGGER_NAME = 'SSTemplateUpdateTrigger'
VERSION = '2.1.2'
class Main:
""" SSTemplateUpdate
"""
def __init__(self, *argv):
""" Initialization. Process command line argument and initialize logging.
"""
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent('''\
NAME
SSTemplateUpdate.py
VERSION
2.1.2
DESCRIPTION
Stream Spec template update.
TRIGGER CONFIGURATION
This script is intended to be configured as a Perforce server trigger.
The entry in the 'Triggers:' table looks like the following:
SSTemplateUpdate change-content //GameName/CBD_dev/....cbdsst "/p4/common/bin/cbd/triggers/SSTemplateUpdate.py %changelist%"
EXIT CODES
Zero indicates normal completion. Non-zero indicates an error.
'''),
epilog="Copyright (c) 2008-2015 Perforce Software, Inc."
)
parser.add_argument('changelist', help="Changelist containing an update to an versioned stream spec template (*.cbdsst) file.")
parser.add_argument('-L', '--log', default=DEFAULT_LOG_FILE, help="Default: " + DEFAULT_LOG_FILE)
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)
self.myOptions = parser.parse_args()
self.log = logging.getLogger(LOGGER_NAME)
self.log.setLevel(self.myOptions.verbosity)
format='%(levelname)s [%(asctime)s] [%(funcName)s : %(lineno)d] - %(message)s'
logging.basicConfig(format=format, filename=self.myOptions.log, level=self.myOptions.verbosity)
self.logHandler = logging.FileHandler(self.myOptions.log, mode='a')
df = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%m/%d/%Y %H:%M:%S')
bf = logging.Formatter('%(levelname)s: %(message)s')
self.logHandler.setFormatter(bf)
self.log.addHandler (self.logHandler)
self.log.debug ("Command Line Options: %s\n" % self.myOptions)
# Connect to Perforce
def initP4(self):
"""Connect to Perforce."""
self.p4 = P4()
self.p4.prog = LOGGER_NAME
self.p4.version = VERSION
self.p4.port = P4PORT
self.p4.user = P4USER
self.p4.client = P4CLIENT
self.p4.ticket_file = os.getenv ('P4TICKETS', 'UNDEFINED_P4TICKETS_VALUE')
# API Levels are defined here: http://answers.perforce.com/articles/KB/3197
# Ensure this matches the P4Python version used.
# API Level 78 is for p4d 2015.1.
self.p4.api_level = 78
self.stream = None
try:
self.p4.connect()
except P4Exception:
self.log.fatal("Unable to connect to Perforce at P4PORT=%s. The error from the Perforce server is:" % P4PORT)
for e in self.p4.errors:
print (e)
self.log.error("Errors: " + e)
for w in self.p4.warnings:
print (w)
self.log.warn("Warnings: " + w)
return False
try:
self.log.debug("Doing 'p4 login -s' login status check.")
self.p4.run_login('-s')
except P4Exception:
userMessage = "Your attempt to submit changelist %s has been rejected because the CBD system user [%s] is not logged into the server [P4PORT=%s]. Please contact your Perforce Administrator for help. The error from the Perforce server is:\n" % (self.myOptions.changelist, P4USER, P4PORT)
print (userMessage)
self.log.fatal("CBD admin user not logged in. User error message is:\n%s\n" % userMessage)
for e in self.p4.errors:
print (e)
self.log.error("Errors: " + e)
for w in self.p4.warnings:
print (w)
self.log.warn("Warnings: " + w)
return False
return True
def get_sst_files(self):
"""Find all *.cbdsst files submitted in the given changelist.
Set set.workspace, self.sst_files[].
Return sst_file_count, the number of *.cbdsst files associated with the changelist.
"""
sst_file_count = 0
if (not re.match ('\d+$', self.myOptions.changelist)):
self.log.fatal ("The value supplied as a changelist number [%s] must be purely numeric." % self.myOptions.changelist)
return 0
self.log.info ("Processing changelist %s." % self.myOptions.changelist)
try:
cData = self.p4.run_describe('-s', self.myOptions.changelist)
except P4Exception:
userMessage = "Your attempt to submit changelist %s has been rejected because the CBD system cannot get information about that changelist. Please contact your Perforce Administrator for help. The error from the Perforce server is:\n" % (self.myOptions.changelist)
print (userMessage)
self.log.fatal("Failed to describe changlist. User error message is:\n%s\n" % userMessage)
for e in self.p4.errors:
print (e)
self.log.error("Errors: " + e)
for w in self.p4.warnings:
print (w)
self.log.warn("Warnings: " + w)
sys.exit(1)
self.log.debug ("Data: %s" % cData)
self.p4.client = cData[0]['client']
self.sst_files = []
index = -1
sst_file_count = 0
self.cspec = self.p4.fetch_client (self.p4.client)
self.log.debug ("CDATA: %s" % self.cspec)
if (self.cspec['Stream']):
self.stream = self.cspec['Stream']
self.log.debug ("Stream: %s" % self.stream)
else:
self.stream = None
self.log.debug ("No stream associated with workspace %s." % self.p4.client)
# Since this script is called as a trigger firing on changelists containing *.cbdsst files,
# we can assume that at least one file will be associated with the changelist. However, if
# a task stream, those files will not be visibile to us. So we detect if no *.cbdsst files
# are associated with the given changelist, and bail if so.
isTaskStream = 0
try:
if cData[0]['depotFile']:
isTaskStream = 0
except KeyError:
isTaskStream = 1
if not isTaskStream:
for file in cData[0]['depotFile']:
index = index + 1
if (re.search ('\.cbdsst$', file)):
action = cData[0]['action'][index]
rev = cData[0]['rev'][index]
# Ignore actions other than add, move/add, branch, edit, and integrate.
# The delete and move/delete actions are ignored, as well as the
# purged and archived.
if (not re.search ('add|branch|edit|integrate', action)):
self.log.warn ("Ignored '%s' action on %s#%s." % (action, file, rev))
continue
self.sst_files.append(file)
sst_file_count = sst_file_count + 1
self.log.debug ("Found %d *.cbdsst files in changelist %s." % (sst_file_count, self.myOptions.changelist))
return sst_file_count
def update_stream_spec_and_keys (self, file):
"""Update the stream spec on the live server from the template.
Paths:
share Source/...
import Framework/... //Framework/main/...@5036
"""
# When called as a 'submit-content' trigger, the content of not-yet-submitted files can be accessed
# with '@=' syntax, symilar to referencing content of files in a shelved changelist. This allows us
# to verify that the stream spec can be updated on the live server before allowing the submit of the
# *.cbdsst file to proceed.
fileWithRevSpec = "%s%s%s" % (file, r'@=', self.myOptions.changelist)
self.log.info ("Updating live stream spec from %s." % fileWithRevSpec)
sData = self.p4.fetch_stream (self.stream)
self.log.debug ("SDATA1: %s" % sData)
sstFileContents = self.p4.run_print ('-q', fileWithRevSpec)
oldDesc = sstFileContents[1]
newDesc = ''
# Parse 'Description:' field value from the *.cbdsst file, substituting the
# stream name tag.
for line in oldDesc.split('\n'):
if (re.match ('^\s*#', line) or re.match('^Description\:', line)):
continue
if (re.match ('Paths\:', line)):
break
line = re.sub ('__EDITME_STREAM__', self.stream, line)
line.strip()
if not newDesc:
newDesc = line
else:
newDesc = newDesc + '\n' + line
newDesc = re.sub ('^\t', '', newDesc)
newDesc = re.sub ('\n\t', '\n', newDesc)
sData['Description'] = newDesc
# The paths() array includes data from the 'Paths:' field of the stream
# spec, augmented by revision specifiers extracted from the stream spec
# template file.
paths = list()
# The newPaths() list is similar to paths, but excludes the revsion
# specifiers, as they're not valid for P4D 2013.2 and lower servers.
# It is in a form suitalbe for feeding directly to the 'Paths' field
# of the stream spec using the P4Python API.
newPaths = list()
# This is a count/index for both the paths() and newPaths() arrays,
# as they both have the name number of elements.
pathsCount = 0
# Parse import path entries form the 'Paths:' field in the *.cbdsst file.
# If we find revision specifiers on the depot paths, strip them off
# before feeding them to the server, but also preserve the revision
# specifiers for writing to the 'p4 keys' store.
pathEntriesStarted = False
for line in oldDesc.split('\n'):
if (re.match ('Paths\:', line)):
pathEntriesStarted = True
continue
if (pathEntriesStarted == False):
continue
line = line.strip()
if (re.match ('share ', line)):
# shortPath is just what follows the 'share' token in the
# 'Paths:' field entry of a stream spec.
# sharePath is the fully-qualified form of the shortPath following
# the 'share' token in values in the 'Paths:' field of the stream
# spec. It is obtained by prefixing shortPath with the stream
# name and '/'. So for 'share src/...' in the //Jam/MAIN
# stream, sharePath would be "//Jam/MAIN/src/...".
shortPath = re.sub ('^share ', '', line)
sharePath = "%s/%s" % (self.stream, re.sub ('^share ', '', line))
paths.append (('share', sharePath))
newPaths.append ("share %s" % shortPath)
self.log.debug ("PATH DATA %s %s" % paths[pathsCount])
if (re.match ('import ', line)):
# If a revsion specifier was found on an 'import' line, store it.
revSpec = '#head'
if (re.search('#', line)):
revSpec = line
revSpec = re.sub('^.*#', '#', line)
if (re.search('@', line)):
revSpec = line
revSpec = re.sub('^.*@', '@', line)
localPath = re.sub ('import ', '', line)
localPath = re.sub (' //.*$', '', localPath)
depotPath = re.sub ('^.*//', '//', line)
depotPath = re.sub ('(#|@).*$', '', depotPath)
paths.append (('import', localPath, depotPath, revSpec))
# The newPaths() list excludes the revision specifier, since P4D servers
# 2013.2 and lower cannot handle it.
newPaths.append ("import %s %s" % (localPath, depotPath))
self.log.debug ("PATH DATA %s L:[%s] D:[%s] R:[%s]" % paths[pathsCount])
if (re.match ('import\+ ', line)):
# If a revsion specifier was found on an 'import+' line, store it.
revSpec = '#head'
if (re.search('#', line)):
revSpec = line
revSpec = re.sub('^.*#', '#', line)
if (re.search('@', line)):
revSpec = line
revSpec = re.sub('^.*@', '@', line)
localPath = re.sub ('import\+ ', '', line)
localPath = re.sub (' //.*$', '', localPath)
depotPath = re.sub ('^.*//', '//', line)
depotPath = re.sub ('(#|@).*$', '', depotPath)
paths.append (('import+', localPath, depotPath, revSpec))
# The newPaths() list excludes the revision specifier, since P4D servers
# 2013.2 and lower cannot handle it.
newPaths.append ("import+ %s %s" % (localPath, depotPath))
self.log.debug ("PATH DATA %s L:[%s] D:[%s] R:[%s]" % paths[pathsCount])
pathsCount = pathsCount + 1
self.log.debug ("== Path Entries ==")
for pathEntry in paths:
self.log.debug ("RAW PATH ENTRY[0]: %s" % pathEntry[0])
if (pathEntry[0] == 'share'):
self.log.debug ("PATH ENTRY: share %s" % pathEntry[1])
if (pathEntry[0] == 'import'):
self.log.debug ("PATH ENTRY: import %s %s%s" % (pathEntry[1], pathEntry[2], pathEntry[3]))
if (pathEntry[0] == 'import+'):
self.log.debug ("PATH ENTRY: import+ %s %s%s" % (pathEntry[1], pathEntry[2], pathEntry[3]))
sData['Paths'] = newPaths
self.log.debug ("SDATA2: %s" % sData)
try:
self.log.debug ("Saving stream spec data %s: %s" % (self.stream, sData))
self.p4.save_stream(sData)
except P4Exception:
userMessage = "Your attempt to submit changelist %s has been rejected because the stream spec failed to update on the server. It may have been rejected by the Perforce server. The error from the Perforce server is:\n" % self.myOptions.changelist
print (userMessage)
self.log.fatal("Failed to save stream spec. User error message is:\n%s\n" % userMessage)
for e in self.p4.errors:
print (e)
self.log.error("Errors: " + e)
for w in self.p4.warnings:
print (w)
self.log.warn("Warnings: " + w)
return False
# Next, update the 'p4 keys' on the server.
# First, generate a list of existing keys for this stream to remove.
keyNameBase = 'cbd_stream_%s' % self.stream
keyNameBase = re.sub ('//', '', keyNameBase)
keyNameBase = re.sub ('/', '_', keyNameBase)
pathKeySearch = keyNameBase
vSpecKeySearch = keyNameBase
pathKeySearch = '%s_path*' % pathKeySearch
vSpecKeySearch = '%s_vspec*' % vSpecKeySearch
pathKeysData = self.p4.run_keys('-e', pathKeySearch)
vSpecKeysData = self.p4.run_keys('-e', vSpecKeySearch)
for keyData in pathKeysData:
self.log.debug ("Running: p4 key -d %s" % keyData['key'])
try:
self.p4.run_key('-d', keyData['key'])
except P4Exception:
self.log.fatal("Failed to delete key [%s] from server." % keyData['key'])
for e in self.p4.errors:
self.log.error("Errors: " + e)
for w in self.p4.warnings:
self.log.warn("Warnings: " + w)
for keyData in vSpecKeysData:
self.log.debug ("Running: p4 key -d %s" % keyData['key'])
try:
self.p4.run_key('-d', keyData['key'])
except P4Exception:
self.log.fatal("Failed to delete key [%s] from server." % keyData['key'])
for e in self.p4.errors:
self.log.error("Errors: " + e)
for w in self.p4.warnings:
self.log.warn("Warnings: " + w)
# Finally, generate the new keys.
i = 0
for pathEntry in paths:
pathKeyName = "%s_path%d" % (keyNameBase, i)
vSpecKeyName = "%s_vspec%d" % (keyNameBase, i)
if (pathEntry[0] == 'share'):
pathKeyValue = pathEntry[1]
vSpecKeyValue = '#head'
if (pathEntry[0] == 'import'):
pathKeyValue = pathEntry[2]
vSpecKeyValue = pathEntry[3]
if (pathEntry[0] == 'import+'):
pathKeyValue = pathEntry[2]
vSpecKeyValue = pathEntry[3]
self.log.debug ("Running: p4 key %s %s" % (pathKeyName, pathKeyValue))
try:
self.p4.run_key(pathKeyName, pathKeyValue)
except P4Exception:
self.log.fatal("Failed to create path key [%s] with value [%s]." % (pathKeyName, pathKeyValue))
for e in self.p4.errors:
self.log.error("Errors: " + e)
for w in self.p4.warnings:
self.log.warn("Warnings: " + w)
self.log.debug ("Running p4 key %s %s" % (vSpecKeyName, vSpecKeyValue))
try:
self.p4.run_key(vSpecKeyName, vSpecKeyValue)
except P4Exception:
self.log.fatal("Failed to create vspec key [%s] with value [%s]." % (vSpecKeyName, vSpecKeyValue))
for e in self.p4.errors:
self.log.error("Errors: " + e)
for w in self.p4.warnings:
self.log.warn("Warnings: " + w)
i = i + 1
return True
def update_modified_stream_specs (self):
"""Update stream specs for the given changelist."""
if (self.get_sst_files()):
for file in self.sst_files:
if (not self.update_stream_spec_and_keys (file)):
self.log.debug ("Processing complete. Changelist %s REJECTED." % self.myOptions.changelist)
return False
self.log.debug ("Processing complete. Changelist %s ACCEPTED." % self.myOptions.changelist)
return True
else:
self.log.warn ("No actionalble *.cbdsst files found in changelist %s. Allowing change to submit." % self.myOptions.changelist)
self.log.debug ("Processing complete. Changelist %s ACCEPTED." % self.myOptions.changelist)
return True
if __name__ == '__main__':
""" Main Program
"""
main = Main(*sys.argv[1:])
if (not Main.initP4(main)):
sys.exit (1)
if (Main.update_modified_stream_specs(main)):
sys.exit (0)
else:
sys.exit (1)
| # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #11 | 19429 | C. Thomas Tyler | Released CBD/MultiArch/2016.2/19425 (2016/05/17). | ||
| #10 | 19351 | C. Thomas Tyler |
Released CBD/MultiArch/2016.2/19348 (2016/05/10). Copy Up using 'p4 copy -r -b perforce_software-cbd-dev'. |
||
| #9 | 16705 | C. Thomas Tyler | Set to use SDP standard Python/P4Python. | ||
| #8 | 16702 | C. Thomas Tyler | Configured to ensure python3 is used. | ||
| #7 | 15273 | C. Thomas Tyler |
Copy Up using 'p4 copy -r -b perforce_software-cbd-ntx64'. Stabilization changes. Test suite enhancements. |
||
| #6 | 15158 | C. Thomas Tyler |
Copy Up from dev to main for CBD, using: p4 copy -r -b perforce_software-cbd-dev |
||
| #5 | 15009 | C. Thomas Tyler | Promoted CBD development work to main from dev. | ||
| #4 | 13832 | C. Thomas Tyler |
Single-file bug fix promotion to main from ntx64 branch: Updated Stream Spec Update Trigger (SSTemplateUpdate.py.) * Refined import+ handling. * Switched logging to 'append' mode. * Increased default logging verbosity INFO -> DEBUG. * Added stream spec data to debug output. |
||
| #3 | 13805 | C. Thomas Tyler |
Copy Up from to main from dev. Completed support for 'import+' handling. Simplified logging. Fixed bug where CBD keys were not cleanly updated in case of a removal of an import. Fixed internal doc bugs. |
||
| #2 | 11366 | C. Thomas Tyler | Promoted CBD from dev to main. | ||
| #1 | 11356 | C. Thomas Tyler |
Promotion from Dev Branch. What's included: * CBD scripts for Streams as demonstrated at Merge 2014. * Deletion of files from the original PoC that aren't needed. What's coming later, still work in progress on the dev branch: * Documentation. * Test Suite with complete Vagrant-based Test Environment. * CBD scripts for Classic. |
||
| //guest/perforce_software/cbd/dev/triggers/SSTemplateUpdate.py | |||||
| #1 | 11355 | C. Thomas Tyler | Added CBD triggers. | ||