Last active
October 1, 2018 11:54
-
-
Save simonmcconnell/c8bc34be9a1d6c7fd9c343d7884fa317 to your computer and use it in GitHub Desktop.
Maintain version history when upgrading from VSS to SVN version control in Proficy Change Management
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'********************************************************************************* | |
' Product Module : Scheduler Scripts | |
' Script Name : getAllVersions.vb | |
' Function : Backup each version of a Machine Edition project and its history | |
' file. Designed to be used in conjunction with proficy_import.py | |
' script when migrating from VSS to SVN. | |
' retreives <projectname>_<version>.zip and <projectname>_history.xml | |
' for all projects selected in a manually triggered Scheduler Event. | |
' | |
' Change BackupFolder to the location of the project backup files. | |
' | |
' | |
' Version: 9.50 (Build 7677) | |
' | |
' Revision History: | |
' SM - Original 25/05/2018 | |
'********************************************************************************* | |
' | |
Execute fxIncludeStartupScript("fxConstants.vb") | |
Execute fxIncludeStartupScript("scriptErrors.vb") | |
Execute fxIncludeStartupScript("Sched_Logging.vb") | |
Execute fxIncludeStartupScript("GlobalScripts.vb") | |
Dim ProjectType | |
Dim ProjectName | |
Dim MaxVersion | |
Dim v | |
Dim WorkingDir | |
Dim BackupFolder | |
Dim LogFileName | |
Dim LogFile | |
Dim LogEvent | |
Dim CheckoutStatus | |
CleanupVariables() | |
ProjectType = CurrentProjectType ' product type should be MACHINE EDITION | |
ProjectName = CurrentProject | |
MaxVersion = fxGetProjectVersionMax(ProjectType, ProjectName) | |
WorkingDir = fxGetDirPath(5) & "\" & CurrentServer & "\FrameworX\" | |
'"C:\Users\Public\Documents\GE Intelligent Platforms\PME\SecurWORX\<PCM SERVER NAME>\FrameworX" | |
fxDirSetCurrent(WorkingDir & ProjectName) | |
BackupFolder = "E:\ProficyBackup\" | |
CheckDir WorkingDir | |
CheckDir BackupFolder | |
CreateLogFile() | |
If ( Not Main() ) Then LogEvent("Something went wrong") | |
Cleanup() | |
Function Main() | |
HistoryReport() | |
fxDirSetCurrent(WorkingDir & "\" & ProjectName) | |
' if it is checked out on this computer, backup this version also | |
CheckoutStatus = fxGetProjectCheckoutStatus(ProjectType, ProjectName) | |
select case CheckoutStatus | |
' -3: An internal error occurred while attempting to acquire the checkout status. | |
' Tip: When fxGetProjectCheckoutStatus returns this value, use fxGetLastError (and fxGetErrorDescription) for more information on the error that occurred. | |
' -2: A file or project exists with the specified ProjectName, but it is not recognizable as a project. | |
' -1: The specified project cannot be found. | |
' 0: The project is not checked out by anyone. | |
' 1: The project is checked out by the current user on the current computer. | |
' 2: The project is checked out by the current user on a different computer. | |
' 3: The project is checked out by a different user on the current computer. | |
' 4: The project is checked out by a different user on a different computer. | |
' 5: The project is a local project. It does not exist on the Server and thus cannot be checked out. | |
' 6: The project is a new project created by the current user while disconnected from the Server. | |
' 7: The project is a new project created by someone else while disconnected from the Server. | |
case -3 | |
AddToLog "An internal error occurred while attempting to acquire the checkout status. Error: " & CStr(fxGetLastError) & " " & fxGetErrorDescription(fxGetLastError) | |
Exit Function | |
case -2 | |
AddToLog "A file or project exists with the specified ProjectName, but it is not recognizable as a project." | |
Exit Function | |
case -1 | |
AddToLog "The specified project cannot be found." | |
Exit Function | |
case 0 | |
If fxGetProjectLocalStatus(ProjectType, ProjectName) = 1 Then Call fxDeleteLocal(ProjectType, ProjectName) | |
bDeleteLocal = True | |
BackupAllVersions() | |
case 1 | |
sBackupZip = BackupFolder & ProjectName & "_working.zip" | |
fxZip WorkingDir & ProjectName, "*.*", sBackupZip, 1 | |
' undo checkout | |
iRet = fxUndoCheckout(ProjectType, ProjectName, 0) | |
lastErr = fxGetLastError() | |
if iRet <> 0 Then | |
AddToLog "Failed to undo checkout project with error: " & CStr(lastErr) & " " & fxGetErrorDescription(lastErr) | |
Else | |
BackupAllVersions() | |
' copy working version back | |
fxUnzip sBackupZip, fxDirGetCurrent(), "*.*" ' might have to use WorkingDir & "\" & ProjectName | |
' checkout w/o overwriting files | |
If ( fxCheckout(ProjectType, ProjectName, 1) <> 0 ) Then | |
lastErr = fxGetLastError() | |
AddToLog "Failed to checkout project with error: " & CStr(lastErr) & " " & fxGetErrorDescription(lastErr) | |
End If | |
End If | |
AddToLog "The project was checked out by the current user on the current computer. Check the project in and retry. Backed up as " & ProjectName & "_working.zip" | |
case 2 | |
AddToLog "The project is checked out by the current user on a different computer. Check that version into the new PCM server after restoring the full history." | |
bDeleteLocal = True | |
BackupAllVersions() | |
case 3 | |
AddToLog "The project is checked out by a different user on the current computer. Please have them check this project in and try again." | |
case 4 | |
AddToLog "The project is checked out by a different user on a different computer. Check that version into the new PCM server after restoring the full history." | |
bDeleteLocal = True | |
BackupAllVersions() | |
end select | |
if bDeleteLocal Then | |
Call fxDeleteLocal(ProjectType, ProjectName) | |
End If | |
Main = True | |
End Function | |
Function BackupAllVersions() | |
v = 1 | |
Do Until v > MaxVersion | |
' be nice if this worked... | |
' bGetSuccess = fxGetProjectImage(ProjectType, ProjectName, v, BackupFolder, 2) | |
' If bGetSuccess = 0 Then | |
' AddToLog ProjectType & "\" & ProjectName & " Failed to get version " & CStr(v) & " to " & BackupFolder | |
' End If | |
' clean the working directory | |
Call fxDeleteLocal(ProjectType, ProjectName) | |
' get the specific version. Files are output to WorkingDir. | |
iRet = fxGet(ProjectType, ProjectName, CStr(v), 0) | |
if iRet = 0 Then | |
' Zip the contents of the Get folder (local working directory). | |
' This is essentially backing up the project but fxGetProjectImage refuses to work. | |
fxDirSetCurrent(WorkingDir & ProjectName) | |
fxZip fxDirGetCurrent(), "*.*", BackupFolder & ProjectName & "_" & CStr(v) & ".zip", 1 | |
Else | |
AddToLog "fxGet failed with error: " & fxGetErrorDescription(fxGetLastError()) | |
End If | |
v = v + 1 | |
Loop | |
BackupAllVersions = True | |
End Function | |
Function CreateLogFile() | |
LogFileName = ProjectName & "_backup.log" | |
fxDirSetCurrent(BackupFolder) | |
If fxFileExists(LogFileName) then fxFileDelete(LogFileName) | |
LogFile = fxFileCreate(LogFileName) | |
CreateLogFile = True | |
End Function | |
Function HistoryReport() | |
' generate XML history report for project: overwrite files and display oldest to newest | |
If ( fxHistoryReport(ProjectType, ProjectName, BackupFolder & ProjectName & "_history.xml", 6) <> 0 ) Then | |
AddToLog "Failed to get history report." | |
End If | |
HistoryReport = True | |
End Function | |
Function Cleanup() | |
fxDirSetCurrent(BackupFolder) | |
fxFileClose LogFile | |
' delete empty log files | |
set fso = createobject("scripting.filesystemobject") | |
if not fso.folderexists(BackupFolder) then | |
set fso = nothing | |
exit function | |
end if | |
for each ofile in fso.getfolder(BackupFolder).files | |
if ofile.size = 0 then fso.deletefile ofile.path, true | |
next | |
set fso = nothing | |
CleanupVariables() | |
End Function | |
Function CheckDir(Dir) | |
if (Right(Dir, 1) <> "\") then | |
Dir = Dir & "\" | |
end if | |
End Function | |
Sub AddToLog(MyString) | |
PreviousDir = fxDirGetCurrent() | |
fxDirSetCurrent(BackupFolder) | |
If (MyString <> "") Then | |
LogEvent("[" & ProjectType & "\" & ProjectName & "]: " & MyString) | |
iRet = fxFileWriteString(LogFile, CStr(Time()) & " [" & ProjectType & "\" & ProjectName & "]: " & MyString) | |
if iRet = 0 then LogEvent("Failed to write " & MyString & " to file " & LogFile) | |
iRet = fxFileWriteEOL(LogFile) | |
if iRet = 0 then LogEvent("Failed to write EOL to file " & LogFile) | |
End If | |
fxDirSetCurrent(PreviousDir) | |
End Sub | |
'************************************** | |
' Cleanup Variables used by the script. | |
'************************************** | |
Function CleanupVariables() | |
CheckoutStatus = Empty | |
ProjectType = Empty | |
ProjectName = Empty | |
MaxVersion = Empty | |
WorkingDir = Empty | |
BackupFolder = Empty | |
LogFileName = Empty | |
LogFile = Empty | |
' return our success | |
CleanupVariables = True | |
End Function |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# import projects into PCM that were backed up to <PROJECT>_<VER>.ZIP | |
# from the getAllVersions.vb Scheduler script | |
# can also rename projects on the way | |
# useful when migrating from VSS to SVN | |
import sys | |
from pathlib import Path | |
from shutil import make_archive, unpack_archive, copy2 | |
import re | |
from xml.etree.ElementTree import ParseError, parse | |
import pywintypes | |
import win32com.client | |
# TODO: add logging | |
PCM_SERVER = 'MYSERVERNAME' | |
LOC_WORKING_DIR = Path(r'C:\Users\Public\Documents\GE Intelligent Platforms\PME\SecurWORX\Local\FrameworX') | |
SVR_WORKING_DIR = Path(r'C:\Users\Public\Documents\GE Intelligent Platforms\PME\SecurWORX') / PCM_SERVER / 'FrameworX' | |
def get_source_dir(): | |
# directory containing the backups | |
while True: | |
source_dir = Path(input("Enter directory containing backup files: ")) | |
if source_dir.exists() and source_dir.is_dir(): | |
break | |
else: | |
print(f'"{source_dir}" is not a valid directory! Ensure it exists...') | |
return source_dir | |
def get_backup_dir(): | |
# where to save backup files | |
while True: | |
backup_dir = Path(input("Where do you want to save the renamed backups to? ")) | |
if backup_dir.exists(): | |
if backup_dir.is_dir(): | |
break | |
else: | |
print(f'"{backup_dir}" is not a valid directory!') | |
else: | |
if input(f'"{backup_dir} doesn\'t exist, create it? ').lower() == 'y' or 'yes': | |
backup_dir.mkdir(parents=True) | |
break | |
return backup_dir | |
def rename_backups(ProjectName, NewProjectName, source_dir=None, backup_dir=None): | |
# check if there is a project of this name stored locally already | |
# if so: ask if we want to delete it or abort | |
# for each backup in backup directory: | |
# restore | |
# rename to new name | |
# backup to <new name>_<version> in user specified directory | |
# need to be concious of not overwriting existing backups, could we rename to <new name>_<version>x.zip? | |
def order_versions(files): | |
""" | |
Return a sorted list of lists containing the version and filename. | |
[[1, ProjectName_1.zip], [2, ProjectName_2.zip] ... ] | |
:param files: list of files to sort | |
:return: sorted list of lists containing the version and filename | |
""" | |
def key(item): | |
return item[0] | |
backups = [] | |
for f in files: | |
version = int(f.name.replace('.zip', '').split('_')[-1]) | |
backups.append([version, f]) | |
return sorted(backups, key=key) | |
print(f'\nAttempting to rename all {ProjectName} backups to {NewProjectName}...\n') | |
if Projects.DoesProjectExist(NewProjectName, 1): | |
while True: | |
if input(f"Planned project name already exists locally. Delete it? ").lower() == 'yes' or 'y': | |
Projects.DeleteProject(NewProjectName, 1) | |
break | |
else: | |
print("Project renaming aborted!") | |
print(f"Please remove or rename the local copy of {NewProjectName} and try again.") | |
return | |
if Projects.DoesProjectExist(ProjectName, 1): | |
while True: | |
delete = input(f"Project {ProjectName} already exists locally. Do you want to delete it? ") | |
if delete.lower() == 'y' or 'yes': | |
Projects.DeleteProject(ProjectName, 1) | |
break | |
elif delete.lower() == 'n' or 'no': | |
print("Project renaming aborted!") | |
print(f"Please remove or rename the local copy of {ProjectName} and try again.") | |
return | |
if source_dir is None: | |
source_dir = get_source_dir() | |
if backup_dir is None: | |
backup_dir = get_backup_dir() | |
# ordered list of the backup files [version, filename] | |
backups = order_versions([ | |
f | |
for f in list(source_dir.glob(f'{ProjectName}_*.zip')) | |
if '_'.join(f.name.split('_')[:-1]) == ProjectName | |
]) | |
print(f'{backups[-1][0]} versions of {ProjectName} found...') | |
for v, f in backups: | |
# restore | |
# rename | |
# backup to <new name>_<version> in user specified directory (zip the contents of the folder) | |
print(f"renaming version {v} of {ProjectName} to {NewProjectName}") | |
Projects.RestoreProject(f) # TODO: add exception checking | |
Projects.RenameProject(NewProjectName, ProjectName, 1) # TODO: add exception checking | |
try: | |
zipfile = backup_dir / (NewProjectName + '_' + str(v)) | |
make_archive(zipfile, 'zip', LOC_WORKING_DIR / NewProjectName) | |
except FileNotFoundError: | |
print(f'Failed to zip: folder "{zipfile}" not found') | |
return # TODO: add error code for return value | |
finally: | |
Projects.DeleteProject(NewProjectName, 1) | |
else: | |
copy2(source_dir / (ProjectName + '_history.xml'), backup_dir / (NewProjectName + '_history.xml')) | |
def read_xml(xmlfile): | |
# open xml file | |
# create structure of [ {version, date, user, comment}, ... ] | |
# trim the comment to 255 chars | |
# return list of dicts\ | |
try: | |
tree = parse(xmlfile) | |
except ParseError: | |
print(f'Failed to parse xml file "{xmlfile}"!') | |
return [], 0 | |
except FileNotFoundError: | |
print(f'File not found: "{xmlfile}"') | |
return [], 0 | |
root = tree.getroot() | |
versions = root.findall('./HistoryVersions/Version') | |
vcount = int(root.find('HistoryVersions').attrib['count']) | |
vlist = [] | |
for v in versions: | |
comment = v.find('Comment').text | |
if not comment: | |
comment = '' | |
regex = re.compile('Created *') | |
action = v.find('Action').text | |
if re.match(regex, action): | |
comment = 'Created ' + comment | |
vlist.append({ | |
'version': v.find('VersionNumber').text, | |
'date': v.find('VersionDate').text, | |
'user': v.find('User').text, | |
'comment': comment, | |
'action': action | |
}) | |
def version_key(item): | |
return int(item['version']) | |
return sorted(vlist, key=version_key), vcount | |
# TODO: move to ./util/ | |
def delete_folder(folder: Path): | |
if folder.exists(): | |
for subdir in folder.iterdir(): | |
if subdir.is_dir(): | |
delete_folder(subdir) | |
else: | |
subdir.unlink() | |
folder.rmdir() | |
else: | |
print(f"Cannot delete '{folder}', it doesn't exist!") | |
def make_comment(version): | |
c = ' '.join([version['date'][:11], version['user'], version['comment']]) | |
if len(c) > 255: | |
c = ' '.join([version['date'][:11], version['user'], version['comment']]) | |
print(c) | |
if len(c) > 255: | |
c = ' '.join([version['date'][:11], version['comment']]) | |
print(c) | |
if len(c) > 255: | |
return version['comment'] | |
return c | |
def add_backup_to_pcm(ProjectName, zipfile, comment): | |
""" | |
Add a backup of an existing project to the Proficy Change Management server. | |
The project must be checked in on the server before executing this function. | |
:rtype: bool | |
:param ProjectName: Project name | |
:param zipfile: Backup zip file | |
:param comment: Check in comment | |
:return: True if successful | |
""" | |
# check that we can check out the project | |
# if it is checked out already, abort | |
# otherwise: | |
# check out the project | |
# delete the local copy from working directory | |
# restore our backup to working directory | |
# check in w/comment from *_history.xml | |
print(f"Check in comment: {comment}") | |
if Manager.CanCheckoutProject(ProjectName): | |
Manager.CheckOut(ProjectName, '') # TODO: add exception handling | |
project_folder = SVR_WORKING_DIR / ProjectName | |
delete_folder(project_folder) | |
unpack_archive(zipfile, project_folder, 'zip') | |
try: | |
Manager.CheckIn(ProjectName, comment, 1) # TODO: extract comment | |
return True | |
except: | |
return False | |
else: | |
print(f'Unable to check out {ProjectName}! Already checked out?') | |
return False | |
def restore_backups(ProjectName, source_dir=None, skip_v1=False): | |
""" | |
Restore all backups in the user specified directory that are in the format <Project Name>_<Version>.zip | |
to Proficy Change Management. | |
:param ProjectName: The project name | |
:param source_dir: Directory containing backup files | |
:param skip_v1: Set True to skip version 1 | |
:return: | |
""" | |
# get list of versions to add from xml file | |
# add each version to PCM | |
# cleanup? | |
if source_dir is None: | |
source_dir = get_source_dir() | |
vlist, vcount = read_xml(Path(source_dir / (ProjectName + '_history.xml'))) | |
print(f"\n{vcount} versions for {ProjectName}") | |
for v in vlist: | |
if skip_v1 and v['version'] == '1': | |
pass | |
else: | |
print(f"Adding {ProjectName} version {v['version']} to PCM server {PCM_SERVER}") | |
add_backup_to_pcm( | |
ProjectName, | |
Path(source_dir) / (ProjectName + '_' + v['version'] + '.zip'), | |
make_comment(v) | |
) | |
# if restored: | |
# print(f"Restored {ProjectName} version {v['version']} to {PCM_SERVER}") | |
# else: | |
# print(f"Failed to restore {ProjectName} version {v['version']} to {PCM_SERVER}") | |
def connect(): | |
""" | |
Create a Machine Edition (FrameworX.exe) COM object, CimplicityME.Applicaiton, and the Projects and Manager interfaces. | |
""" | |
try: | |
fx = win32com.client.Dispatch('CimplicityME.Application') | |
except pywintypes.com_error: | |
print("Failed to load Proficy automation interface 'CimplicityME.Applicaion'") | |
print("Please close all instances of Machine Edition") | |
sys.exit(1) | |
try: | |
Projects = fx.Projects | |
except AttributeError: | |
print("Failed to get Projects automation interface") | |
fx.Quit() | |
sys.exit(1) | |
try: | |
Manager = fx.ManagerObject | |
except AttributeError: | |
print("Failed to get Manager automation interface") | |
fx.Quit() | |
sys.exit(1) | |
return fx, Projects, Manager | |
if __name__ == '__main__': | |
proficy, Projects, Manager = connect() | |
try: | |
pass | |
# restore = ['PROJECT1', 'PROJECT2'] | |
# for proj in restore: | |
# restore_backups(proj, source_dir=Path(r'D:\ProficyBackup\BACKUPDIR'), skip_v1=True) | |
# rename_backups('OLDNAME', 'NEWNAME', Path(r'D:\ProficyBackup\SOURCEDIR'), Path(r'D:\ProficyBackup\BACKUPDIR')) | |
# input("\nManually restore version 1 of NEWNAME before continuting:") | |
# restore_backups('NEWNAME', source_dir=Path(r'D:\ProficyBackup\BACKUPDIR'), skip_v1=True) | |
finally: | |
proficy.Quit() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment