# # $Header: //guest/tommy_fad/utils/user_management/updatePerforce.pl#3 $ # # Script written by Chriz Bartz and modified by Tommy Fad and Steve Lysohir. See comments below. # use Mail::Sendmail; use p4; use Getopt::Std; use SHA 1.2; use strict; sub logStr; sub vLogStr; sub logErr; sub dieOnP4Err; sub logP4Err; sub dieOnErr; sub logP4Command; # Set the following variable to the email address of who should get # messages about updates to perforce. my $emailNotify = 'licensingperson@youremail, licensingperson2@youreamil'; my $ccAddrs = 'perforceadmins@youremail'; my $fromAddr = '"P4 Agent" <p4agent@youremail>'; # You should not have to modify anything below this point. my $helpStr = <<EOT This script reads the perforceUsers.txt file that is produced from the the Perforce Users database. This files which list users are active and should be added and/or updated, which groups those users belong to, and which users are to be deleted. The perforce server is updated to reflect the information in this file. When deleting users there are a number of common errors that this script handles. Common errors are that users have files opened. All clients that are owned by the deleted users are also deleted as are any clients that the deleted user has open files on. Deleting these clients effectively 'reverts' the open files. If other users have files open on those clients then the deletion of the client will fail and appropriate email is sent. Usage: $0 [p4 usage options] [-s <file>] [-l <logDir> -v] [-a] notesData Supports all p4 usage options except -x and -V (e.g. -u <username>) -l <logFile> - log file name -v - turns on verbose logging -s - SHA file. This is used to store a checksum of the notesData file to avoid talking to perforce if the file hasn't changed. It is put in the logDir directory. notesData - File that contains the data from the notes database in the following format: One item per line. Each item looks like: GROUP,<groupName>,<userName> USER,<userName>,<full name>,<email address> DELETE ME,<userName>,<full name>,<email address> EOT ; my $delUserErrMsg = <<EOT Error deleting a user EOT ; my %options = (); my %p4DefinedUsers = (); my %p4ClientsByUser = (); my %p4DefinedGroups = (); my %p4ClientHost = (); my %p4ClientLocked = (); my %p4ClientsByUser = (); my %usersToDelete = (); my %usersToAdd = (); my %groupsToAdd = (); my %usersByGroup = (); my %openFilesByClient = (); my %clientsUsedByUser = (); my $errMsg = ""; my $emailMsg = ""; my $p4Output; my $tmpFile = "$0." . time . ".tmp"; my $loggedMsg; # Get command line options, set up perforce command options based on them, # and open log file getopts ('?hc:d:p:P:u:l:vs:', \%options); if ($options{'h'} || $options{'?'}) { print $helpStr; exit; } my $p4 = ""; $p4 .= " -c $options{'c'}" if $options{'c'}; $p4 .= " -d $options{'d'}" if $options{'d'}; $p4 .= " -P $options{'P'}" if $options{'P'}; $p4 .= " -p $options{'p'}" if $options{'p'}; $p4 .= " -u $options{'u'}" if $options{'u'}; my $logDir = $options{'l'} ? $options{'l'} : "."; my $logFile = "$logDir\\log"; my $delClients = "$logDir\\deletedClients"; my $delGroups = "$logDir\\deletedGroups"; my $delUsers = "$logDir\\deletedUsers"; # Only open the log file if there is something useful to write my $logFileOpen = 0; my $notesChecksum = ''; $notesChecksum = "$logDir\\" . $options{'s'} if $options{'s'}; mkdir "$logDir", umask; mkdir "$delClients", umask; mkdir "$delGroups", umask; mkdir "$delUsers", umask; dieOnErr "You must specify the file created by the Notes Database\n" unless $ARGV[0]; dieOnErr "\nFile from Notes Database is 0 length; can not proceed\n\n" unless -s $ARGV[0]; dieOnErr "You must specify a server with the -p option\n" unless $options{'p'}; my $sha = new SHA; $sha->reset(); open NOTESCHKSUM, "<$notesChecksum"; my $oldDigest = <NOTESCHKSUM>; close NOTESCHKSUM; # Read the perforce.txt file my $lineNum = 0; open NOTESDATA, "<$ARGV[0]" or dieOnErr "Can't open $ARGV[0]: $!\n"; while (<NOTESDATA>) { $sha->add($_); $lineNum++; s/\n//; split ','; if ($_[0] eq "GROUP") { $_[1] =~ s/ //g; $groupsToAdd{lc $_[1]} = $_[1] unless $groupsToAdd{lc $_[1]}; if ($groupsToAdd{lc $_[1]} ne $_[1]) { my $str = "\nUser \"$_[2]\" listed in group \"$_[1]\".\n" . "\tIt probably should be \"$groupsToAdd{lc $_[1]}\".\n" . "\tUpdate this user in the PerforceUsers database\n"; $errMsg .= $str; logStr $str; } $usersByGroup{$groupsToAdd{lc $_[1]}}{lc $_[2]} = $_[2]; } elsif ($_[0] eq "USER") { $usersToAdd{lc $_[1]}{'username'} = $_[1]; $usersToAdd{lc $_[1]}{'fullname'} = $_[2]; $usersToAdd{lc $_[1]}{'emailAddr'} = $_[3]; } elsif ($_[0] eq "DELETED USER") { $usersToDelete{lc $_[1]}{'username'} = $_[1]; $usersToDelete{lc $_[1]}{'fullname'} = $_[2]; $usersToDelete{lc $_[1]}{'emailAddr'} = $_[3]; } else { dieOnErr "illegal format on line $lineNum in $ARGV[0]\n$_"; } } close NOTESDATA; my $digest = $sha->hexdigest(); if ($digest eq $oldDigest) { finish(); exit; } # build hash of currently defined users from Perforce $p4Output = logP4Command ("$p4 users"); while ($p4Output =~ /(\S+)([^\n]*)\n/g) { $p4DefinedUsers{lc $1} = $1; } # Build hash of all groups in Perforce. $p4Output = logP4Command ("$p4 groups"); while ($p4Output =~ /(.*)\n/g) { $p4DefinedGroups{lc $1} = $1; } # Get list of all open files. open FILES, "p4 $p4 opened -a|" or dieOnErr "Can't open p4 $p4 open -a pipe: $!\n"; while (<FILES>) { my ($file, $user, $client) = m!([^#]*#\d+).*by ([^\@]*)\@([^\s]*)!; push @{ $openFilesByClient{$client}{$user}}, $file; $clientsUsedByUser{$user}{$client} = 1; } close FILES; # Build list of all clients and who owns them from Perforce open CLIENTS, "p4 $p4 clients|" or dieOnErr "Can't p4 clients: $!"; my $clientCount = 0; while (<CLIENTS>) { $clientCount++; print "$clientCount\r"; /Client (\S+)/; my $client = $1; my $clientSpec = logP4Command ("$p4 client -o $client"); my $user = ($clientSpec =~ /\nOwner:\s+(\S+)/g) ? $1 : 'noUser'; push @{ $p4ClientsByUser{lc $user} }, $client; $p4ClientHost{lc $client} = ($clientSpec =~ /\nHost:\s+(\S+)/g) ? $1 : 'noHost'; $p4ClientLocked{lc $client} = ($clientSpec =~ /\nOptions:[^\n]* (locked)/g) ? 'locked' : 'unlocked'; } close CLIENTS; # Delete all the users marked for deletion in the file my $user; foreach $user (sort keys %usersToDelete) { # Check if this user is in usersToAdd also just to make sure. # The database should not allow this but it has in the past. if ($usersToAdd{lc $user}) { logStr "Skipping delete of $user because this user is being added.\n" . "This should not happen but the script has to assume that the\n" . "add is correct and skip the deletion.\n\n"; next; } # Check if this user is in Perforce if ($p4DefinedUsers{lc $user}) { my $userName = $p4DefinedUsers{$user}; my $emailMsg = "Deleting User $userName (" . $usersToDelete{lc $user}{'fullname'} . ")\n"; logStr "Deleting user $userName\n"; open USER, ">$delUsers\\$userName"; print USER logP4Command ("$p4 user -o $userName"); close USER; # Delete clients owned by this user unless other users # have open files on those clients. my $emailErrors = 0; my $clientName; foreach $clientName (@{$p4ClientsByUser{lc $user}}) { logStr "\tDeleting client $clientName\n"; my $clientSpec = p4::p4Command("$p4 client -o $clientName"); open CLIENT, ">$delClients\\$clientName"; print CLIENT $clientSpec; close CLIENT; my $u; my $deleteClient = 1; foreach $u (keys %{$openFilesByClient{$clientName}}) { $emailErrors = 1; if (lc $user ne lc $u) { $emailMsg .= "Can not delete client '$clientName' owned by" . " '$user' because user '$u' has open files on it.\n"; $emailMsg .= "Deleting user '$user' will fail because of this.\n" if $openFilesByClient{$clientName}{$user}; $deleteClient = 0; } $emailMsg .= "Files open by '$u' on client '$clientName':\n\n\t"; $emailMsg .= join "\n\t", @{$openFilesByClient{$clientName}{$u}}; $emailMsg .= "\n\n"; } if ($deleteClient) { $p4Output = logP4Command("$p4 client -fd $clientName"); logStr "\tp4 output: $p4Output\n"; } delete $clientsUsedByUser{$user}{$clientName}; } # Delete all clients *USED* by but not owned by this user unless that # client is also being used by another user. foreach $clientName (keys %{$clientsUsedByUser{$user}}) { logStr "\tDeleting client $clientName\n"; my $clientSpec = p4::p4Command("$p4 client -o $clientName"); open CLIENT, ">$delClients\\$clientName"; print CLIENT $clientSpec; close CLIENT; my $u; my $deleteClient = 1; foreach $u (keys %{$openFilesByClient{$clientName}}) { $emailErrors = 1; if (lc $user ne lc $u) { $emailMsg .= "User '$user' is using client '$clientName'.\n" . "'$user' does NOT own this client and user '$u' also has " . "files open on this client so the client can not be " . "deleted automatically.\n" . "Deleting user '$user' will fail because of this.\n"; $deleteClient = 0; } $emailMsg .= "Files open by '$u' on client '$clientName':\n\n\t"; $emailMsg .= join "\n\t", @{$openFilesByClient{$clientName}{$u}}; $emailMsg .= "\n\n"; } if ($deleteClient) { $p4Output = logP4Command("$p4 client -fd $clientName"); logStr "\tp4 output: $p4Output\n"; } } # Actually delete the user my $p4Output = logP4Command("$p4 user -fd $userName"); logStr "p4 output: $p4Output\n"; if ($p4Output =~ /can't be deleted/) { $emailErrors = 1; $emailMsg .= $p4Output if $p4Output =~ /can't be deleted/; } logStr $emailMsg; if ($emailErrors) { $digest = 0; my %mail = (To => $emailNotify, Cc => $ccAddrs, From => $fromAddr, Subject => "Errors deleting user $userName from Perforce", Message => scalar localtime() . "\n" . $emailMsg ); if (not sendmail %mail) { logStr "Error sending mail: $Mail::Sendmail::error \n"; } logStr "Mail log:\n$Mail::Sendmail::log\n"; } } } # Process groups. For each group in the notes database, check # if the members have changed. If they have, update the group # and list the changes in the email to perforceAdmins. Also # report on new groups and deleted groups. my $group; foreach $group (keys %usersByGroup) { my %currMembers = (); my $groupChanged = 0; # # Compare to old group to find new and deleted members my $groupOutput = logP4Command ("$p4 group -o $group"); my $users = $groupOutput; $groupOutput =~ s/(^.*)\nUsers:.*$/$1/s; $groupOutput =~ s/^.*\n(Group:[^\n]*)\n//s; # Check if the group is empty before looking for users if ($users =~ /\nUsers:.?\n/) { $users =~ s/^.*\nUsers:.?\n//s; while ($users =~ /\s*([^\n]*)/g) { my $user = $1; next unless $user =~ /\S/; $currMembers{lc $user} = $user; if ($usersByGroup{$group}{lc $user} ne $user) { logStr "Removing user $user from group $group\n"; $groupChanged = 1; } } foreach $user (keys %{$usersByGroup{$group}}) { if ($currMembers{$user} ne $usersByGroup{$group}{$user}) { logStr "Adding user $user ($usersToAdd{lc $user}{'fullname'})" . " to group $group\n"; $groupChanged = 1; } } } else { logStr "Creating new group $group with the following members:\n\t"; foreach $user (keys %{$usersByGroup{$group}}) { logStr "\n\t$usersByGroup{$group}{$user}"; } logStr "\n"; $groupChanged = 1; } # Update group if it has changed if ($groupChanged) { logStr "updating group $group\n"; logP4Command("$p4 group -d $group"); open GROUP_PIPE, "|p4 $p4 group -i > $tmpFile" or dieOnErr "Can't open $p4 group -i PIPE: $!"; print GROUP_PIPE "Group:\t$group\n$groupOutput\nUsers:\n\t"; foreach $user (keys %{$usersByGroup{$group}}) { print GROUP_PIPE "\n\t$usersByGroup{$group}{$user}"; } print GROUP_PIPE "\n"; close GROUP_PIPE or dieOnErr "Can't close $p4 group -i PIPE: $!"; open TMP, "<$tmpFile"; logStr "p4 output: " . join "", <TMP>; close TMP; } } # Update the allUsers group open GROUP_PIPE, "|p4 $p4 group -i > $tmpFile" or dieOnErr "Can't open $p4 group -i PIPE: $!"; print GROUP_PIPE "Group:\tallUsers\nUsers:\n\t"; foreach $user (keys %usersToAdd) { print GROUP_PIPE "\n\t$usersToAdd{$user}{'username'}"; } print GROUP_PIPE "\n"; close GROUP_PIPE or dieOnErr "Can't close $p4 group -i PIPE: $!"; open TMP, "<$tmpFile"; my $tmpStr = <TMP>; close TMP; logStr "p4 output: $tmpStr" unless $tmpStr =~ /not/; # Create/Update all users defined in the database my $key; foreach $key (keys %usersToAdd) { my $user; my $emailMsg = ""; my $emailSubject = ""; if (!$p4DefinedUsers{lc $key}) { $user = $usersToAdd{$key}{'username'}; logStr "Adding user $user ($usersToAdd{$key}{'fullname'})\n"; } else { $user = $p4DefinedUsers{lc $key}; } my $userSpec = logP4Command("$p4 user -o $user"); $userSpec =~ /\nUser:\s+(.*?)\n.*\nEmail:\s+(.*?)\n.*\nFullName:\s+(.*?)\n/s; my $username = $1; my $emailAddr = $2; my $fullName = $3; if ($username ne $usersToAdd{$key}{'username'} || $emailAddr ne $usersToAdd{$key}{'emailAddr'} || $fullName ne $usersToAdd{$key}{'fullname'}) { logStr "Updating user $user (now $usersToAdd{$key}{'username'}) " . "($usersToAdd{$key}{'fullname'}) <$usersToAdd{$key}{'emailAddr'}>\n"; logStr "\tIt was \"$username\" ($fullName) <$emailAddr>\n"; $userSpec =~ s/\nUser:[^\n]*\n/\nUser:\t$usersToAdd{$key}{'username'}\n/s; $userSpec =~ s/\nEmail:[^\n]*\n/\nEmail:\t$usersToAdd{$key}{'emailAddr'}\n/s; $userSpec =~ s/\nFullName:[^\n]*\n/\nFullName:\t$usersToAdd{$key}{'fullname'}\n/s; # Change this to an open PIPE to p4 open PIPE, "|p4 $p4 user -fi > $tmpFile" or logErr "Can't open p4 $p4 user -fi pipe: $!\n"; print PIPE $userSpec; close PIPE or logErr "Can't close p4 $p4 user -fi pipe: $!\n"; open TMP, "<$tmpFile"; logStr "p4 output: " . join "", <TMP>; close TMP; } } # Check for users that are in Perforce but are not in the Notes database # at all (neither in the toAdd or toDelete lists). Send email about these. foreach $key (keys %p4DefinedUsers) { if (!$usersToAdd{lc $key} && !$usersToDelete{lc $key}) { my $str = "\nUser \"$p4DefinedUsers{$key}\" is defined on perforce " . "server $options{'p'}\n" . "\tbut is not listed in the Perforce Users database. This\n" . "\tuser should be added to the database or deleted from the\n" . "\tperforce server.\n"; $errMsg .= $str; logStr $str; } } if ($errMsg) { $digest = 0; my %mail = (To => $emailNotify, Cc => $ccAddrs, From => $fromAddr, Subject => "Errors updating perforce from Notes Database", Message => scalar localtime() . "\n" . $errMsg ); if (not sendmail %mail) { logStr "Error sending mail: $Mail::Sendmail::error \n"; } logStr "Mail log:\n$Mail::Sendmail::log\n"; } p4::p4Final(); if ($options{'s'}) { open NOTESCHKSUM, ">$notesChecksum"; print NOTESCHKSUM $digest; close NOTESCHKSUM; } finish (); ## Error checking/logging functions sub logStr { if (!$logFileOpen) { open LOG, ">>$logFile"; print LOG "\n\n------------------------------\n" . "Script started at " . scalar localtime($^T) . "\n"; print LOG "Digest: $digest\n"; $logFileOpen = 1; } print LOG @_; } sub vLogStr { logStr @_ if $options{'v'}; } sub logErr { logStr @_; } sub dieOnP4Err { dieOnErr "Perforce error on command: p4 $_\nerrMsg:\n$p4::errMsg\n" . "errNo: $p4::errNo\nCan not continue after fatal error" if ($p4::errMsg); } sub logP4Err { logErr "Perforce error on command: p4 $_\nerrMsg:\n$p4::errMsg\n" . "errNo: $p4::errNo\n" if $p4::errMsg; } sub dieOnErr { logStr @_; logStr "Can not continue after fatal error\n"; finish (); die @_; } sub logP4Command { my $command = shift; my $output = p4::p4Command($command); vLogStr "Output from p4 $command:\n$output\n"; logErr "Perforce error on command: p4 $command\nerrMsg:\n$p4::errMsg\n" . "errNo: $p4::errNo\n" if $p4::errMsg; return $output; } sub finish { unlink $tmpFile; if ($logFileOpen) { logStr "Script finished at " . scalar localtime() . "\n"; close LOG; } }