Artifacts/windows-vsts-build-agent/vsts-agent-install.ps1 (383 lines of code) (raw):
# 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(
[string] $vstsAccount,
[string] $vstsUserPassword,
[string] $agentName,
[string] $agentNameSuffix,
[string] $poolName,
[string] $windowsLogonAccount,
[string] $windowsLogonPassword,
[ValidatePattern("[c-zC-Z]")]
[ValidateLength(1, 1)]
[string] $driveLetter,
[string] $workDirectory,
[boolean] $runAsAutoLogon,
[boolean] $replaceAgent = $false
)
###################################################################################################
#
# 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"
# Suppress progress bar output.
$ProgressPreference = 'SilentlyContinue'
# Ensure we force use of TLS 1.2 for all downloads.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Configure strict debugging.
Set-PSDebug -Strict
# 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
}
###################################################################################################
#
# 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.
$message = $Error[0].Exception.Message
if ($message)
{
Write-Host -Object "`nERROR: $message" -ForegroundColor Red
}
Write-Host "`nThe artifact failed to apply.`n"
# 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
}
###################################################################################################
#
# Functions used in this script.
#
function Test-Parameters
{
[CmdletBinding()]
param(
[string] $VstsAccount,
[string] $WorkDirectory
)
if ($VstsAccount -match "https*://" -or $VstsAccount -match "visualstudio.com")
{
throw "VSTS account '$VstsAccount' should not be the URL, just the account name."
}
if (![string]::IsNullOrWhiteSpace($WorkDirectory) -and !(Test-ValidPath -Path $WorkDirectory))
{
throw "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)
{
throw "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/win7-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" | Out-Null
break
}
catch
{
$exceptionText = ($_ | Out-String).Trim()
if (++$retries -gt $maxRetries)
{
throw "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
throw "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))
{
throw "Agent installer file not found: $agentExePath"
}
return $agentExePath
}
function Extract-AgentPackage
{
[CmdletBinding()]
param(
[string] $PackagePath,
[string] $Destination
)
# Identify a new folder in the temp path to extract
$TempFolder = $Env:TEMP
Do
{
$TempFileName = "$(Get-Random).tmp"
$TempAgentPath = Join-Path $TempFolder $TempFileName
}
Until (-not (Test-Path -LiteralPath $TempAgentPath))
# Extract the agent zip
Write-Host "Extracting Agent: $TempAgentPath"
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory("$PackagePath", "$TempAgentPath")
# Copy the extract content to the agent folder
Write-Host "Copying Agent to Destination: $Destination"
Copy-Item -Recurse -Force -Path "$TempAgentPath\*" -Destination $Destination
}
function Prep-MachineForAutologon
{
param(
$Config
)
if ([string]::IsNullOrWhiteSpace($Config.WindowsLogonPassword))
{
throw "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)
{
try
{
$objUser = New-Object System.Security.Principal.NTAccount($Config.WindowsLogonAccount)
$securityId = $objUser.Translate([System.Security.Principal.SecurityIdentifier])
$securityId = $securityId.Value
}
catch
{
throw "Login '$($Config.WindowsLogonAccount)' could not be mapped to a SID. Please verify account."
}
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)
{
Prep-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
}
if ($Config.ReplaceAgent)
{
$agentConfigArgs += "--replace"
}
& $Config.AgentExePath $agentConfigArgs
if ($LASTEXITCODE -ne 0)
{
throw "Agent configuration failed with exit code: $LASTEXITCODE"
}
}
finally
{
Pop-Location
}
}
###################################################################################################
#
# Main execution block.
#
try
{
# Ensure we set the working directory to that of the script.
Push-Location $PSScriptRoot
Write-Host 'Validating parameters'
Test-Parameters -VstsAccount $vstsAccount -WorkDirectory $workDirectory
Write-Host 'Preparing agent installation location'
$agentInstallPath = New-AgentInstallPath -DriveLetter $driveLetter -AgentName $agentName
if (-not $replaceAgent)
{
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'
Extract-AgentPackage -PackagePath $agentPackagePath -Destination $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
ReplaceAgent = $replaceAgent
RunAsAutoLogon = $runAsAutoLogon
ServerUrl = "https://$VstsAccount.visualstudio.com"
VstsUserPassword = $vstsUserPassword
WindowsLogonAccount = $windowsLogonAccount
WindowsLogonPassword = $windowsLogonPassword
WorkDirectory = $workDirectory
}
Install-Agent -Config $config
Write-Host "`nThe artifact was applied successfully.`n"
}
finally
{
Pop-Location
}