tests/build/Install-VstsAgentOnWindowsServerCoreContainer.ps1
######################################################################################################################## # Script Disclaimer ######################################################################################################################## # This script is not supported under any Microsoft standard support program or service. # This script is provided AS IS without warranty of any kind. # Microsoft disclaims all implied warranties including, without limitation, any implied warranties of # merchantability or of fitness for a particular purpose. The entire risk arising out of the use or # performance of this script and documentation remains with you. In no event shall Microsoft, its authors, # or anyone else involved in the creation, production, or delivery of this script be liable for any damages # whatsoever (including, without limitation, damages for loss of business profits, business interruption, # loss of business information, or other pecuniary loss) arising out of the use of or inability to use # this script or documentation, even if Microsoft has been advised of the possibility of such damages. <# .SYNOPSIS This scripts configures a Windows Server Core based container with Terraform, json2hcl, the selected PowerShell modules and installs and configures the Visual Studio Team Services build agent on it. .DESCRIPTION This scripts configures a Windows Server Core based container (with the latest version of the microsoft/windowsservercore LTSC image available on Docker Hub), with the latest version of the Azure DevOps agent, Terraform, json2hcl and the selected PowerShell modules (by default Az, AzureAD, Pester). This container is intended to be run as an Azure Container Instance. After the successfully configuration, it prints the available disk space, and keeps periodically checking of the vstsagent service is in a running state, keeping the container alive by that. .PARAMETER VSTSAccountName Name of the Azure DevOps account - formerly Visual Studio Team Services (VSTS) account, e.g. https://<Azure DevOps Account Name>.visualstudio.com - OR - https://dev.azure.com/<Azure DevOps Account Name>/ .PARAMETER PATToken PAT token generated by the user who is configuring the container to be used by VSTS. .PARAMETER AgentNamePrefix Prefix of the name of the agent shown on the Azure DevOps (VSTS) portal. .PARAMETER PoolName Name of the Agent pool. It defaults to the "Default" pool when not defined. .PARAMETER RequiredPowerShellModules List of the required PowerShell modules, e.g. Az, AzureAD, Pester .PARAMETER InstallAzureCli Switch to define whether or not you want to install the Azure CLI on your container. .PARAMETER InstallPowerShellCore Switch to define whether or not you want to install Azure PowerShell Core on your container. .PARAMETER UseChocolatey Switch to define whether or not Chocolatey should be used to install the supported components .EXAMPLE .\Install-VstsAgentWindowsServerCoreContainer.ps1 -VSTSAccountName "<Azure DevOps account Name>" -PATToken "<PAT Token value>" This installs all the components with the default configuration (Default Agent Pool, "Az", "AzureAD", "Pester" PowerShell modules, randomly generated agent name). .EXAMPLE .\Install-VstsAgentWindowsServerCoreContainer.ps1 -VSTSAccountName "<Azure DevOps account Name>" -PATToken "<PAT Token value>" -AgentNamePrefix "<prefix of the Azure DevOps agent's name>" -PoolName "CoreContainers" This installs all the components with the defined Agent name, and Pool name, with the default PowerShell modules. .EXAMPLE .\Install-VstsAgentWindowsServerCoreContainer.ps1 -VSTSAccountName "<Azure DevOps account Name>" -PATToken "<PAT Token value>" -AgentNamePrefix "<prefix of the Azure DevOps agent's name>" -PoolName "CoreContainers" -RequiredPowerShellModules "Az", "AzureAD", "Pester" This installs all the components with the defined Agent name, Pool name, and PowerShell modules. .INPUTS <none> .OUTPUTS <none> .NOTES Version: 1.0 Author: Mate Barabas, Andrew Auret Creation Date: 2018-08-23 References: The Install-VstsAgent function is a slightly modified version of the provisioning script available as part of Azure DevTest Labs (available in August 2018). #> param ( [Parameter(Mandatory = $true, HelpMessage = "Name of the Visual Studio Team Services Account (VSTS), e.g. https://<VSTSAccountName>.visualstudio.com")] [ValidateNotNullOrEmpty()] [string]$VSTSAccountName, [Parameter(Mandatory = $true, HelpMessage = "PAT token generated by the user who is configuring the container to be used by VSTS.")] [ValidateNotNullOrEmpty()] [string]$PATToken, [Parameter(Mandatory = $false, HelpMessage = "Prefix of the name of the agent shown on the VSTS portal.")] [ValidateNotNullOrEmpty()] [string]$AgentNamePrefix, [Parameter(Mandatory = $false, HelpMessage = "Name of the Agent pool. It defaults to the ""Default"" pool when not defined.")] [ValidateNotNullOrEmpty()] [string]$PoolName = "Default", [Parameter(Mandatory = $false, HelpMessage = "List of the required PowerShell modules, e.g. Az, AzureAD, Pester")] [ValidateNotNullOrEmpty()] [array]$RequiredPowerShellModules = @("Az", "AzureAD", "Pester"), [Parameter(Mandatory = $false, HelpMessage = "Switch to define whether or not you want to install the Azure CLI on your container.")] [ValidateNotNullOrEmpty()] [string]$InstallAzureCli, [Parameter(Mandatory = $false, HelpMessage = "Switch to define whether or not you want to install Azure PowerShell Core on your container.")] [ValidateNotNullOrEmpty()] [string]$InstallPowerShellCore, [Parameter(Mandatory = $false, HelpMessage = "Switch to define whether or not Chocolatey should be used to install the supported components")] [ValidateNotNullOrEmpty()] [string]$UseChocolatey ) #region Variable conversion (from text to boolean) $InstallAzureCli = switch ($InstallAzureCli) { '1' { $true } 'true' { $true } '$true' { $true } '0' { $false } 'false' { $false } '$false' { $false } Default { $false } } $InstallPowerShellCore = switch ($InstallPowerShellCore) { '1' { $true } 'true' { $true } '$true' { $true } '0' { $false } 'false' { $false } '$false' { $false } Default { $false } } $UseChocolatey = switch ($UseChocolatey) { '1' { $true } 'true' { $true } '$true' { $true } '0' { $false } 'false' { $false } '$false' { $false } Default { $false } } #endregion #region Functions function Install-Nuget { if (-not (Get-PackageProvider -Name "Nuget" -ListAvailable -ErrorAction SilentlyContinue)) { Write-Output "-----------------------------------------------------------------------------------" $NewPackageProvider = Find-PackageProvider -Name "Nuget" $NewPackageProviderVersion = $NewPackageProvider.Version.ToString() Write-Output "Installing Nuget package provider ($NewPackageProviderVersion)..." Install-PackageProvider -Name "Nuget" -Force -Confirm:$false | Out-Null if (Get-PackageProvider "Nuget") { Write-Output "Nuget package provider ($NewPackageProviderVersion) successfully installed." } else { Write-Error "Nuget package provider ($NewPackageProviderVersion) installation failed." } Write-Output "Waiting 10 seconds..." Start-Sleep -Seconds 10 } } function Install-PowerShellModules { param ( [array]$RequiredModules ) Write-Output "-----------------------------------------------------------------------------------" Write-Output "PowerShell modules to install: $($RequiredModules -join ", ")" foreach ($Module in $RequiredModules) { if (-not (Get-Module $Module -ErrorAction SilentlyContinue)) { Write-Output "-----------------------------------------------------------------------------------" Write-Output "Getting $Module module..." $NewModule = Find-Module $Module $NewModuleVersion = $NewModule.Version.ToString() Write-Output "Installing $Module ($NewModuleVersion) module..." Install-Module -Name $Module -Force -Confirm:$false -SkipPublisherCheck -AllowClobber } } if (Get-Module -ListAvailable | Where-Object { $_.Name -eq "Az.Accounts" }) { Write-Output "Enabling AzureRm compatibility mode" Enable-AzureRmAlias -Scope LocalMachine -Confirm:$false } } function Install-Choco { Write-Output "-----------------------------------------------------------------------------------" Write-Output "Installing Chocolatey..." Invoke-WebRequest -Uri "https://chocolatey.org/install.ps1" -OutFile "c:\chochoinstall.ps1" .\chochoinstall.ps1 | Out-Null } function Install-ChocoTerraform { Write-Output "-----------------------------------------------------------------------------------" Write-Output "Installing Terraform with Chocolatey..." choco install terraform -y --limit-output --no-progress } function Install-Terraform { param ( [Parameter(Mandatory = $false, HelpMessage = "Use this parameter to decide if the absolute latest or the latest stable Terraform release should be installed.")] [ValidateNotNullOrEmpty()] [bool]$SkipNonStableReleases = $true ) # Get the list of available Terraform versions $Response = Invoke-WebRequest -Uri "https://releases.hashicorp.com/terraform" -UseBasicParsing # Find the latest version if ($SkipNonStableReleases -eq $true) { $Links = $Response.Links | Where-Object { $_.href.Split("/")[2] -match "^(\d|\d\d)\.(\d|\d\d)\.(\d|\d\d)$" } $LatestTerraformVersion = $Links[0].href.Split("/")[2] } else { $LatestTerraformVersion = $Response.Links[1].href.Split("/")[2] } $Version = $LatestTerraformVersion # Find the download URL for the latest version $Response = Invoke-WebRequest -Uri "https://releases.hashicorp.com/terraform/$Version" -UseBasicParsing $RelativePath = ($Response.Links | Where-Object { $_.href -like "*windows_amd64*" }).href # URL will be similar to this: "https://releases.hashicorp.com/terraform/0.11.8/terraform_0.11.8_windows_amd64.zip" $URL = "https://releases.hashicorp.com$RelativePath" # Create folder $FileName = Split-Path $url -Leaf $FolderPath = "C:\terraform" $FilePath = "$FolderPath\$FileName" New-Item -ItemType Directory -Path $FolderPath -ErrorAction SilentlyContinue | Out-Null # Download and extract Terraform, remove the temporary zip file Write-Output "-----------------------------------------------------------------------------------" Write-Output "Downloading Terraform ($Version) to $FolderPath..." Start-BitsTransfer -Source $URL -Destination $FilePath Expand-Archive -LiteralPath $FilePath -DestinationPath $FolderPath Remove-Item -Path $FilePath # Setting PATH environmental variable for Terraform Write-Output "Setting PATH environmental variable for Terraform..." # Get the PATH environmental Variable $Path = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path # Create New PATH environmental Variable $NewPath = $Path + ";" + $FolderPath # Set the New PATH environmental Variable Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $NewPath $env:Path += $NewPath # Verify the Path # (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path } function Install-Json2Hcl { # Get the list of available Terraform versions $Response = Invoke-WebRequest -Uri "https://github.com/kvz/json2hcl/releases" -UseBasicParsing # Find the latest version $RelativePathToLatestVersion = (($Response.Links | Where-Object { $_.href -like "*windows_amd64*" }).href)[0] $Version = $RelativePathToLatestVersion.Split("/")[-2] # URL will be similar to this: "https://github.com/kvz/json2hcl/releases/download/v0.0.6/json2hcl_v0.0.6_windows_amd64.exe" $URL = "https://github.com/$RelativePathToLatestVersion" # Create folder $FileName = Split-Path $url -Leaf $FolderPath = "C:\json2hcl" $FilePath = "$FolderPath\$FileName" New-Item -ItemType Directory -Path $FolderPath -ErrorAction SilentlyContinue | Out-Null # Download and extract Json2HCL Write-Output "-----------------------------------------------------------------------------------" Write-Output "Downloading Json2HCL ($Version) to $FolderPath..." $WebClient = New-Object System.Net.WebClient $WebClient.DownloadFile($URL, $FilePath) Rename-Item -Path $FolderPath\$FileName -NewName "json2hcl.exe" # Setting PATH environmental variable Write-Output "Setting PATH environmental variable for Json2HCL..." # Get the PATH environmental Variable $Path = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path # Create New PATH environmental Variable $NewPath = $Path + ";" + $FolderPath # Set the New PATH environmental Variable Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $NewPath $env:Path += $NewPath } function Install-ChocoAzureCli { Write-Output "-----------------------------------------------------------------------------------" Write-Output "Installing Azure CLI with Chocolatey..." choco install azure-cli -y --limit-output --no-progress } function Install-AzureCli { Write-Output "-----------------------------------------------------------------------------------" Write-Output "Searching for the latest version of Azure CLI" $AzureCliUrl = "https://aka.ms/installazurecliwindows" $AzureCliInstallerFullPath = "C:\azurecli.msi" $response = Invoke-WebRequest -UseBasicParsing -Uri $AzureCliUrl -Method Head $AzureCliInstallerFileName = $response.BaseResponse.ResponseUri.AbsolutePath.Split("/")[-1] Write-Output "Downloading Azure CLI installer ($AzureCliInstallerFileName)" $WebClient = New-Object System.Net.WebClient $WebClient.DownloadFile($AzureCliUrl, $AzureCliInstallerFullPath) if (Test-Path $AzureCliInstallerFullPath) { Write-Output "Installing Azure CLI ($AzureCliInstallerFileName)" Start-Process msiexec.exe -Wait -ArgumentList "/i $AzureCliInstallerFullPath /quiet /passive /qn" $AzureCli = (Get-WmiObject -Class win32_product) | Where-Object { $_.name -like "*Microsoft Azure CLI*" } if ($AzureCli) { Write-Output "Azure CLI (version $($AzureCli.Version)) was successfully installed" Remove-Item -Path $AzureCliInstallerFullPath -Force -Confirm:$false # Setting PATH environmental variable Write-Output "Setting PATH environmental variable for Azure CLI..." # Get the PATH environmental Variable $Path = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path # Create New PATH environmental Variable $NewPath = $Path + ";" + "C:\Program Files (x86)\Microsoft SDKs\Azure\CLI2\wbin" # Set the New PATH environmental Variable Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $NewPath $env:Path += $NewPath } else { Write-Error "Azure CLI could not be installed" } } } function Install-ChocoPowerShellCore { Write-Output "-----------------------------------------------------------------------------------" Write-Output "Installing PowerShell Core with Chocolatey..." choco install pwsh -y --limit-output --no-progress } function Install-PowerShellCore { Write-Output "-----------------------------------------------------------------------------------" Write-Output "Searching for the latest version of PowerShell Core" $PwshInstallerFullPath = "c:\pwsh.msi" $response = Invoke-WebRequest -UseBasicParsing -Uri "https://github.com/PowerShell/PowerShell/releases" $PwshInstallerRelativeUrl = ($response.Links.href | Where-Object { $_ -like "*win-x64.msi" -and $_ -notlike "*preview*" -and $_ -notlike "*rc*" })[0] $PwshInstallerFileName = $PwshInstallerRelativeUrl.Split("/")[-1] Write-Output "Downloading PowerShell Core ($PwshInstallerFileName)" $PwshInstallerUrl = "https://github.com" + $PwshInstallerRelativeUrl # Download PowerShell Core $WebClient = New-Object System.Net.WebClient $WebClient.DownloadFile($PwshInstallerUrl, $PwshInstallerFullPath) if (Test-Path $PwshInstallerFullPath) { Write-Output "Installing PowerShell Core ($PwshInstallerFileName)" Start-Process msiexec.exe -Wait -ArgumentList "/i $PwshInstallerFullPath /quiet /passive /qn" $Pwsh = (Get-WmiObject -Class win32_product) | Where-Object { $_.name -like "*PowerShell*-x64" } if ($Pwsh) { Write-Output "PowerShell Core (version $($Pwsh.Version) was successfully installed)" Remove-Item -Path $PwshInstallerFullPath -Force -Confirm:$false # Setting PATH environmental variable Write-Output "Setting PATH environmental variable for Azure CLI..." # Get the PATH environmental Variable $Path = (Get-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH).Path # Create New PATH environmental Variable $NewPath = $Path + ";" + "c:\Program Files\PowerShell\6\" # Set the New PATH environmental Variable Set-ItemProperty -Path 'Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment' -Name PATH -Value $NewPath $env:Path += $NewPath } else { Write-Error "PowerShell Core could not be installed" } } } function Install-ChocoVstsAgent { # Downloads the Visual Studio Online Build Agent, installs on the new machine, registers with the Visual # Studio Online account, and adds to the specified build agent pool [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$vstsAccount, [Parameter(Mandatory = $true)][string]$vstsUserPassword, [Parameter(Mandatory = $true)][string]$agentName, [Parameter(Mandatory = $false)][string]$agentNameSuffix, [Parameter(Mandatory = $true)][string]$poolName, [Parameter(Mandatory = $true)][string]$windowsLogonAccount, [Parameter(Mandatory = $false)][string]$windowsLogonPassword, [Parameter(Mandatory = $true)][ValidatePattern("[c-zC-Z]")][ValidateLength(1, 1)][string]$driveLetter, [Parameter(Mandatory = $false)][string]$workDirectory, [Parameter(Mandatory = $true)][boolean]$runAsAutoLogon ) $AgentPreparationStartTime = Get-Date Write-Output "-----------------------------------------------------------------------------------" Write-Output "Installing Azure Devops Agent with Chocolatey..." choco install azure-pipelines-agent -y --limit-output --no-progress ################################################################################################### # if the agentName is empty, use %COMPUTERNAME% as the value if ([String]::IsNullOrWhiteSpace($agentName)) { $agentName = $env:COMPUTERNAME } # if the agentNameSuffix has a value, add this to the end of the agent name if (![String]::IsNullOrWhiteSpace($agentNameSuffix)) { $agentName = $agentName + $agentNameSuffix } # # PowerShell configurations # # NOTE: Because the $ErrorActionPreference is "Stop", this script will stop on first failure. # This is necessary to ensure we capture errors inside the try-catch-finally block. $ErrorActionPreference = "Stop" # Ensure we set the working directory to that of the script. Push-Location $PSScriptRoot # Configure strict debugging. Set-PSDebug -Strict ################################################################################################### # # Functions used in this script. # function Show-LastError { [CmdletBinding()] param( ) $message = $error[0].Exception.Message if ($message) { Write-Host -Object "ERROR: $message" -ForegroundColor Red } # IMPORTANT NOTE: Throwing a terminating error (using $ErrorActionPreference = "Stop") still # returns exit code zero from the PowerShell script when using -File. The workaround is to # NOT use -File when calling this script and leverage the try-catch-finally block and return # a non-zero exit code from the catch block. exit -1 } function Test-Parameters { [CmdletBinding()] param( [string] $VstsAccount, [string] $WorkDirectory ) if ($VstsAccount -match "https*://" -or $VstsAccount -match "visualstudio.com") { Write-Error "Azure DevOps account '$VstsAccount' should not be the URL, just the account name." } if (![string]::IsNullOrWhiteSpace($WorkDirectory) -and !(Test-ValidPath -Path $WorkDirectory)) { Write-Error "Work directory '$WorkDirectory' is not a valid path." } } function Test-ValidPath { param( [string] $Path ) $isValid = Test-Path -Path $Path -IsValid -PathType Container try { [IO.Path]::GetFullPath($Path) | Out-Null } catch { $isValid = $false } return $isValid } function Test-AgentExists { [CmdletBinding()] param( [string] $InstallPath, [string] $AgentName ) $agentConfigFile = Join-Path $InstallPath '.agent' if (Test-Path $agentConfigFile) { Write-Error "Agent $AgentName is already configured in this machine" } } function New-AgentInstallPath { [CmdletBinding()] param( [string] $DriveLetter, [string] $AgentName ) [string] $agentInstallPath = $null # Construct the agent folder under the specified drive. $agentInstallDir = $DriveLetter + ":" try { # Create the directory for this agent. $agentInstallPath = Join-Path -Path $agentInstallDir -ChildPath $AgentName New-Item -ItemType Directory -Force -Path $agentInstallPath | Out-Null } catch { $agentInstallPath = $null Write-Error "Failed to create the agent directory at $installPathDir." } return $agentInstallPath } function Get-AgentInstaller { param( [string] $InstallPath ) $agentTempFolderName = "C:\agent" if (!(Test-Path $InstallPath)) { Write-Error "Agent install path doesn't exist: $InstallPath" } else { $agentExePath = Join-Path $agentTempFolderName "config.cmd" if (!(Test-Path $agentExePath)) { Write-Error "Agent installer file not found: $agentExePath" } } return $agentExePath } function Set-MachineForAutologon { param( $Config ) if ([string]::IsNullOrWhiteSpace($Config.WindowsLogonPassword)) { Write-Error "Windows logon password was not provided. Please retry by providing a valid windows logon password to enable autologon." } # Create a PS session for the user to trigger the creation of the registry entries required for autologon $computerName = "localhost" $password = ConvertTo-SecureString $Config.WindowsLogonPassword -AsPlainText -Force if ($Config.WindowsLogonAccount.Split("\").Count -eq 2) { $domain = $Config.WindowsLogonAccount.Split("\")[0] $userName = $Config.WindowsLogonAccount.Split('\')[1] } else { $domain = $Env:ComputerName $userName = $Config.WindowsLogonAccount } $credentials = New-Object System.Management.Automation.PSCredential("$domain\\$userName", $password) Enter-PSSession -ComputerName $computerName -Credential $credentials Exit-PSSession try { # Check if the HKU drive already exists Get-PSDrive -PSProvider Registry -Name HKU | Out-Null $canCheckRegistry = $true } catch [System.Management.Automation.DriveNotFoundException] { try { # Create the HKU drive New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS | Out-Null $canCheckRegistry = $true } catch { # Ignore the failure to create the drive and go ahead with trying to set the agent up Write-Warning "Moving ahead with agent setup as the script failed to create HKU drive necessary for checking if the registry entry for the user's SId exists.\n$_" } } # 120 seconds timeout $timeout = 120 # Check if the registry key required for enabling autologon is present on the machine, if not wait for 120 seconds in case the user profile is still getting created while ($timeout -ge 0 -and $canCheckRegistry) { $objUser = New-Object System.Security.Principal.NTAccount($Config.WindowsLogonAccount) $securityId = $objUser.Translate([System.Security.Principal.SecurityIdentifier]) $securityId = $securityId.Value if (Test-Path "HKU:\\$securityId") { if (!(Test-Path "HKU:\\$securityId\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run")) { New-Item -Path "HKU:\\$securityId\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run" -Force Write-Host "Created the registry entry path required to enable autologon." } break } else { $timeout -= 10 Start-Sleep(10) } } if ($timeout -lt 0) { Write-Warning "Failed to find the registry entry for the SId of the user, this is required to enable autologon. Trying to start the agent anyway." } } function Install-Agent { param( $Config ) try { # Set the current directory to the agent dedicated one previously created. Push-Location -Path $Config.AgentInstallPath if ($Config.RunAsAutoLogon) { Set-MachineForAutologon -Config $Config # Arguements to run agent with autologon enabled $agentConfigArgs = "--unattended", "--url", $Config.ServerUrl, "--auth", "PAT", "--token", $Config.VstsUserPassword, "--pool", $Config.PoolName, "--agent", $Config.AgentName, "--runAsAutoLogon", "--overwriteAutoLogon", "--windowslogonaccount", $Config.WindowsLogonAccount } else { # Arguements to run agent as a service $agentConfigArgs = "--unattended", "--url", $Config.ServerUrl, "--auth", "PAT", "--token", $Config.VstsUserPassword, "--pool", $Config.PoolName, "--agent", $Config.AgentName, "--runasservice", "--windowslogonaccount", $Config.WindowsLogonAccount } if (-not [string]::IsNullOrWhiteSpace($Config.WindowsLogonPassword)) { $agentConfigArgs += "--windowslogonpassword", $Config.WindowsLogonPassword } if (-not [string]::IsNullOrWhiteSpace($Config.WorkDirectory)) { $agentConfigArgs += "--work", $Config.WorkDirectory } & $Config.AgentExePath $agentConfigArgs if ($LASTEXITCODE -ne 0) { Write-Error "Agent configuration failed with exit code: $LASTEXITCODE" } } finally { Pop-Location } } ################################################################################################### # # Handle all errors in this script. # trap { # NOTE: This trap will handle all errors. There should be no need to use a catch below in this # script, unless you want to ignore a specific error. Show-LastError } ################################################################################################### # # Main execution block. # try { Write-Host 'Validating agent parameters' Test-Parameters -VstsAccount $vstsAccount -WorkDirectory $workDirectory Write-Host 'Preparing agent installation location' $agentInstallPath = New-AgentInstallPath -DriveLetter $driveLetter -AgentName $agentName Write-Host 'Checking for previously configured agent' Test-AgentExists -InstallPath $agentInstallPath -AgentName $agentName Write-Host "Getting agent installer path $agentInstallPath" $agentExePath = Get-AgentInstaller -InstallPath $agentInstallPath # Call the agent with the configure command and all the options (this creates the settings file) # without prompting the user or blocking the cmd execution. Write-Host 'Installing agent' $config = @{ AgentExePath = $agentExePath AgentInstallPath = $agentInstallPath AgentName = $agentName PoolName = $poolName ServerUrl = "https://$VstsAccount.visualstudio.com" VstsUserPassword = $vstsUserPassword RunAsAutoLogon = $runAsAutoLogon WindowsLogonAccount = $windowsLogonAccount WindowsLogonPassword = $windowsLogonPassword WorkDirectory = $workDirectory } Install-Agent -Config $config Write-Host 'Done' } finally { Pop-Location } } function Install-VstsAgent { # Downloads the Visual Studio Online Build Agent, installs on the new machine, registers with the Visual # Studio Online account, and adds to the specified build agent pool [CmdletBinding()] param( [Parameter(Mandatory = $true)][string]$vstsAccount, [Parameter(Mandatory = $true)][string]$vstsUserPassword, [Parameter(Mandatory = $true)][string]$agentName, [Parameter(Mandatory = $false)][string]$agentNameSuffix, [Parameter(Mandatory = $true)][string]$poolName, [Parameter(Mandatory = $true)][string]$windowsLogonAccount, [Parameter(Mandatory = $false)][string]$windowsLogonPassword, [Parameter(Mandatory = $true)][ValidatePattern("[c-zC-Z]")][ValidateLength(1, 1)][string]$driveLetter, [Parameter(Mandatory = $false)][string]$workDirectory, [Parameter(Mandatory = $true)][boolean]$runAsAutoLogon ) Write-Output "-----------------------------------------------------------------------------------" Write-Output "Installing VSTS Agent..." ################################################################################################### # if the agentName is empty, use %COMPUTERNAME% as the value if ([String]::IsNullOrWhiteSpace($agentName)) { $agentName = $env:COMPUTERNAME } # if the agentNameSuffix has a value, add this to the end of the agent name if (![String]::IsNullOrWhiteSpace($agentNameSuffix)) { $agentName = $agentName + $agentNameSuffix } # # PowerShell configurations # # NOTE: Because the $ErrorActionPreference is "Stop", this script will stop on first failure. # This is necessary to ensure we capture errors inside the try-catch-finally block. $ErrorActionPreference = "Stop" # Ensure we set the working directory to that of the script. Push-Location $PSScriptRoot # Configure strict debugging. Set-PSDebug -Strict ################################################################################################### # # Functions used in this script. # function Show-LastError { [CmdletBinding()] param( ) $message = $error[0].Exception.Message if ($message) { Write-Host -Object "ERROR: $message" -ForegroundColor Red } # IMPORTANT NOTE: Throwing a terminating error (using $ErrorActionPreference = "Stop") still # returns exit code zero from the PowerShell script when using -File. The workaround is to # NOT use -File when calling this script and leverage the try-catch-finally block and return # a non-zero exit code from the catch block. exit -1 } function Test-Parameters { [CmdletBinding()] param( [string] $VstsAccount, [string] $WorkDirectory ) if ($VstsAccount -match "https*://" -or $VstsAccount -match "visualstudio.com") { Write-Error "VSTS account '$VstsAccount' should not be the URL, just the account name." } if (![string]::IsNullOrWhiteSpace($WorkDirectory) -and !(Test-ValidPath -Path $WorkDirectory)) { Write-Error "Work directory '$WorkDirectory' is not a valid path." } } function Test-ValidPath { param( [string] $Path ) $isValid = Test-Path -Path $Path -IsValid -PathType Container try { [IO.Path]::GetFullPath($Path) | Out-Null } catch { $isValid = $false } return $isValid } function Test-AgentExists { [CmdletBinding()] param( [string] $InstallPath, [string] $AgentName ) $agentConfigFile = Join-Path $InstallPath '.agent' if (Test-Path $agentConfigFile) { Write-Error "Agent $AgentName is already configured in this machine" } } function Get-AgentPackage { [CmdletBinding()] param( [string] $VstsAccount, [string] $VstsUserPassword ) # Create a temporary directory where to download from VSTS the agent package (agent.zip). $agentTempFolderName = Join-Path $env:temp ([System.IO.Path]::GetRandomFileName()) New-Item -ItemType Directory -Force -Path $agentTempFolderName | Out-Null $agentPackagePath = "$agentTempFolderName\agent.zip" $serverUrl = "https://$VstsAccount.visualstudio.com" $vstsAgentUrl = "$serverUrl/_apis/distributedtask/packages/agent/win-x64?`$top=1&api-version=3.0" $vstsUser = "AzureDevTestLabs" $maxRetries = 3 $retries = 0 do { try { $basicAuth = ("{0}:{1}" -f $vstsUser, $vstsUserPassword) $basicAuth = [System.Text.Encoding]::UTF8.GetBytes($basicAuth) $basicAuth = [System.Convert]::ToBase64String($basicAuth) $headers = @{ Authorization = ("Basic {0}" -f $basicAuth) } $agentList = Invoke-RestMethod -Uri $vstsAgentUrl -Headers $headers -Method Get -ContentType application/json $agent = $agentList.value if ($agent -is [Array]) { $agent = $agentList.value[0] } Invoke-WebRequest -Uri $agent.downloadUrl -Headers $headers -Method Get -OutFile "$agentPackagePath" -UseBasicParsing | Out-Null break } catch { $exceptionText = ($_ | Out-String).Trim() if (++$retries -gt $maxRetries) { Write-Error "Failed to download agent due to $exceptionText" } Start-Sleep -Seconds 1 } } while ($retries -le $maxRetries) return $agentPackagePath } function New-AgentInstallPath { [CmdletBinding()] param( [string] $DriveLetter, [string] $AgentName ) [string] $agentInstallPath = $null # Construct the agent folder under the specified drive. $agentInstallDir = $DriveLetter + ":" try { # Create the directory for this agent. $agentInstallPath = Join-Path -Path $agentInstallDir -ChildPath $AgentName New-Item -ItemType Directory -Force -Path $agentInstallPath | Out-Null } catch { $agentInstallPath = $null Write-Error "Failed to create the agent directory at $installPathDir." } return $agentInstallPath } function Get-AgentInstaller { param( [string] $InstallPath ) $agentExePath = [System.IO.Path]::Combine($InstallPath, 'config.cmd') if (![System.IO.File]::Exists($agentExePath)) { Write-Error "Agent installer file not found: $agentExePath" } return $agentExePath } function Set-MachineForAutologon { param( $Config ) if ([string]::IsNullOrWhiteSpace($Config.WindowsLogonPassword)) { Write-Error "Windows logon password was not provided. Please retry by providing a valid windows logon password to enable autologon." } # Create a PS session for the user to trigger the creation of the registry entries required for autologon $computerName = "localhost" $password = ConvertTo-SecureString $Config.WindowsLogonPassword -AsPlainText -Force if ($Config.WindowsLogonAccount.Split("\").Count -eq 2) { $domain = $Config.WindowsLogonAccount.Split("\")[0] $userName = $Config.WindowsLogonAccount.Split('\')[1] } else { $domain = $Env:ComputerName $userName = $Config.WindowsLogonAccount } $credentials = New-Object System.Management.Automation.PSCredential("$domain\\$userName", $password) Enter-PSSession -ComputerName $computerName -Credential $credentials Exit-PSSession try { # Check if the HKU drive already exists Get-PSDrive -PSProvider Registry -Name HKU | Out-Null $canCheckRegistry = $true } catch [System.Management.Automation.DriveNotFoundException] { try { # Create the HKU drive New-PSDrive -PSProvider Registry -Name HKU -Root HKEY_USERS | Out-Null $canCheckRegistry = $true } catch { # Ignore the failure to create the drive and go ahead with trying to set the agent up Write-Warning "Moving ahead with agent setup as the script failed to create HKU drive necessary for checking if the registry entry for the user's SId exists.\n$_" } } # 120 seconds timeout $timeout = 120 # Check if the registry key required for enabling autologon is present on the machine, if not wait for 120 seconds in case the user profile is still getting created while ($timeout -ge 0 -and $canCheckRegistry) { $objUser = New-Object System.Security.Principal.NTAccount($Config.WindowsLogonAccount) $securityId = $objUser.Translate([System.Security.Principal.SecurityIdentifier]) $securityId = $securityId.Value if (Test-Path "HKU:\\$securityId") { if (!(Test-Path "HKU:\\$securityId\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run")) { New-Item -Path "HKU:\\$securityId\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run" -Force Write-Host "Created the registry entry path required to enable autologon." } break } else { $timeout -= 10 Start-Sleep(10) } } if ($timeout -lt 0) { Write-Warning "Failed to find the registry entry for the SId of the user, this is required to enable autologon. Trying to start the agent anyway." } } function Install-Agent { param( $Config ) try { # Set the current directory to the agent dedicated one previously created. Push-Location -Path $Config.AgentInstallPath if ($Config.RunAsAutoLogon) { Set-MachineForAutologon -Config $Config # Arguements to run agent with autologon enabled $agentConfigArgs = "--unattended", "--url", $Config.ServerUrl, "--auth", "PAT", "--token", $Config.VstsUserPassword, "--pool", $Config.PoolName, "--agent", $Config.AgentName, "--runAsAutoLogon", "--overwriteAutoLogon", "--windowslogonaccount", $Config.WindowsLogonAccount } else { # Arguements to run agent as a service $agentConfigArgs = "--unattended", "--url", $Config.ServerUrl, "--auth", "PAT", "--token", $Config.VstsUserPassword, "--pool", $Config.PoolName, "--agent", $Config.AgentName, "--runasservice", "--windowslogonaccount", $Config.WindowsLogonAccount } if (-not [string]::IsNullOrWhiteSpace($Config.WindowsLogonPassword)) { $agentConfigArgs += "--windowslogonpassword", $Config.WindowsLogonPassword } if (-not [string]::IsNullOrWhiteSpace($Config.WorkDirectory)) { $agentConfigArgs += "--work", $Config.WorkDirectory } & $Config.AgentExePath $agentConfigArgs if ($LASTEXITCODE -ne 0) { Write-Error "Agent configuration failed with exit code: $LASTEXITCODE" } } finally { Pop-Location } } ################################################################################################### # # Handle all errors in this script. # trap { # NOTE: This trap will handle all errors. There should be no need to use a catch below in this # script, unless you want to ignore a specific error. Show-LastError } ################################################################################################### # # Main execution block. # try { Write-Host 'Validating agent parameters' Test-Parameters -VstsAccount $vstsAccount -WorkDirectory $workDirectory Write-Host 'Preparing agent installation location' $agentInstallPath = New-AgentInstallPath -DriveLetter $driveLetter -AgentName $agentName Write-Host 'Checking for previously configured agent' Test-AgentExists -InstallPath $agentInstallPath -AgentName $agentName Write-Host 'Downloading agent package' $agentPackagePath = Get-AgentPackage -VstsAccount $vstsAccount -VstsUserPassword $vstsUserPassword Write-Host 'Extracting agent package contents' Expand-Archive -LiteralPath $agentPackagePath -DestinationPath $agentInstallPath Write-Host 'Getting agent installer path' $agentExePath = Get-AgentInstaller -InstallPath $agentInstallPath # Call the agent with the configure command and all the options (this creates the settings file) # without prompting the user or blocking the cmd execution. Write-Host 'Installing agent' $config = @{ AgentExePath = $agentExePath AgentInstallPath = $agentInstallPath AgentName = $agentName PoolName = $poolName ServerUrl = "https://$VstsAccount.visualstudio.com" VstsUserPassword = $vstsUserPassword RunAsAutoLogon = $runAsAutoLogon WindowsLogonAccount = $windowsLogonAccount WindowsLogonPassword = $windowsLogonPassword WorkDirectory = $workDirectory } Install-Agent -Config $config Write-Host 'Done' } finally { Pop-Location } } function Get-SystemData { # Get available Volume size $LogicalDisk = Get-WmiObject -Class win32_logicaldisk -Property * $FreeSpace = $LogicalDisk.FreeSpace / 1GB $Size = $LogicalDisk.Size / 1GB Write-Output "$($FreeSpace.ToString("#.##")) of $($Size.ToString("#.##")) GB disk space available" } function Watch-VstsAgentService { Write-Output "This container will keep running as long as the Azure DevOps agent (vstsagent) service in it is not interrupted for longer than 3 minutes." $TryCount = 0 while ($true) { if ((Get-Service "vstsagent*").Status -eq "Running") { Start-Sleep -Seconds 60 | Out-Null # Test-Connection -ComputerName localhost -Quiet -Delay 60 | Out-Null } else { $TryCount++ } if ($TryCount -gt 3) { break } } } #endregion #region Main # Record start time $StartDate = Get-Date Write-Output "-----------------------------------------------------------------------------------" Write-Host "Configuration started at $StartDate" # Set SSL version preference [Net.ServicePointManager]::SecurityProtocol = "Tls12, Tls11, Tls" # Original: Ssl3, Tls # Install Chocolatey $ChocoInstallStart = Get-Date if ($UseChocolatey -eq $true) { Install-Choco $ChocoInstallEnd = Get-Date $ChocoInstallDuration = New-TimeSpan -Start $ChocoInstallStart -End $ChocoInstallEnd Write-Host "Chocolatey installation took $($ChocoInstallDuration.Hours.ToString("00")):$($ChocoInstallDuration.Minutes.ToString("00")):$($ChocoInstallDuration.Seconds.ToString("00")) (HH:mm:ss)" } # Install Terraform $TerraformInstallStart = Get-Date if ($UseChocolatey -eq $true) { Install-ChocoTerraform } else { Install-Terraform } $TerraformInstallEnd = Get-Date $TerraformInstallDuration = New-TimeSpan -Start $TerraformInstallStart -End $TerraformInstallEnd Write-Host "Terraform installation took $($TerraformInstallDuration.Hours.ToString("00")):$($TerraformInstallDuration.Minutes.ToString("00")):$($TerraformInstallDuration.Seconds.ToString("00")) (HH:mm:ss)" # Install Json2HCL $Json2HclInstallStart = Get-Date Install-Json2Hcl $Json2HclInstallEnd = Get-Date $Json2HclInstallDuration = New-TimeSpan -Start $Json2HclInstallStart -End $Json2HclInstallEnd Write-Host "Json2HCL installation took $($Json2HclInstallDuration.Hours.ToString("00")):$($Json2HclInstallDuration.Minutes.ToString("00")):$($Json2HclInstallDuration.Seconds.ToString("00")) (HH:mm:ss)" # Install Nuget $NugetInstallStart = Get-Date Install-Nuget $NugetInstallEnd = Get-Date $NugetInstallDuration = New-TimeSpan -Start $NugetInstallStart -End $NugetInstallEnd Write-Host "Nuget installation took $($NugetInstallDuration.Hours.ToString("00")):$($NugetInstallDuration.Minutes.ToString("00")):$($NugetInstallDuration.Seconds.ToString("00")) (HH:mm:ss)" # Install Powershell Modules $PoShModuleInstallStart = Get-Date Install-PowerShellModules -RequiredModules $RequiredPowerShellModules $PoShModuleInstallEnd = Get-Date $PoShModuleInstallDuration = New-TimeSpan -Start $PoShModuleInstallStart -End $PoShModuleInstallEnd Write-Host "PowerShell module installation took $($PoShModuleInstallDuration.Hours.ToString("00")):$($PoShModuleInstallDuration.Minutes.ToString("00")):$($PoShModuleInstallDuration.Seconds.ToString("00")) (HH:mm:ss)" # Install Azure CLI $AzureCliInstallStart = Get-Date if ($InstallAzureCli -eq $true) { if ($UseChocolatey -eq $true) { Install-ChocoAzureCli } else { Install-AzureCli } $AzureCliInstallEnd = Get-Date $AzureCliInstallDuration = New-TimeSpan -Start $AzureCliInstallStart -End $AzureCliInstallEnd Write-Output "Azure CLI installation took $($AzureCliInstallDuration.Hours.ToString("00")):$($AzureCliInstallDuration.Minutes.ToString("00")):$($AzureCliInstallDuration.Seconds.ToString("00")) (HH:mm:ss)" } # Install PowerShell Core $PoShCoreInstallStart = Get-Date if ($InstallPowerShellCore -eq $true) { if ($UseChocolatey -eq $true) { Install-ChocoPowerShellCore } else { Install-PowerShellCore } $PoShCoreInstallEnd = Get-Date $PoShCoreInstallDuration = New-TimeSpan -Start $PoShCoreInstallStart -End $PoShCoreInstallEnd Write-Output "PowerShell Core installation took $($PoShCoreInstallDuration.Hours.ToString("00")):$($PoShCoreInstallDuration.Minutes.ToString("00")):$($PoShCoreInstallDuration.Seconds.ToString("00")) (HH:mm:ss)" } # Install VSTS Agent $AgentInstallStart = Get-Date $AgentName = "$AgentNamePrefix-$(Get-Date -Format yyyyMMdd-HHmmss)" if ($UseChocolatey -eq $true) { Install-ChocoVstsAgent -vstsAccount $VSTSAccountName -vstsUserPassword $PATToken -agentName $AgentName -poolName $PoolName -windowsLogonAccount "NT AUTHORITY\NetworkService" -driveLetter "C" -runAsAutoLogon:$false } else { Install-VstsAgent -vstsAccount $VSTSAccountName -vstsUserPassword $PATToken -agentName $AgentName -poolName $PoolName -windowsLogonAccount "NT AUTHORITY\NetworkService" -driveLetter "C" -runAsAutoLogon:$false } $AgentInstallEnd = Get-Date $AgentInstallDuration = New-TimeSpan -Start $AgentInstallStart -End $AgentInstallEnd Write-Host "Agent installation took $($AgentInstallDuration.Hours.ToString("00")):$($AgentInstallDuration.Minutes.ToString("00")):$($AgentInstallDuration.Seconds.ToString("00")) (HH:mm:ss)" # Get available Volume size, RAM Write-Output "-----------------------------------------------------------------------------------" Get-SystemData # Calculate duration $OverallDuration = New-TimeSpan -Start $StartDate -End (Get-Date) Write-Output "-----------------------------------------------------------------------------------" Write-Host "It took $($OverallDuration.Hours.ToString("00")):$($OverallDuration.Minutes.ToString("00")):$($OverallDuration.Seconds.ToString("00")) (HH:mm:ss) to install the required components." Write-Host "Installation finished at $(Get-Date)" Write-Host "Container successfully configured." # Do NOT change this text, as this is the success criteria for the wrapper script. # Keep the container running by checking if the VSTS service is up Write-Output "-----------------------------------------------------------------------------------" Watch-VstsAgentService #endregion # SIG # Begin signature block # MIINEAYJKoZIhvcNAQcCoIINATCCDP0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUu1cpQ9vh4J+hzFU/c3YW9C4J # flugggpSMIIFGjCCBAKgAwIBAgIQAsF1KHTVwoQxhSrYoGRpyjANBgkqhkiG9w0B # AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz # c3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTE3MDUwOTAwMDAwMFoXDTIwMDUx # MzEyMDAwMFowVzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMQ8wDQYD # VQQHEwZWaWVubmExETAPBgNVBAoTCGRiYXRvb2xzMREwDwYDVQQDEwhkYmF0b29s # czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAI8ng7JxnekL0AO4qQgt # Kr6p3q3SNOPh+SUZH+SyY8EA2I3wR7BMoT7rnZNolTwGjUXn7bRC6vISWg16N202 # 1RBWdTGW2rVPBVLF4HA46jle4hcpEVquXdj3yGYa99ko1w2FOWzLjKvtLqj4tzOh # K7wa/Gbmv0Si/FU6oOmctzYMI0QXtEG7lR1HsJT5kywwmgcjyuiN28iBIhT6man0 # Ib6xKDv40PblKq5c9AFVldXUGVeBJbLhcEAA1nSPSLGdc7j4J2SulGISYY7ocuX3 # tkv01te72Mv2KkqqpfkLEAQjXgtM0hlgwuc8/A4if+I0YtboCMkVQuwBpbR9/6ys # Z+sCAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrEuXsqCqOl6nEDwGD5LfZldQ5Y # MB0GA1UdDgQWBBRcxSkFqeA3vvHU0aq2mVpFRSOdmjAOBgNVHQ8BAf8EBAMCB4Aw # EwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAwbjA1oDOgMYYvaHR0cDovL2Ny # bDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwNaAzoDGGL2h0 # dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3MtZzEuY3JsMEwG # A1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3 # LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcBAQR4MHYwJAYI # KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggrBgEFBQcwAoZC # aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJ # RENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQAD # ggEBANuBGTbzCRhgG0Th09J0m/qDqohWMx6ZOFKhMoKl8f/l6IwyDrkG48JBkWOA # QYXNAzvp3Ro7aGCNJKRAOcIjNKYef/PFRfFQvMe07nQIj78G8x0q44ZpOVCp9uVj # sLmIvsmF1dcYhOWs9BOG/Zp9augJUtlYpo4JW+iuZHCqjhKzIc74rEEiZd0hSm8M # asshvBUSB9e8do/7RhaKezvlciDaFBQvg5s0fICsEhULBRhoyVOiUKUcemprPiTD # xh3buBLuN0bBayjWmOMlkG1Z6i8DUvWlPGz9jiBT3ONBqxXfghXLL6n8PhfppBhn # daPQO8+SqF5rqrlyBPmRRaTz2GQwggUwMIIEGKADAgECAhAECRgbX9W7ZnVTQ7Vv # lVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp # Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0Rp # Z2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBaFw0yODEw # MjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNI # QTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUA # A4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/lqJ3bMtdx # 6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fTeyOU5JEj # lpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqHCN8M9eJN # YBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+bMt+dDk2 # DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLoLFH3c7y9 # hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIByTASBgNV # HRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEF # BQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp # Z2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDig # NoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 # QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwAAgQwKjAo # BggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAKBghghkgB # hv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0jBBgwFoAU # Reuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7sDVoks/Mi # 0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGSdQ9RtG6l # jlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6r7VRwo0k # riTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo+MUSaJ/P # QMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qzsIzV6Q3d # 9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHqaGxEMrJm # oecYpJpkUe8xggIoMIICJAIBATCBhjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM # RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQD # EyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBAhACwXUo # dNXChDGFKtigZGnKMAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3AgEMMQowCKACgACh # AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM # BgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBR53uHcZr/UgRVwXu6uR9unXJ4R # CzANBgkqhkiG9w0BAQEFAASCAQCNRzZa1SvIfzZvGCNW5Q5mwByTNfEwWIQsJ4Wc # 2uOc3opBj4QNotkaVi+isoR6Kco+SeRJ6XNmrf3G/GDkfwhSCaFw8Gu7TnTHAR29 # LXeHbRSl1ibDgygrlX+Ag6E7VMS2E5STqDleLzsx2XUK0tenV4p7MKs9hHja1kev # HUHcAMbKR3OOsLPi1laI1vCwWroECRk3SLIzh5CUg+9zPKzDsQ2IA3qfW70ES6p4 # 3bg++PAN4SC/2D5LfrUUlpieZlMEfVuOcUCRPuPMiIon/+zcWB7JKjmD4sh5sXDB # AkGUPvm0VR2GzeD+PkwZYM97lIt1ID8V6+e/bTWTwiT1INdf # SIG # End signature block |