<# .SYNOPSIS Fetches and installs sas given the install data provided. .DESCRIPTION Given a zip file containing entitlement data for an order, Install-Sas will fetch and install all packages that a user is entitled to .PARAMETER install Switch - when specified, run the install .PARAMETER config Switch - when specified, run the configuration .PARAMETER apply-license Switch - when specified, run apply-license. This cannot be specified with -install, -config, or -update .PARAMETER version Switch - when specified, display the version of the deployment tool, all other parameters are ignored .PARAMETER deployDataFile Path to the zip file containing the deployment data. If empty, will search in the root of the deployment tools directory. .PARAMETER installDir Directory to install the fetched packages to .PARAMETER downloadDir Directory to store downloaded packages in .PARAMETER noPrompt Switch - when specified, do not prompt before updating a package, has no meaning if -update is not specified. .PARAMETER update Switch - when specified, update packages if updates are available .EXAMPLE Install-Sas .NOTES If neither -install nor -config is specified, both will be run #> function Install-Sas() { [CmdletBinding()] param ( [switch] $install = $false, [switch] $config = $false, [switch] ${apply-license} = $false, [switch] $version = $false, [string] $deployDataFile, [string] $installDir = $(Join-Path $env:ProgramFiles "SAS"), [string] $downloadDir, [switch] $noPrompt, [switch] $update, [switch] $skipservicecheck, [string] $installLogDir = $null ) Set-StrictMode -Version 5 # Set directory tracking structure $staticFile = Join-Path $PSScriptRoot "static_vars.psd1" $staticVars = Import-PowerShellDataFile $staticFile $statics = [StaticVariables]::new($staticVars) $hierarchy = [ViyaHierarchy]::new($installDir, $statics.deploymentId, $PSScriptRoot) # Refers to the location of the deployment tools in the order, not in the install destination. $deploymentToolsRootDir = $hierarchy.GetDeploymentToolsRoot() # If version switch is passed in, display version and then exit tool. if ($version.IsPresent) { $statics.version exit 0 } if ($null -eq $downloadDir -or '' -eq $downloadDir) { $downloadDir = $(Join-Path $deploymentToolsRootDir "Downloads") } $timestamp = Get-Date -Format FileDateTime $logFilename = "deploy-$timestamp.log" $logFile = Join-Path $deploymentToolsRootDir $logFilename Start-Transcript -Path $logFile # The -apply-license option cannot be used with the -install, -config, or -update options. if (${apply-license} -eq $true -and ($install -eq $true -or $config -eq $true -or $update -eq $true)) { Write-Error "The -apply-license option must be used without the -install, -config, and -update options." exit 1 } # If neither install, config, update, nor apply-license are selected, default to performing the actions install and config if ($update -eq $false -and $install -eq $false -and $config -eq $false -and ${apply-license} -eq $false) { $install = $true $config = $true } $date = Get-Date Write-Host "Deployment started at $date" # Import deploymnent library modules ImportRequiredModules $hierarchy.GetDeploymentModuleLibrary() # Load SAS runtime modules and install module directory to the system PSModulePath AddRuntimeModulePath -modulePath $hierarchy.GetRuntimeModulesDir() # Load user variables $file = Join-Path $deploymentToolsRootDir "vars.psd1" $userVars = Import-PowerShellDataFile $file if ($null -eq $deployDataFile -or '' -eq $deployDataFile) { $deployDataFile = Join-Path $deploymentToolsRootDir "SAS_Viya_deployment_data.zip" } #test installLogDir inputs if (($installLogDir -eq $null) -or ($installLogDir -eq "")){ $installLogDir = $hierarchy.GetInstallLogDir() } Verify-Path -path $installLogDir # define SAS Deployment Data content paths $entCertDir = $deploymentToolsRootDir $caCertDir = $deploymentToolsRootDir $licenseDir = $deploymentToolsRootDir $licensePath = Join-Path $licenseDir $userVars.LICENSE_FILENAME $licenseOK = ValidateLicense -path $licensePath Write-Host "Found license file: '$licensePath'" $compositeLicensePath = Join-Path $licenseDir $userVars.COMPOSITE_LICENSE_FILENAME Write-Host "Found composite license file: '$compositeLicensePath'" if (!$licenseOk) { Write-Error "The license file '$licensePath' is incorrect, cannot continue." exit 1 } # define and verify certificate paths $caCertPath = Join-Path $caCertDir $statics.caCert if (!(Test-Path -Path $caCertPath)) { Write-Error "Deployment data for your order cannot be found.`nERROR: The '$caCertPath' file does not exist." exit 1 } $entCertPath = Join-Path $entCertDir $statics.entitlementCert if (!(Test-Path -Path $entCertPath)) { Write-Error "Deployment data for your order cannot be found.`nERROR: The '$entCertPath' file does not exist." exit 1 } $selectedProducts = New-Object System.Collections.ArrayList # If environment file found, get selected packages $environmentDataFile = Join-Path $deploymentToolsRootDir "env.psd1" if ((Test-Path $environmentDataFile) -eq $true) { $environmentData = Import-LocalizedData -BaseDirectory $deploymentToolsRootDir -FileName "env.psd1" $productGroups = $environmentData.ProductGroups | Select-Object -Unique foreach ($productGroup in $productGroups) { [string] $dir = Join-Path $deploymentToolsRootDir "product-groups" [PSObject] $productsForGroup = Import-LocalizedData -BaseDirectory $dir -FileName "$productGroup.psd1" foreach ($product in $productsForGroup) { if ($selectedProducts -notcontains $product) { $selectedProducts.Add($product) > $null } } } } else { Write-Error "ERROR: Cannot find enviroment data file '$environmentDataFile'" exit 1 } if ($install -or $update) { # check for running services, terminate if found unless -skipservicecheck is specified if (($skipservicecheck -eq $false) -and (ServicesAreRunning -eq $true)) { $statusCode = 1 TerminateGracefully $statusCode "No action performed." } #install saspm from entitled repo Import-Module (Join-Path $deploymentToolsRootDir "\library\SasPMInstaller") -Force $retcode = Install-SasPM -statics $statics -deployroot $deploymentToolsRootDir -installDir $installDir if ($retcode -ne 0){ TerminateGracefully -errorCode $retcode -message "Install failed. Problem installing SAS Product Manager." } # distribute artifacts to the system. Add-UninstallKey -filepath $hierarchy.GetModulesDir() -installPath $installDir -version $statics.viyaVersion Install-DeploymentModules -sourceDir $deploymentToolsRootDir -destinationDir $hierarchy.GetModulesDir() -Verbose:$VerbosePreference # Required parameters for installing packages [hashtable] $params = @{ 'downloadDir' = $downloadDir; 'installDir' = $installDir; 'installLogDir' = $installLogDir; 'caCertPath' = $caCertPath; 'entCertPath' = $entCertPath; 'rootDir' = $deploymentToolsRootDir; } # If repository file found, supply reposity file for installing packages $repositoryFile = Join-Path $deploymentToolsRootDir "package-repositories.json" if ((Test-Path $repositoryFile) -eq $true) { $params.Add('repositoryFile', $repositoryFile) } # Install packages # assure the list of packages to install is unique $uniqueUnits = $selectedProducts.UnitName | Select-Object -Unique $params.Add('packageList', $uniqueUnits) $stage = "" $statusCode = 0 if($install){ $statusCode = Install-SasPackage @params -Verbose:$VerbosePreference $stage = "install" } if($update){ if($noPrompt -eq $true){ $params['noprompt'] = $true } $statusCode = Update-SasPackage @params -Verbose:$VerbosePreference $stage = "update" } Write-Host "StatusCode is '$statusCode' in SasDeploymentController" if ($statusCode -eq 0 -or $statusCode -eq 3010){ $meteredBillingAgentExe = "sas-mbagent.exe" $deploymentId = $statics.deploymentId $meteredBillingAgent = Join-Path $env:ProgramFiles "SAS\$deploymentId\bin\$meteredBillingAgentExe" $argumentList = @( "deployment", "environment" ) if ((Test-Path $meteredBillingAgent) -eq $true) { $statusCode = RunProcess $meteredBillingAgent $argumentList Write-Host "Metered billing task completed with return code '$statusCode'" if($statusCode -ne 0){ TerminateGracefully $statusCode "Metered Billing Agent failed." } else { Write-Host "Metered billing agent task completed successfully." } } } # To manage update/upgrade we need to change service start types to manual. ModifyServiceStartTypes -deploymentId $statics.deploymentId # needs to happen after running the metered billing agent in case of reboot for locked files # will exit when return code is not 0 or not 3010 ProcessMSIStatusCode -statusCode $StatusCode -DeploymentStage $stage -deploymentId $statics.deploymentId } $configTasksDir = Join-Path $deploymentToolsRootDir "config" $sequencedProductsToConfigure = GetProductsToConfigure -configTasksDir $configTasksDir -selectedProductsList $selectedProducts if ($config) { Publish-Configuration -configTasksDir $configTasksDir ` -productsToConfigure $sequencedProductsToConfigure ` -installDir $installDir ` -licenseFile $licensePath ` -compositeLicenseFile $compositeLicensePath ` -programDataConfig $hierarchy.GetConfigDir() ` -publicConfigDir $hierarchy.GetPublicSasDir() ` -staticVars $statics ` -userVars $userVars ` -Verbose:$VerbosePreference } if (${apply-license}) { Update-License -configTasksDir $configTasksDir ` -productsToConfigure $sequencedProductsToConfigure ` -installDir $installDir ` -licenseFile $licensePath ` -compositeLicenseFile $compositeLicensePath ` -programDataConfig $hierarchy.GetConfigDir() ` -publicConfigDir $hierarchy.GetPublicSasDir() ` -staticVars $statics ` -userVars $userVars ` -Verbose:$VerbosePreference } $date = Get-Date Write-Host "Deployment completed at $date" TerminateGracefully 0 "Install successful." } <# .SYNOPSIS Unconfigures and then uninstalls installed software .DESCRIPTION Scans the install directory for uninstall metadata, and then run unconfigure where needed, and uninstall on all packages. If neither -uninstall nor -config are specified, both uninstall and unconfigure will be run. .PARAMETER uninstall Switch - when specified, run the uninstall .PARAMETER config Switch - when specified, run the unconfigure .PARAMETER installDir Path to the install directory .EXAMPLE Remove-Sas .NOTES If neither -uninstall nor -config is specified, both will be run #> function Remove-Sas { [CmdletBinding()] param ( [switch] $uninstall = $false, [switch] $config = $false, [string] $installDir = $(Get-InstallDir), [string] $installLogDir=$null ) Set-StrictMode -Version 5 if ($installDir -eq ''){ $installDir = Get-InstallDir } if ($installDir -eq ''){ #quit since the installDir is unknown TerminateGracefully -message "Unable to determine the install directory to remove. Re-run with -installDir parameter." } # If neither install or config is selected, default to performing the actions install and config if ($uninstall -eq $false -and $config -eq $false) { $uninstall = $true $config = $true } $staticFile = Join-Path $PSScriptRoot "static_vars.psd1" $staticVars = Import-PowerShellDataFile $staticFile $statics = [StaticVariables]::new($staticVars) $hierarchy = [ViyaHierarchy]::new($installDir, $statics.deploymentId, $PSScriptRoot) $deploymentToolsRootDir = $hierarchy.GetDeploymentToolsRoot() $timestamp = Get-Date -Format FileDateTime $logFilename = "remove-$timestamp.log" $logFile = Join-Path $hierarchy.GetDeploymentToolsRoot() $logFilename Start-Transcript -Path $logFile #test installLogDir inputs if (($installLogDir -eq $null) -or ($installLogDir -eq "")){ #set to default location $installLogDir = $hierarchy.GetInstallLogDir() } Verify-Path -Path $installLogDir # Import deploymnent library modules ImportRequiredModules $hierarchy.GetDeploymentModuleLibrary() # Load SAS runtime modules and install module directory to the system PSModulePath AddRuntimeModulePath -modulePath $hierarchy.GetRuntimeModulesDir() # Load user variables $file = Join-Path $hierarchy.GetDeploymentToolsRoot() "vars.psd1" $userVars = Import-PowerShellDataFile $file $installedProducts = Get-InstalledProducts $installedProductNames = $installedProducts.ProductName $selectedProducts = New-Object System.Collections.ArrayList # If environment file found, get selected packages $environmentDataFile = Join-Path $deploymentToolsRootDir "env.psd1" if ((Test-Path $environmentDataFile) -eq $true) { $environmentData = Import-LocalizedData -BaseDirectory $deploymentToolsRootDir -FileName "env.psd1" $productGroups = $environmentData.ProductGroups | Select-Object -Unique foreach ($productGroup in $productGroups) { [string] $dir = Join-Path $deploymentToolsRootDir "product-groups" [PSObject] $productsForGroup = Import-LocalizedData -BaseDirectory $dir -FileName "$productGroup.psd1" foreach ($product in $productsForGroup) { AddSelectedProduct -product $product -installedNames $installedProductNames -selected $selectedProducts } } } else { Write-Error "ERROR: Cannot find enviroment data file '$environmentDataFile'" exit 1 } if ($config) { $configTasksDir = Join-Path $deploymentToolsRootDir "config" $sequencedProductsToUnConfigure = GetProductsToConfigure -configTasksDir $configTasksDir -selectedProductsList $selectedProducts [array]::Reverse($sequencedProductsToUnConfigure) Remove-Configuration -configTasksDir $configTasksDir ` -productsToConfigure $sequencedProductsToUnConfigure ` -installDir $installDir ` -licenseFile "nolicense" ` -programDataConfig $hierarchy.GetConfigDir() ` -publicConfigDir $hierarchy.GetPublicSasDir() ` -staticVars $statics ` -userVars $userVars ` -Verbose:$VerbosePreference } if ($uninstall) { Write-Host "Uninstalling SAS software" $statusCode = Uninstall-SasPackage -installedProducts $installedProducts -installDir $installdir -installLogDir $installLogDir ` -rootDir $deploymentToolsRootDir -Verbose:$VerbosePreference ProcessMSIStatusCode -statusCode $StatusCode -DeploymentStage "uninstall" -deploymentId $statics.deploymentId # Remove install module directory from the system PSModulePath, this is ok becasue we do not support seletive uninstall # TODO: should really call this when we no all products were uninstalled DeleteRuntimeModulePath -modulePath $hierarchy.GetRuntimeModulesDir() if ($null -eq $installedProducts -or $installedProducts.Length -eq 0) { Write-Host "There were no products installed to uninstall." } } Remove-UninstallKey TerminateGracefully 0 "Uninstall successful." } function ImportRequiredModules { param( [string] $moduleLib ) if(-Not($env:PsModulePath -like $moduleLib)){ $env:PsModulePath += ";$moduleLib" } Import-Module SasInstaller -Force Import-Module SasConfiguration -Force } function Get-InstallDir { #The install dir we use will be the one listed with saspm. This is because #saspm should always be there to reference. We search on the package name #rather than the legal name, since the legal name can change. $SASRegistryPath = "HKLM:\SOFTWARE\SAS Institute Inc." $sasRegKeys = Get-ChildItem $SASRegistryPath foreach ($key in $sasRegKeys) { $productName = $key.GetValue("ProductName") if($productName -eq "sas-saspmforwin"){ $dir = $key.GetValue("InstallLocation") return $dir.TrimEnd('\\') } } return '' } function ValidateLicense { param( [Parameter(Mandatory = $true)] [string] $path ) foreach ($entry in Get-Content -path $Path) { # test for WX64 at the beginning of OSNANE if ($entry -match 'OSNAME=.WX64'){ Write-Information "Found Match: $Matches[1]" return $true } } Write-Error "The license file is not for Windows systems." return $false } # # Adds a product to the selected products list only if it is installed and # not already in the selected products list. # # Parameters: # product - the product to be added # installedNames - an array of names of installed products # selected - the list of selected products # function AddSelectedProduct { param( [System.Collections.Hashtable] $product, [string[]] $installedNames, [System.Collections.ArrayList] $selected ) # If the product is not installed, do not add it if ($installedNames -notcontains $product.UnitName) { return } # If the list is empty, add the product if ($selected.Count -eq 0) { $selected.Add($product) > $null return } # If the product is not already in the list, add it $selectedNames = $selected.UnitName if ($selectedNames -notcontains $product.UnitName) { $selected.Add($product) > $null } } # # Gets a list of products to be configured or unconfigured, in the correct sequence based # on the order in config-start-sequence.psd1. # # Parameters: # configTasksDir - path to the directory where config-start-sequence.psd1 is found # selectedProductsList - the list of products selected for configuration or unconfiguration # function GetProductsToConfigure { param( [String] $configTasksDir, [System.Collections.ArrayList] $selectedProductsList ) $productsToConfigure = Import-LocalizedData -BaseDirectory $configTasksDir -FileName "config-start-sequence.psd1" # Grabs all matches of productsToConfigure in selectedProducts retaining order of productsToConfigure. $initialProductsToConfigure = $productsToConfigure.sequence | Select-String -Pattern $selectedProducts.AppName -SimpleMatch $selectedProductsArray = $selectedProductsList.ToArray() $productList = New-Object System.Collections.Arraylist($null) # need to handle exact product matches like 1) sasstudio, sasstudiov; 2)templates and reporttemplates # need to maintain sequence order from productsToConfigure foreach ($product in $initialProductsToConfigure){ if ([string]::IsNullOrEmpty($product)) { continue } foreach ($selectedProd in $SelectedProductsArray) { $selectedProdName = $selectedProd.AppName if ([string]::IsNullOrEmpty($selectedProdName)) { continue } # need to use join to remove any blanks at end and to get all chars in the hash if (($product -join '') -eq ($selectedProdName -join '')) { if ($productList -notcontains $product) { $productList.Add($product) > $null } } } } return $productList.ToArray() } function TerminateGracefully { param( [int] $errorCode, [string] $message ) if ($message.Length -gt 0){ if ($errorCode -ne 0) { Write-Warning "$message" }else { Write-Host "$message" } } Write-Host "Deployment finished with return code '$errorCode'" Stop-Transcript exit $errorCode } function ServicesAreRunning { $services = Get-Service -DisplayName "SAS *" | Where Status -ne "Stopped" if ($services -ne $null) { Write-Host "$($services.count) Viya services are still running." -ForegroundColor Yellow Write-Host "Please shut down all Viya services before an install or update." -ForegroundColor Yellow Write-Host "See 'General Servers and Services: Start and Stop All Servers and Services' in the 'SAS Viya Administration' documentation for instructions on shutting down Viya services." -ForegroundColor Yellow return $true } return $false } function ProcessMSIStatusCode { param( [int] $StatusCode, [string] $DeploymentStage, [String] $deploymentId ) Write-Host "ProcessMSIStatusCode statusCode is '$statusCode'" # 3010 MSI code means success. But, the machine must reboot to complete the install/update/uninstall due to locked files. if ($StatusCode -eq 3010){ $rebootMsg = "You must reboot in order to complete $DeploymentStage." if ($DeploymentStage -eq "install" -or $DeploymentStage -eq "update") { # set sas-viya-all-services to manual start for reboot $service = "sas-$deploymentId-all-services" $allService = Get-Service -Name $service if (($null -ne $allService) -and ($allService.StartType -ne 'Manual')) { Set-Service -Name $service -StartupType Manual $rebootMsg = "$rebootMsg After reboot, you must run 'setup.bat -config' to complete the deployment." } } TerminateGracefully $StatusCode $rebootMsg } # remaining non-zero need to terminate. 0 will continue. if ($statusCode -ne 0){ TerminateGracefully $StatusCode "'$DeploymentStage' failed." } } # # Function to modify all SAS services to manual startup, except for the SAS Service # Manager service (sas--all-services), which is set to automatic if # is is not already automatic. Note that during an update/upgrade, if a reboot is # required, the SAS Service Manager will be set back to manual to prevent all of the # services from being started after the reboot before configuration runs. That is # done in ProcessMSIStatusCode, so this function must be called before that to avoid # un-doing what it did. # # Parameters: # deploymentID - the deployment ID # function ModifyServiceStartTypes() { param( [String] $deploymentId ) $all = "sas-$deploymentId-all-services" $httpd = 'Apache2.4' $services = Get-Service -Name 'sas*' $count = $services.Count $changed = 0 $httpdService = Get-Service -Name $httpd -ErrorAction SilentlyContinue if ($null -ne $httpdService) { $count = $count + 1 } Write-Host "Task: changing service start types." -ForegroundColor Green Write-Host "Checking $count services" if (($null -ne $httpdService) -and ($httpdService.StartType -ne 'Manual')) { Write-Verbose "Setting service '$httpd' to Manual startup" Set-Service -Name $httpd -StartupType Manual $changed = $changed + 1 } foreach ($service in $services) { $name = $service.Name $startType = $service.StartType # Special handling for sas--all-services if ( $name -eq $all ) { $allService = Get-Service -Name $all #reset to automatic if it's not already automatic if (($null -ne $allService) -and ($allService.StartType -ne 'Automatic')) { Write-Verbose "Setting service '$name' to Automatic startup" Set-Service -Name $all -StartupType Automatic $changed = $changed + 1 continue } Write-Host "Skipping service $all" continue } # For all others, set the start type to manual if ($startType -ne 'Manual') { Write-Verbose "Setting service '$name' to Manual startup" Set-Service -Name $name -StartupType Manual $changed = $changed + 1 } } $plural = 's' if ($changed -eq 1) { $plural = '' } Write-Host "Changed $changed service$plural" Write-Host "Task Complete." -ForegroundColor Green } function Verify-Path() { param( [String] $path ) if (!(Test-Path $path)) { try{ $ret = New-Item -ItemType directory -Path $path if ($ret -eq $null){ TerminateGracefully -errorCode 1 -message "Could not create path $path" } } catch{ TerminateGracefully -errorCode 1 -message "Could not create path $path" } } $tmpFileName = "tmp-"+[guid]::NewGuid() $tmpFilePath = (Join-Path $path $tmpFileName) try { # Try to add a new file $ret = New-Item -ItemType File -Path $tmpFilePath if ($ret -eq $null){ TerminateGracefully -errorCode 1 -message "Could not write to path $path" } # If we got this far, adding a new file was successful, so let's clean it up Remove-Item -ErrorAction SilentlyContinue $tmpFilePath } catch { TerminateGracefully -errorCode 1 -message "Could not write to path $path" } }