#!/usr/bin/perl -w # # Perforce Swarm Trigger Script # # @copyright 2013-2015 Perforce Software. All rights reserved. # @version 2015.1/1060524 # # This script is meant to be called from a Perforce trigger. # It should be placed on the Perforce Server machine. # See usage information below for more details. # # Ported to Perl by Jason Leonard (jason.leonard@citrix.com) # Based on swarm-trigger.sh (2015.1/1060524) # Limited tested only on Windows with ActivePerl 5.10.1 use strict; use File::Basename; use Getopt::Long; use JSON::PP; use Data::Dumper; # ---------------------------------------------------------------------------- # # This script requires certain variables defined to operate correctly. # # You can utilize one of these default configuration files to define the # variables needed (SWARM_HOST and SWARM_TOKEN at least): # /etc/perforce/swarm-trigger.conf # /opt/perforce/etc/swarm-trigger.conf # swarm-trigger.conf (in the same directory as this script) # # You can also specify '-c ' on the command line to specify a file # to source; anything defined in this file will override variables defined in # the default config files above. # # Alternatively, you can edit this script directly if you prefer, but note that # any values defined in the default config files (or one specified via -c) will # override what is set here. In addition, if you replace or update this script # to a new version, please ensure you preserve your changes. # SWARM_HOST (required) # Hostname of your Swarm instance, with leading "http://" or "https://". my $SWARM_HOST="http://my-swarm-host"; # SWARM_TOKEN (required) # The token used when talking to Swarm to offer some security. To obtain the # value, log in to Swarm as a super user and select 'About Swarm' to see the # token value. my $SWARM_TOKEN="MY-UUID-STYLE-TOKEN"; # SWARM_MAXTIME (optional) # Max time provided to wget or curl my $SWARM_MAXTIME=10; # ADMIN_USER (optional) # For enforcing reviewed changes, optionally specify the Perforce user with # admin privileges (to read keys); if not set, will use whatever Perforce user # is set in environment. my $ADMIN_USER=""; # ADMIN_TICKET_FILE (optional) # For enforcing reviewed changes, optionally specify the location of the # p4tickets file if different from the default ($HOME/.p4tickets). # Ensure this user is a member of a group with an 'unlimited' or very long # timeout; then, manually login as this user from the Perforce server machine to # set the ticket. my $ADMIN_TICKET_FILE=""; # LOGGER (optional, default set here) # For logging errors, we use the 'logger' command. If it is not available in the # PATH of the environment in which Perforce trigger scripts run, specify the # full path here. #my $LOGGER="logger"; # P4, SED & GREP: (optional, defaults set here) # For 'enforce' and 'strict' types, we use the following utilities. If they are # not availabe in the PATH of the environment in which Perforce trigger scripts # run, specify the full path of the utility here. my $P4="p4"; my $SED="sed"; my $GREP="grep"; # DO NOT EDIT PAST THIS LINE ------------------------------------------------- # # This script and its directory my $ME = basename($0); my $MYDIR = dirname($0); # Default flag(s) my $CHECK_REVIEW_CHANGES_ONLY=0; my $CONFIG_FILE=""; my $CONFIG_FILE_USE=""; my $DISPLAY_FLAG_SET=0; my $UTIL="wget"; my $TYPE; my $VALUE; my $P4_PORT; my $GROUP; my $PATHSEP = ($^O =~ /mswin/i) ? '\\' : '/'; sub usage { print STDERR << "USAGE_END"; Usage: $ME -t -v [-p ] [-r] [-g ] [-c ] $ME -o -t: specify the Swarm trigger type (e.g. job, shelve, commit) -v: specify the ID value -p: specify optional (recommended) P4PORT, only intended for '-t enforce' or '-t strict' -r: when using '-t strict' or '-t enforce', only apply this check to changes that are in review. -g: specify optional group to exclude for '-t enforce' or '-t strict'; members of this group, or subgroups thereof will not be subject to these triggers -c: specify optional config file to source variables -o: convenience flag to output the trigger lines This script is meant to be called from a Perforce trigger. It should be placed on the Perforce Server machine and the following entries should be added using 'p4 triggers' (use the -o flag to this script to only output these lines): USAGE_END display_trigger_entries(); print STDERR << 'USAGE_END'; Notes: * The use of '%quote%' is not supported on 2010.2 servers (they are harmless though); if you're using this version, ensure you don't have any spaces in the pathname to this script. * This script requires configuration to be set in an external configuration file or directly in the script itself, such as the Swarm host and token. By default, this script will source any of these config file: /etc/perforce/swarm-trigger.conf /opt/perforce/etc/swarm-trigger.conf swarm-trigger.conf (in the same directory as this script) Lastly, if -c is passed, that file will be sourced too. * For 'enforce' triggers (enforce that a change to be submitted is tied to an approved review), or 'strict' triggers (verify that the content of a change to be submitted matches the content of its associated approved review), uncomment the appropriate lines and replace DEPOT_PATH as appropriate. For additional paths to check, increment the trigger name suffix so that each trigger name is named uniquely. * For 'enforce' or 'strict' triggers, you can optionally specify a group whose members will not be subject to these triggers. * For 'enforce' or 'strict' triggers, if your Perforce Server is SSL-enabled, add the "ssl:" protocol prefix to "%serverport%". USAGE_END exit 99 } sub display_trigger_entries { # Define the trigger entries suitable for this script; replace depot paths as appropriate print STDERR << "EOF-TRIGGER"; swarm.job form-commit job "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t job -v %formname%" swarm.user form-commit user "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t user -v %formname%" swarm.userdel form-delete user "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t userdel -v %formname%" swarm.group form-commit group "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t group -v %formname%" swarm.groupdel form-delete group "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t groupdel -v %formname%" swarm.changesave form-save change "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t changesave -v %formname%" swarm.shelve shelve-commit //... "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t shelve -v %change%" swarm.commit change-commit //... "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t commit -v %change%" #swarm.enforce.1 change-submit //DEPOT_PATH1/... "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t enforce -v %change% -p %serverport%" #swarm.enforce.2 change-submit //DEPOT_PATH2/... "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t enforce -v %change% -p %serverport%" #swarm.strict.1 change-content //DEPOT_PATH1/... "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t strict -v %change% -p %serverport%" #swarm.strict.2 change-content //DEPOT_PATH2/... "%quote%$MYDIR$PATHSEP$ME%quote%$CONFIG_FILE_USE -t strict -v %change% -p %serverport%" EOF-TRIGGER } # Source any external configuration sub source_config { # Source a default set of configuration files (if present). # Variables contained in these files will override any variables defined # at the top of this file. foreach my $file ( "/etc/perforce/swarm-trigger.conf", "/opt/perforce/etc/swarm-trigger.conf", $MYDIR.$PATHSEP."swarm-trigger.conf", $CONFIG_FILE) { if ($file && -s $file) { my %config; if (open(CONFIG, "< $file")) { while () { chomp; s/#.*//; # Remove comments s/^\s+//; # Remove opening whitespace s/\s+$//; # Remove closing whitespace next unless length; my ($key, $value) = split(/\s*=\s*/, $_, 2); $config{$key} = $value; } $SWARM_HOST = $config{SWARM_HOST} if $config{SWARM_HOST}; $SWARM_TOKEN = $config{SWARM_TOKEN} if $config{SWARM_TOKEN}; $SWARM_MAXTIME = $config{SWARM_MAXTIME} if $config{SWARM_MAXTIME}; $ADMIN_USER = $config{ADMIN_USER} if $config{ADMIN_USER}; $ADMIN_TICKET_FILE = $config{ADMIN_TICKET_FILE} if $config{ADMIN_TICKET_FILE}; $P4 = $config{P4} if $config{P4}; $SED = $config{SED} if $config{SED}; $GREP = $config{GREP} if $config{GREP}; $UTIL = $config{UTIL} if $config{UTIL}; close CONFIG; } } } } # Show the usage if no arguments passed usage() if scalar(@ARGV) == 0; # Trap call to self to post to Swarm if ($ARGV[0] eq "background-call") { my $TYPE = $ARGV[1]; my $VALUE = $ARGV[2]; $CONFIG_FILE = $ARGV[3]; source_config(); # We assume SWARM_HOST and SWARM_TOKEN are properly set at this point my $SWARM_QUEUE="$SWARM_HOST/queue/add/$SWARM_TOKEN"; my $RC = 0; if ($UTIL eq "wget") { `wget --quiet --output-document /dev/null --timeout 10 --post-data "$TYPE,$VALUE" "$SWARM_QUEUE"`; $RC = $?; } # If wget spews a high-level error code, try curl instead # Reference: http://www.tldp.org/LDP/abs/html/exitcodes.html if ($RC >= 1 || $UTIL eq "curl") { $UTIL="curl"; `curl --silent --output /dev/null --max-time 10 --data "$TYPE,$VALUE" "$SWARM_QUEUE"`; $RC = $? >> 8; } if ($RC != 0) { # "$LOGGER" -p3 -t "$ME" "Error ($RC) trying to post [$TYPE,$VALUE] via [$UTIL] to [$SWARM_QUEUE]" print "ERROR!!!"; } # Exit cleanly exit 0 } my $help = 0; GetOptions('t=s' => \$TYPE, 'v=s' => \$VALUE, 'p=s' => \$P4_PORT, 'r' => \$CHECK_REVIEW_CHANGES_ONLY, 'g=s' => \$GROUP, 'c=s' => \$CONFIG_FILE, 'o' => \$DISPLAY_FLAG_SET, 'h' => \$help); # Parse arguments #OPT= #while getopts :t:v:p:g:rc:oh OPT #do # case "$OPT" in # t) TYPE="$OPTARG" ;; # v) VALUE="$OPTARG" ;; # p) P4_PORT="$OPTARG" ;; # r) CHECK_REVIEW_CHANGES_ONLY=true ;; # g) GROUP="$OPTARG" ;; # c) CONFIG_FILE="$OPTARG"; CONFIG_FILE_USE=" -c %quote%$CONFIG_FILE%quote%";; # o) DISPLAY_FLAG_SET=true ;; # h) usage ;; # *) "$LOGGER" -p3 -t "$ME" -s "unknown argument [-$OPTARG]" && usage ;; # esac #done $CONFIG_FILE_USE = " -c %quote%$CONFIG_FILE%quote%" if $CONFIG_FILE; usage() if $help; if ($DISPLAY_FLAG_SET) { display_trigger_entries(); exit 0; } # Sanity check supplied arguments if (!$TYPE) { print "no event type supplied\n"; usage(); } if (!$VALUE) { print "$TYPE: no value supplied\n"; usage(); } # Source any configuration files source_config(); # If -t enforce/strict is specified, perform that logic here if ($TYPE eq "enforce" || $TYPE eq "strict") { # Sanity test program variables #for cmd_var in P4 SED GREP #do # cmd="${!cmd_var}" # if ! command -v "$cmd" > /dev/null # then # echo "$ME: $cmd_var is not set properly; please contact your administrator." # exit 1 # fi #done # Set up how we call p4 my @P4_CMD = ("$P4", "-zprog=p4($ME)"); push @P4_CMD, ("-p", "$P4_PORT") if ($P4_PORT); push @P4_CMD, ("-u", "$ADMIN_USER") if ($ADMIN_USER); $ENV{'P4TICKETS'} = $ADMIN_TICKET_FILE if ($ADMIN_TICKET_FILE); # Set character-set explicitly if talking to a unicode server if (`@P4_CMD -ztag info` =~ /\.\.\. unicode enabled/) { push @P4_CMD, ("-C", "utf8"); } # Verify our credentials if (system("@P4_CMD login -s >nul")) { print "Invalid login credentials to [$P4_PORT] within this trigger script; please contact your administrator.\n"; exit 1; } # Check if a group was specified if ($GROUP) { # Obtain the user from the change my $CHANGE_USER = $1 if (`@P4_CMD -ztag change -o $VALUE` =~ /\.\.\. User (\S+)/); # Check the user's groups and see if the group to exclude is there if ($CHANGE_USER && `@P4_CMD -ztag groups -i -u $CHANGE_USER` =~ /\.\.\. group $GROUP\n/) { # User belong to group to exclude, exit cleanly # "$LOGGER" -p5 -t "$ME" "$TYPE: accept change $VALUE: $CHANGE_USER belongs to exempt group $GROUP" exit 0 } } # Search for the review key based on the encoded change number my $FUNNYCHANGE = $VALUE; $FUNNYCHANGE =~ s/(\d)/3$1/g; my $SEARCH_CMD = "search 1301=$FUNNYCHANGE"; my $REVIEW_KEY = `@P4_CMD $SEARCH_CMD`; chomp($REVIEW_KEY); my $RC = $?; # Detect if there is any problem with the command if ($RC > 0) { print "Error searching Perforce for reviews involving this change ($VALUE); please contact your administrator.\n"; # "$LOGGER" -p3 -t "$ME" "$TYPE: reject change $VALUE: error ($RC) from [${P4_CMD[@]} $SEARCH_CMD]" exit $RC; } # Detect if no review is found if (!$REVIEW_KEY) { # if enforcement is only set for reviews, exit happy for changes not associated to any if ($CHECK_REVIEW_CHANGES_ONLY) { exit 0 } print "Cannot find a Swarm review associated with this change ($VALUE).\n"; # "$LOGGER" -p5 -t "$ME" "$TYPE: reject change $VALUE: no Swarm review found" exit 1; } # Detect if the key name is badly formated if (!$REVIEW_KEY =~ /swarm-review-[0-9a-f]*/) { print "Bad review key for this change ($VALUE); please contact your administrator.\n"; # "$LOGGER" -p3 -t "$ME" "$TYPE: reject change $VALUE: bad Swarm review key ($REVIEW_KEY)" exit 1; } # Obtain the JSON value of the associated review my $REVIEW_JSON = `@P4_CMD counter -u $REVIEW_KEY`; $RC = $?; # Detect if there is an error or no value for the key (stale index?) if ($RC > 0 || !$REVIEW_JSON) { print "Cannot find Swarm review data for this change ($VALUE).\n"; # "$LOGGER" -p4 -t "$ME" "$TYPE: reject change $VALUE: empty value for $REVIEW_KEY" exit 1; } # Calculate the human friendly review ID my $REVIEW_ID = $REVIEW_KEY; $REVIEW_ID =~ s/swarm-review-//; $REVIEW_ID = 0xffffffff - hex($REVIEW_ID); #decode the JSON object my $review = decode_json $REVIEW_JSON; # Locate the change inside the review's associated changes my $REVIEW_CHANGES = $review->{'changes'}; if (!grep(/^$VALUE/, @$REVIEW_CHANGES)) { print "This change ($VALUE) is not associated with its linked Swarm review $REVIEW_ID.\n"; # "$LOGGER" -p5 -t "$ME" "$TYPE: reject change $VALUE: change not part of $REVIEW_KEY ($REVIEW_ID)" exit 1; } # Obtain review state and see if it's approved my $REVIEW_STATE = $review->{'state'}; if ("$REVIEW_STATE" ne "approved") { print "Swarm review $REVIEW_ID for this change ($VALUE) is not approved ($REVIEW_STATE).\n"; # "$LOGGER" -p5 -t "$ME" "$TYPE: reject change $VALUE: $REVIEW_KEY ($REVIEW_ID) not approved ($REVIEW_STATE)" exit 1; } # for -t strict, check that the change's content matches that of its review if ($TYPE eq "strict") { my $REVIEW_FSTAT = `@P4_CMD fstat -Ol -T "depotFile, headType, digest" @=$REVIEW_ID`; my $RC1 = $?; my $CHANGE_FSTAT = `@P4_CMD fstat -Ol -T "depotFile, headType, digest" @=$VALUE`; my $RC2 = $?; if ($RC1 != 0 || $RC2 != 0 || !$REVIEW_FSTAT || !$CHANGE_FSTAT) { print "Error obtaining fstat output for this change ($VALUE) or its associated review ($REVIEW_ID); please contact your administrator.\n"; # "$LOGGER" -p3 -t "$ME" "$TYPE: reject change $VALUE: error obtaining fstat output for either change or review ($REVIEW_ID)" exit 1; } # check that the fstat output matches if ($REVIEW_FSTAT != $CHANGE_FSTAT) { print "The content of this change ($VALUE) does not match the content of the associated Swarm review ($REVIEW_ID).\n"; # "$LOGGER" -p5 -t "$ME" "$TYPE: reject change $VALUE: content does not match review ($REVIEW_ID)" exit 1; } } # # Return success at this point exit 0; } # Sanity check global variables we need for posting events to Swarm if (!$SWARM_HOST or $SWARM_HOST eq "http://my-swarm-host") { print "SWARM_HOST is not set properly; please contact your administrator.\n"; exit 1; } if (!$SWARM_TOKEN or $SWARM_TOKEN eq "MY-UUID-STYLE-TOKEN") { print "SWARM_TOKEN is not set properly; please contact your administrator.\n"; exit 1; } # For other Swarm trigger types, post the event to Swarm asynchronously # (call self, but detach to the background) system(1, ($0, "background-call", $TYPE, $VALUE, $CONFIG_FILE)); # Always return success to avoid affecting Perforce users exit 0;