SSTemplateUpdate.py #16

  • //
  • guest/
  • perforce_software/
  • cbd/
  • dev/
  • triggers/
  • SSTemplateUpdate.py
  • View
  • Commits
  • Open Download .zip Download (24 KB)
#!/p4/common/python/bin/python3
# -*- coding: utf-8 -*-
"""@SSTemplateUpdate.py
|------------------------------------------------------------------------------|
| Copyright (c) 2008-2016 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')
P4CONFIG = os.getenv ('P4CONFIG', 'UNDEFINED_P4CONFIG_VALUE')
P4BIN = os.getenv("P4BIN", "p4")

if (P4CONFIG != 'UNDEFINED_P4CONFIG_VALUE'):
    del os.environ[ 'P4CONFIG']

DEFAULT_LOG_FILE = '%s/SSTemplateUpdate.log' % LOGDIR
DEFAULT_VERBOSITY = 'DEBUG'

LOGGER_NAME = 'SSTemplateUpdateTrigger'

VERSION = '2.1.7'

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.7
            
            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 //....cbdsst "/p4/common/bin/cbd/triggers/SSTemplateUpdate.py %changelist%"

               Use of two entries is intended to enhance robustness for scenarios where 'p4 populate' is used.
            
            EXIT CODES
            Zero indicates normal completion.  Non-zero indicates an error.
            
            '''),
         epilog="Copyright (c) 2008-2016 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 79 is for p4d 2015.2.
      self.p4.api_level = 79
      self.stream = None
      self.streamDepot = None
      self.streamDepth = None
      self.streamType = None
      self.streamShortName = None
      self.VersionRemappedField = False
      self.VersionIgnoredField = False

      try:
         self.p4.connect()

      except P4Exception:
         self.log.fatal("Unable to connect to Perforce at P4PORT=%s.\n\nThe 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.\n\nThe 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.\n\nThe 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)

      # 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 archive actions.
               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

               if not self.streamDepot:
                  # Take something like //fgs/main/fgs.cbdsst, and derive values from it,
                  # streamDepot = fgs
                  # streamShortName = main
                  # streamName = //fgs/main
                  self.streamDepot = re.sub (r'^//', '', file)
                  self.streamDepot = re.sub ('/.*$', '', self.streamDepot)
                  dData = self.p4.fetch_depot (self.streamDepot)
                  if (dData['StreamDepth']):
                     self.streamDepth = re.sub ('^.*/', '', dData['StreamDepth'])
                     self.streamDepth = int(self.streamDepth)
                  else:
                     self.streamDepth = 1

                  # Split the path into elements using the '/' delimter.
                  # Elements 0 and 1 are always empty (due to Perforce paths starting
                  # with '//'.  Element 2 is the stream depot name.
                  # The stream name will look like //fgs/main, or //components/fgs/main.
                  # The streamDepth tells how many levels of directory to count.
                  # The streamShortName (e.g. 'main') is the n'th element, dependent on
                  # the streamDepth (e.g. element 3 for //fgs/main, element 4 for 
                  # //components/fgs/main.
                  pathElements = file.split ('/')
                  self.streamShortName = pathElements[self.streamDepth+2]
                  self.stream = '//%s/%s' % (self.streamDepot, pathElements[3])

                  # For stream depots with  StreamDepth> 1,
                  i = 1
                  while (i < self.streamDepth):
                     self.stream = "%s/%s" % (self.stream, pathElements[i+3])
                     i = i + 1

                  self.log.debug ("Stream for CBDSST files is: %s" % self.stream)

      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)

      try:
         sstFileContents = self.p4.run_print ('-q', fileWithRevSpec)

      except P4Exception:
         userMessage = "Your attempt to submit changelist %s failed because the CBD automation was unable to get the contents of [%s] from the Perforce server.  This could happen if no changes were made to the *.cbdsst file.\n\nThe error from the Perforce server is:\n" % (self.myOptions.changelist, fileWithRevSpec)
         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)

         sys.exit (1)

      oldDesc = sstFileContents[1]
      ###oldDesc = sstFileContents
      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 ('(Stream|Options|Owner|Type|Parent)\:', 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 from 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\s+', '', line)
            sharePath = "%s/%s" % (self.stream, re.sub ('^share\s+', '', line))
            paths.append (('share', sharePath))
            newPaths.append ("share %s" % shortPath)
            self.log.debug ("PATH DATA %s %s" % paths[pathsCount])

         if (re.match ('import\s+', 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\s+', '', 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\+\s+', '', 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

      if self.VersionRemappedField:
         # The newRemapped() array includes data from the 'Remapped:' field of the stream
         # spec.
         newRemapped = list()

         # This is a count/index for the newRemapped() array.
         remappedCount = 0

         # Parse entries from the 'Remapped:' field in the *.cbdsst file.
         remappedEntriesStarted = False

         for line in oldDesc.split('\n'):
            if (re.match ('Remapped\:', line)):
               remappedEntriesStarted = True
               continue
            if (remappedEntriesStarted == False):
               continue

            if (not re.match ('\t', line)):
                break

            line = line.strip()
            line = line.rstrip()
            self.log.debug ("Remapped Entry: %s" % line)
            newRemapped.append(line)

            remappedCount = remappedCount + 1

         if (remappedCount > 0):
             self.log.debug ("Adding these Remapped field entries: %s" % newRemapped)
             sData['Remapped'] = newRemapped

      if self.VersionIgnoredField:
         # The newIgnored() array includes data from the 'Ignored:' field of the stream
         # spec.
         newIgnored = list()

         # This is a count/index for the newIgnored() array.
         ignoredCount = 0

         # Parse entries from the 'Ignored:' field in the *.cbdsst file.
         ignoredEntriesStarted = False

         for line in oldDesc.split('\n'):
            if (re.match ('Ignored\:', line)):
               ignoredEntriesStarted = True
               continue
            if (ignoredEntriesStarted == False):
               continue

            if (not re.match ('\t', line)):
                break

            line = line.strip()
            line = line.rstrip()
            self.log.debug ("Ignored Entry: %s" % line)
            newIgnored.append(line)

            ignoredCount = ignoredCount + 1

         if (ignoredCount > 0):
             self.log.debug ("Adding these Ignored field entries: %s" % newIgnored)
             sData['Ignored'] = newIgnored

      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.\n\nThe 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 actionable *.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
#16 19419 C. Thomas Tyler Fixed typo in logged output.
#15 19337 C. Thomas Tyler Added support for StreamDepth > 1.

Enhanced test cases for StreamDepth > 1.
#14 19253 C. Thomas Tyler Routine merge-down from main to dev.
#13 15274 C. Thomas Tyler Merge Down of cbd to dev from main using:
p4 merge -b perforce_software-cbd-dev
#12 15157 C. Thomas Tyler Fixed CBD environment-deprivation issue for CBD trigger.
       Enhanced stream spec parsing.
#11 14978 C. Thomas Tyler Enhanced error auditing.
#10 14855 C. Thomas Tyler Removed '-n' (NoOp) mode, as it is no longer viable as a pre-commit
triggers.

Updated test suite to use pre-commit 'submit-content' type triggers.
#9 14854 C. Thomas Tyler Removed ability to opt out with 'NO_CBD' tag in the changelist
description.  This feature has not been used, and is not compatible
with the new pre-commit mode of operation, nor with other changes
in progress to enable a bi-directional flow.
#8 14853 C. Thomas Tyler This change introduces a 'p4 login -s' login status check.
 If it
fails, the changelist submit attempt is rejected, with an appropriate
message for the user.

Added user message and change submit rejection if the Perforce connection
check fails.  This is unlikely as we're called by the Perforce server.

Added user message and change submit rejection if the 'p4 describe' on the
user's changelist fails for whatever reason.
#7 14852 C. Thomas Tyler Converted SSTemplateUpdate to work as a trigger of type 'change-content' rather
than 'change-commit', where it has the new responsibility of rejecting the
submit of changelists attempting to update *.cbdsst when the generated stream
specs are rejected by the Helix server (or otherwise fail to update).

The CBD keys are updated if and only the stream spec is accepted by the live
server.

This change removes a common failure mode where *.cbdsst file could be submitted
containing bad content (e.g. syntax errors in the 'Paths:' field), causing the the
corresponding keys not to be updated.

This change introduces a failure mode where the keys could be updated, yet the
changelist containing the *.cbdsst file fails to be submitted, e.g. due to
server crash.  Because the server processes changes first before calling the
trigger, the submit is likely to succeed, since things like protections and
required resolves are already checked by the time this script is called.  We do
NOT rollback stream spec or keys updates in event of such failure.  The assumption
is that the issue with the bogus submit will be addressed quickly.

Several tests performed manually:
* Submit of valid *.cbdsst file.  Succeeds.
* Submit of bogus *.cbdsst file.  Rejected as expected, with server error
  message deliverd with user to help debug it.
* Deletion of jam.cbdsst.  Works as expected.
* Recovery of deleted jam.cbdsst.  Works as expected.
* Rename of jam.cbdsst to foo (a non-*.cbdsst file).  Allows change to
  go thru, no keys processing or stream spec update.
* Failed submit of *.cbdsst due to protections change.  Works as expected;
  handled entirley by p4d.
* Attempted submit of unmodified *.cbdsst in a workspact with SubmitUnchnaged
  not set.  Works as expected; handled entirley by p4d.

TO DO:
* Add several test cases covering this functionality to the test regression
  test suite.
* Update deployment documentation.
* Consider whether we should attempt to rollback keys and stream spec updates
  in unlikely event the 'p4 submit' fails.  Such a failure is unlikely because,
  by the time this script is called, the content s of files are already on the
  server, and 'p4d' has already done various checks such as access checks and
  resolve-needed checks.  Worst case scenario is bad though: stream specs could
  get updated without a submit, leading to lack of notificaiton.  (This situation
  is no worse, and overall much better, than the earlier version.)
#6 14839 C. Thomas Tyler Updated to latest CBD from dev box.
#5 14198 C. Thomas Tyler Got Workshop up to date with latest version of CBD developed
elsewhere.

Added CbdDev.py to illustrate enabling testing a newer version
on a live server, as a supplement to the test suite.  This comes
with supporting scripts wssync.dev.(sh,py)

Added cmd_trig_by_auth.pl, a technology sample script.  Not used
presently.
#4 13833 C. Thomas Tyler Merge-down from main.

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 13773 C. Thomas Tyler Added support for handling import+ entries.

Fixed bug where excess keys were not cleaned up, e.g. if a stream spec
was modified to delete an import entry.

Corrected text in debug output.
#2 11363 C. Thomas Tyler SSTemplateUpdate.py:

* Added exception handling for case where an exception is thrown
in the re.match() block when checking to see if the changelist
description has the NO_CBD tag.

* Generiziced some comments.

* Started to add logic to avoid stripping the 'import @' entry if the
revision specifier is a changelist nubmer, since import@
as of 2014.1 now supports this.  But then adjusted to avoid writing
@change numbers in the revision specifiers to avoid potential user
confusion when change specifiers are made in more than one place (i.e.
in the *.cbdsst file and the live server stream spec).

There is still the possibility users might add @change values to
stream specs directly.  Do we need a stream spec validator to prevent
users from adding @change to import lines, and if possible messaging
the user and letting them know they need to update the *.cbdsst file
instead if they try adding @change to import entries on the live
stream spec?
#1 11355 C. Thomas Tyler Added CBD triggers.