Программа автоматической установки HP UFT с PowerShell
В компании, где я работаю, мы используем HPE Unified Functional Testing для нашего автоматизированного функционального тестирования. Когда мы принимаем нового члена команды, одной из его вводных задач является настройка нового лабораторного компьютера. Однако на этот раз я подумал — «эта задача должна быть проще» — и решил использовать функцию автоматической установки, чтобы создать сценарий установки для HP UFT.
Я разбил сценарий на следующие части:
- Скачать файлы
- Распаковать файлы
- Запустить установщик
Я начал писать сценарии и быстро понял, что мне нужен файл конфигурации. В качестве формата конфигурационного файла я выбрал JSON. Он легко читается PowerShell, удобен для человека, а структура более продвинута, чем .ini-файлы. Я отказался от xml, так как считаю, что это в основном для машин.
Как только я начал работу над сценарием конфигурации, я также выбрал более общее решение, чтобы использовать свой сценарий для установки других элементов.
Файл конфигурации
Файл конфигурации содержит следующую информацию:
- Заголовок — как называется вещь, которую собираются установить?
- Текстовое описание того, что делает весь скрипт установки.
- Расположение заархивированного файла, содержащего все файлы, необходимые для установки.
- Хэш файла, чтобы мы могли проверить, был ли файл уже загружен.
- Имя папки для локального размещения загруженных файлов.
- Шаги установки.
Давайте посмотрим на пример:
{ "Title": "The Example", "Description": "Installs the example app. ", "SetupZipFile": "\\\\Some\\Share\\At\\My\\Compant\\example_app.zip", "SetupZipFileHash": { "Algorithm": "SHA256", "Hash": "8D7ECA5A3438707C2BAC576900261CB7D2C937ED8DFAA6216EA6AE647330D9B9" }, "LocalInstallFolder": "exampleapp", "Steps": [ { "Title": "Install prerequisites", "Type": "exe", "File": "example_setup.exe", "Arguments": "-quiet -allOptions -that -is -needed" }, { "Title": "Install the real package", "Type": "msi", "File": "example.msi", "Arguments": "/quiet -allOptions -that -is -needed" }, ] }
Как видите, сценарий установки может обрабатывать два разных типа шагов. Исполняемый файл и msi-пакет.
Скрипт установщика
Скрипт принимает несколько общих параметров:
param( [Parameter(Mandatory=$True)][string] $package, [switch] $noDownloadAndUnzip, [switch] $dryRun )
$package — это путь к файлу конфигурации. Я использую следующее соглашение об именах ‹package-name›.install.cfg.json, например uft1251.install.cfg.json.
Параметр $noDownloadAndUnzip в основном используется в целях разработки. Когда он установлен, скрипт перейдет прямо к выполнению шагов установки.
$dryRun просто прочитает конфигурационный файл и выведет его содержимое в удобочитаемой форме.
Чтение JSON
Первое, что делается в скрипте, это чтение json-файла:
function readJsonConfigFile($configFileName) { $currentDirectory = $PSScriptRoot $configFile = Join-Path -path $PSScriptRoot -ChildPath $configFileName if (-not (Test-Path -Path $configFile)) { logWrite ("Kunne ikke finde configfilen[" + $configFile + "]") return $false } $configContent = Get-Content -Path $configFile -Raw Try { $config = ConvertFrom-Json -InputObject $configContent return $config } Catch { $ErrorMessage = $_.Exception.Message logWrite ("Læsning af config-filen fejlede[" + $ErrorMessage + "]") return $false } }
В этой функции предполагается, что файл конфигурации находится в той же директории, что и скрипт-установщик. Основная часть скрипта — это CmdLet ConvertFrom-Json. Остальное — просто обработка ошибок.
Копируем файл и используем хэш
Хорошо. Откуда взялся этот Hash-объект в конфиге? Это из CmdLet Get-FileHash. По умолчанию используется алгоритм SHA256. текущий установочный скрипт может использовать только этот в данный момент, но при желании можно легко включить значение из конфигурационного файла.
Чтобы получить начальное хеш-значение:
PS C:\Temp> Get-FileHash HP_UFT_12.51_Package_For_The_Web_UFT1251_Setup.zip | Format-List Algorithm : SHA256 Hash : 1D41D4C3880D86FD22878F072D40F87C6277DE444B4FBA0C2E716C80ABAF7F8C Path : C:\Temp\HP_UFT_12.51_Package_For_The_Web_UFT1251_Setup.zip
Прежде чем что-либо копировать, я сначала проверяю, скачал ли уже файл. Это в основном полезно при разработке конфигурационного файла, так как вы, вероятно, не захотите загружать файл снова и снова. Следующая функция сравнит хэш из файла конфигурации с хэшем из загруженного файла.
function packageAlreadyDownloaded($config) { $localDownloadedZipFilePath = getLocalSetupZipFilePath($config) logWrite ("Testing whether the file[" + $localDownloadedZipFilePath + "] has already been downloaded.") $localDownloadZipFileExist = Test-Path -Path $localDownloadedZipFilePath if ($localDownloadZipFileExist) { $setupZipFileHash = Get-FileHash $localDownloadedZipFilePath logWrite ("Testing whether the file[" + $localDownloadedZipFilePath + "] has the same hash as specified in the config-file.") logWrite ("ConfigFileHash[" + $config.SetupZipFileHash.Hash + "]") logWrite ("Local FileHash [" + $setupZipFileHash.Hash + "]") if ($setupZipFileHash.Hash -eq $config.SetupZipFileHash.Hash) { logWrite ("The local file has a matching hash.") return $true } else { logWrite ("The local file hash does not match.") return $false } } else { logWrite ("The local file does not exist.") return $false } }
Распаковка файла
Мы распаковываем файл с помощью windowsShell и открываем zip-файл как папку. Самый важный шаг — удалить любой существующий файл перед распаковкой. В противном случае нас спросят, хотим ли мы перезаписать существующие файлы для каждого уже существующего файла. Это не диалоговое окно да/нет, которое может быть обработано параметром команды copyHere, а диалоговое окно пропуска/отмены.
Для команды copyHere мы могли бы использовать 20 в качестве второго параметра, что заставило бы i полностью замолчать. Это контролируемая установка (на данный момент), поэтому я предпочитаю, чтобы диалог состояния был виден. Особенно, когда мы распаковываем файл размером 1 гигабайт.
function Expand-ZIPFile ($file, $destination) { logBeginAction "decompressing the installfile" logwrite "Decompressing[$file] to[$destination]" $windowsShell = New-Object -ComObject shell.application $zip = $windowsShell.NameSpace($file) if (!$zip) { logWrite "Decompressing of [$file] failed" return "" } foreach($item in $zip.items()) { $itemPath = $item.Path logWrite "Decompressing: [$itemPath]" $destinationLeafFolder = Split-Path $itemPath -Leaf $destinationFolder = Join-Path -Path $destination -ChildPath $destinationLeafFolder if (Test-Path -Path $destinationFolder) { Remove-Item -Path $destinationFolder -Force -Recurse } $windowsShell.Namespace($destination).copyhere($itemPath) } logWrite ("Decompressing of[" + $file + "] finished") return $true }
Бег по шагам
Мы используем простой цикл, чтобы пройти все этапы. Для каждого шага мы определяем, является ли он «exe» или «msi»-шагом. Это вызовет «вспомогательную» функцию, которая установит правильные параметры для Start-Process CmdLet.
function runProcess($filePath, $arguments, $workingDirectory) { logWrite ("Start-Process filePath[$filePath], arguments[$arguments], workingDirectory[$workingdirectory]") Start-Process -FilePath $filePath -ArgumentList $arguments -WorkingDirectory $workingDirectory -Wait } function runMsiStep($localFolder, $step) { logWrite ("Executing MSI step: " + $step.Title) $filePath = "msiexec.exe" $file = Join-Path -path $localFolder -ChildPath $step.File $arguments = "/i " + $file + " /quiet " + $step.Arguments runProcess -filePath $filePath -arguments $arguments -workingDirectory $localfolder return $true } function runExeStep($localFolder, $step) { logWrite ("Executing EXE step: " + $step.Title) $filePath = Join-Path -path $localFolder -ChildPath $step.File $arguments = $step.arguments runProcess -filePath $filePath -arguments $arguments -workingDirectory $localfolder return $true } function runStep($localFolder, $step) { if ($step.Type -eq "msi") { return runMsiStep -localFolder $localFolder -step $step } elseif ($step.type -eq "exe") { return runExeStep -localFolder $localFolder -step $step } else { logWrite ("Unknown Step-Type[" + $step.Type + "]") return $false } } function runAllSteps($config) { $localInstallfolder = getLocalInstallFolder ($config) foreach($step in $config.Steps) { runStep -localFolder $localInstallfolder -step $step } }
Настройка скрытого установщика UFT
Подробную информацию об установке HPE UFT см. в Руководстве по установке HPE UFT. Для моего сценария я использую следующие два шага:
<downloadfolder>/setup.exe /InstallOnlyPrerequisite /s
При этом будут установлены все необходимые компоненты для запуска UFT, такие как .Net и т. д.
Следующим шагом является запуск фактического .msi-пакета:
msiexex /i <path to downloadfolder>/unified_functional_testing_x64.msi
Этот шаг требует множества аргументов, для моей конкретной настройки я использую следующее:
qb ADDLOCAL=\"Core_Components,IDE,Test_Results_Viewer,Help_Documents,ALM_Plugin,LeanFT_Engine,LeanFT_Client,ActiveX_Add_in,Web_Add_in,Java_Add_in,_Net_Add_in,WPF_Add_in,Silverlight_Add_in\" CONF_MSIE=1 ALLOW_RUN_FROM_ALM=1 ALLOW_RUN_FROM_SCRIPTS=1 LICSVR=almtest
Для вашей установки вам может потребоваться меньше или больше надстроек, и вы должны найти подробную информацию в Руководстве по установке HPE UFT.
Моя окончательная конфигурация выглядит так:
{ "Title": "HPE UFT 12.51", "Description": "Installerer HP UFT version 12.51", "SetupZipFile": "\\\\\\Install\\UFT\\12.51\\HP_UFT_12.51_Package_For_The_Web_UFT1251_Setup.zip", "SetupZipFileHash": { "Algorithm": "SHA256", "Hash": "1D41D4C3880D86FD22878F072D40F87C6277DE444B4FBA0C2E716C80ABAF7F8C" }, "LocalInstallFolder": "uft1251", "Steps": [ { "Title": "UFT Prerequisites", "Type": "exe", "File": "HP_UFT_12.51_Package_For_The_Web_UFT1251_Setup\\setup.exe", "Arguments": "/InstallOnlyPrerequisite /s" }, { "Title": "UFT", "Type": "msi", "File": "HP_UFT_12.51_Package_For_The_Web_UFT1251_Setup\\unified_functional_testing_x64.msi", "Arguments": "/qb ADDLOCAL=\"Core_Components,IDE,Test_Results_Viewer,Help_Documents,ALM_Plugin,LeanFT_Engine,LeanFT_Client,ActiveX_Add_in,Web_Add_in,Java_Add_in,_Net_Add_in,WPF_Add_in,Silverlight_Add_in\" CONF_MSIE=1 ALLOW_RUN_FROM_ALM=1 ALLOW_RUN_FROM_SCRIPTS=1 LICSVR=autopass" } ] } }
Дальнейшее развитие
Написание этого сценария было решением проблемы отказа от жизни в мире открытого исходного кода. В Linux или Mac OS X я бы, вероятно, просто создал небольшой скрипт, который загрузил бы Selnium. Но я живу в Windows-мире.
Я думаю, что есть несколько вариантов будущего. Один из них — Шоколад, а начиная с Windows 10 у нас будет OneGet. Оба варианта по-прежнему ограничены тем, что предлагается в общедоступных репозиториях. Но я предполагаю, что для предприятия решением было бы создать и поддерживать частный репозиторий. Тем не менее, вам, вероятно, придется создавать большинство пакетов самостоятельно.
Кроме того, улучшая скрипт, у меня есть следующее:
- Создание центрального репозитория пакетов и создание стандарта упаковки элементов.
- Создание локального файлового кэша
- Добавление типа cmd-step для дополнительных параметров
- Разрешение папок и файлов вместо только zip-файлов
- Поиск способа избежать распаковки файла снова и снова
Полный сценарий
<# .Description General Installer-script, which is based on a json-formatted configuration, for a given package. #> #Script arguments param( [Parameter(Mandatory=$True)][string] $package, [switch] $noDownloadAndUnzip, [switch] $dryRun ) function logWrite($comment) { Write-Host $comment } function logTitle($title) { logWrite "=============================================" logWrite "*** $title" logWrite "=============================================" } function logBeginAction($actionName) { logWrite "`r`n===Starting $actionName===`r`n" } function readJsonConfigFile($configFileName) { $currentDirectory = $PSScriptRoot $configFile = Join-Path -path $PSScriptRoot -ChildPath $configFileName if (-not (Test-Path -Path $configFile)) { logWrite ("Could not locate the configfile[" + $configFile + "]") return $false } $configContent = Get-Content -Path $configFile -Raw Try { $config = ConvertFrom-Json -InputObject $configContent #TODO: Verify Config return $config } Catch { $ErrorMessage = $_.Exception.Message logWrite ("Parsing the config-file failed[" + $ErrorMessage + "]") return $false } } function getLocalDownloadTempFolder($config) { $downloadFolder = Join-Path $env:TEMP $config.LocalInstallFolder return $downloadFolder } function getLocalInstallFolder($config) { $localInstallFolder = Join-Path $env:TEMP $config.LocalInstallFolder return $localInstallFolder } function getLocalSetupZipFilePath($config) { $localDownloadFolder = getLocalDownloadTempFolder($config) $setupZipFileSourceFileName = Split-Path $config.SetupZipFile -Leaf $localDownloadedZipFilePath = Join-Path -Path $localDownloadFolder -ChildPath $setupZipFileSourceFileName return $localDownloadedZipFilePath } function packageAlreadyDownloaded($config) { $localDownloadedZipFilePath = getLocalSetupZipFilePath($config) logWrite ("Testing whether the file[" + $localDownloadedZipFilePath + "] has already been downloaded.") $localDownloadZipFileExist = Test-Path -Path $localDownloadedZipFilePath if ($localDownloadZipFileExist) { $setupZipFileHash = Get-FileHash $localDownloadedZipFilePath logWrite ("Testing whether the file[" + $localDownloadedZipFilePath + "] has the same hash as specified in the config-file.") logWrite ("ConfigFileHash[" + $config.SetupZipFileHash.Hash + "]") logWrite ("Local FileHash [" + $setupZipFileHash.Hash + "]") if ($setupZipFileHash.Hash -eq $config.SetupZipFileHash.Hash) { logWrite ("The local file has a matching hash.") return $true } else { logWrite ("The local file hash does not match.") return $false } } else { logWrite ("The local file does not exist.") return $false } } function copyInstallPackage ($config){ logBeginAction "download of the installfile" $setupFileIsDownloadedAndIsTheSame = packageAlreadyDownloaded -config $config if ($setupFileIsDownloadedAndIsTheSame) { return getLocalSetupZipFilePath($config) } $destinationfolder = getLocalDownloadTempFolder($config) logWrite ("Downloading[" + $config.SetupZipFile + "to" + $destinationfolder + "]") if (-not (Test-Path -Path $config.SetupZipFile)) { logWrite ("Could not locate the installfile[" + $config.SetupZipFile + "]") return "" } if (Test-Path -Path $destinationFolder) { logWrite ("Deleting the local folder[" + $destinationFolder + "]") Remove-Item -Path $destinationFolder -Force -Recurse } logWrite ("Creating folder[" + $destinationFolder + "]") $createdDestinationfolder = New-Item $destinationFolder -type directory logWrite ("Starting copying of[" + $config.SetupZipFile + "]") $copiedFile = Copy-Item $config.SetupZipFile $destinationFolder $sourceFileName = Split-Path $config.SetupZipFile -Leaf $destinationFile = Join-Path -Path $destinationFolder -ChildPath $sourceFileName if (-not (Test-Path -Path $destinationFile)) { logWrite ("Copying of[" + $config.SetupZipFile + "] til[" + $destinationFolder + "] failed") return "" } logWrite ("Download of[" + $config.SetupZipFile + "] til[" + $destinationFile + "finished") return $destinationFile } function Expand-ZIPFile ($file, $destination) { logBeginAction "decompressing the installfile" logwrite "Decompressing[$file] to[$destination]" $windowsShell = New-Object -ComObject shell.application $zip = $windowsShell.NameSpace($file) if (!$zip) { logWrite "Decompressing of [$file] failed" return "" } foreach($item in $zip.items()) { $itemPath = $item.Path logWrite "Decompressing: [$itemPath]" $destinationLeafFolder = Split-Path $itemPath -Leaf $destinationFolder = Join-Path -Path $destination -ChildPath $destinationLeafFolder if (Test-Path -Path $destinationFolder) { Remove-Item -Path $destinationFolder -Force -Recurse } $windowsShell.Namespace($destination).copyhere($itemPath) } logWrite ("Decompressing of[" + $file + "] finished") return $true } function runProcess($filePath, $arguments, $workingDirectory) { logWrite ("Start-Process filePath[$filePath], arguments[$arguments], workingDirectory[$workingdirectory]") #Redirect stdoutput + redirect stderror Start-Process -FilePath $filePath -ArgumentList $arguments -WorkingDirectory $workingDirectory -Wait } function runMsiStep($localFolder, $step) { logWrite ("Executing MSI step: " + $step.Title) $filePath = "msiexec.exe" $file = Join-Path -path $localFolder -ChildPath $step.File $arguments = "/i " + $file + " /quiet " + $step.Arguments runProcess -filePath $filePath -arguments $arguments -workingDirectory $localfolder return $true } function runExeStep($localFolder, $step) { logWrite ("Executing EXE step: " + $step.Title) $filePath = Join-Path -path $localFolder -ChildPath $step.File $arguments = $step.arguments runProcess -filePath $filePath -arguments $arguments -workingDirectory $localfolder return $true } function runStep($localFolder, $step) { if ($step.Type -eq "msi") { return runMsiStep -localFolder $localFolder -step $step } elseif ($step.type -eq "exe") { return runExeStep -localFolder $localFolder -step $step } else { logWrite ("Unknown Step-Type[" + $step.Type + "]") return $false } } function runAllSteps($config) { $localInstallfolder = getLocalInstallFolder ($config) foreach($step in $config.Steps) { runStep -localFolder $localInstallfolder -step $step } } function describeInstallScript($config) { logTitle ("Installscript: " + $config.Title) logWrite ("Description: " + $config.Description) logWrite ("Installfiel: " + $config.SetupZipFile) logWrite ("Hash: (" + $config.SetupZipFileHash.Algorithm + "): " + $config.SetupZipFileHash.Hash) logWrite ("Temp download folder: " + (getLocalDownloadTempFolder $config)) foreach($step in $config.Steps) { logWrite ("`r`n-----------------------------------") logWrite ("STEP:" + $step.Title) logWrite ("-----------------------------------") logWrite ("TYPE:`r`n`t" + $step.Type) logWrite ("FILE:`r`n`t" + $step.File) logWrite ("ARGUMENTS:`r`n`t" + $step.Arguments) } } function install($package) { #TODO: exitcode $config = readJsonConfigFile -configFileName $package if (-not $config) { logWrite "Could not read config, could not complete installation" return } #Get local temp folder for storing the installationfiles. $downloadFolder = getLocalDownloadTempFolder $config if ($dryRun) { describeInstallScript $config return } logTitle ("Starting install of " + $config.Title) if ($noDownloadAndUnzip) { logWrite "You have chosen not to download or unzip anything and install immediately." } else { #Copy the installation file to the local folder $installLocation = copyInstallPackage $config $unzipFolder = getLocalInstallFolder -config $config #Extract the installation files. $unzipResult = Expand-ZIPFile $installLocation $unzipFolder } runAllSteps ($config) } install -package $package