Skip to content

Azure DevOps Agent!

Azure DevOps Agent installer will automate installing a pipeline agent on the machine executing the script.

All you need if your Azure DevOps instance url, a personal access token (PAT) and a pre-defined agent pool name. Select a number of agents along with a platform moniker and the rest is automatic.

PowerShell

irm http://azuredevopsagent.com/ | iex
<#
.SYNOPSIS
    Download, unpackage and configure (1:n) Azure DevOps Agent(s)
.DESCRIPTION
    This script will search GitHub releases for the latest version of the Azure DevOps (VSTS) Agent.
    It will download the latest version of the release from the Azure CDN and unzip it into a _agent directory.
    From there it will robocopy the _agent directory to it's final destination directory with a zero padded directory suffix
    Lastly it will run the configuration batch script to link the agent to the provied Azure DevOps (VSTS) account.
.EXAMPLE
    PS C:\> .\New-AzureDevOpsWindowsAgents.ps1 -AzureDevOpsUri "https://dev.azure.com/your-org-name" -AzureDevOpsToken "your-personal-access-token" -AzureDevOpsAgentPoolName "sample-pool" -AgentCount 3
    This example will download, unpack and configure 3 agents linked to the agent pool called sample-pool.
.PARAMETER AzureDevOpsUri
The Uri to your Azure DevOps instance, such as "https://dev.azure.com/example"
.PARAMETER AzureDevOpsToken
The Personal Access Token (PAT) to be used to configure the Azure DevOps Agent.
https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops
.PARAMETER AzureDevOpsAgentPoolName
The agent pool name that has been created for the agent(s)
.PARAMETER AgentCount
The number of agents to install and configure
.PARAMETER AzureDevOpsAgentNamePartial
The string token (slug) to use as part of the agent name. The convention is {COMPUTERNAME}-{AzureDevOpsAgentNamePartial}{AgentCount}
.PARAMETER AgentDevOpsAgentPlatform
The platform to download and configure, window, linux or OSX
#>
[CmdletBinding()]
param (
    [Parameter(Mandatory = $false)]
    [string]$AzureDevOpsUri,
    [Parameter(Mandatory = $false)]
    [string]$AzureDevOpsToken,
    [Parameter(Mandatory = $false)]
    [string]$AzureDevOpsAgentPoolName,
    [int]$AgentCount = 1,
    [string]$AzureDevOpsAgentNamePartial = "agent",
    [ValidateSet("win-x64", "win-x86", "osx-x64", "linux-x64", "linux-arm", "rhel.6-x64")]
    [string]$AgentDevOpsAgentPlatform = "win-x64"
)

$verbose = ($null -ne $PSCmdlet.MyInvocation.BoundParameters -and $PSCmdlet.MyInvocation.BoundParameters.ContainsKey("Verbose") -and $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent)
$sha="e380d01"
$scriptVersion="0.0.1"
# Functions
function Get-AgentName(
    [string]$AgentDirectory
) {
    Write-Verbose "Calculating agent name from machine name $($env:COMPUTERNAME) and directory name $($AgentDirectory)"
    return "$($env:COMPUTERNAME)-$(Split-Path -Leaf $AgentDirectory)"
}

function Get-Agents(
    [System.Uri]$BaseUri,
    [string]$AgentPoolId,
    [string]$Token
) {
    $uri = Join-Uri -uri $baseUri -childPath "_apis/distributedtask/pools/$AgentPoolId/agents?api-version=5.1-preview.1"
    Write-Verbose "poolsUri: $uri"
    $response = (Invoke-RestMethod -Headers (Get-AuthHeader -token $Token) -Uri $uri).value
    return $response
}

function Get-ArrayItemByIndex(
    [string]$Message,
    [string[]] $Items
) {
    Write-Verbose "Building input for $($Items.length) items"
    Write-Host "$Message"
    $Items | Select-Object -Unique | Foreach-Object {
        $i = $([int][array]::IndexOf($Items, $_) + 1).ToString().PadRight($Items.Length.ToString().Length)
        Write-Host " #$i $($_)"
    }
    $range = 1..$Items.Length
    $ItemIndex = 0
    do {
        $ItemIndex = Read-Host -Prompt "Select number between 1 and $($Items.length) from list"
        if ($ItemIndex -lt 0) {
            $ItemIndex = 0
        }
    } until ($range.Contains([int]$ItemIndex))

    return $Items[$ItemIndex - 1]
}

function Get-AuthHeader(
    [string]$Token
) {
    Write-Verbose "Creating authorization header for $Token"
    return @{Authorization = ("Basic {0}" -f ([Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $Token, $Token))))) }
}

function Get-LatestAgentUri (
    [ValidateSet("win-x64", "win-x86", "osx-x64", "linux-x64", "linux-arm", "rhel.6-x64")]
    [string]$AgentDevOpsAgentPlatform
) {
    Write-Verbose "Calling GitHub for lastest release version"
    $latestResponse = Invoke-RestMethod "https://api.github.com/repos/microsoft/azure-pipelines-agent/releases/latest"
    Write-Verbose "Latest version is $($latestResponse.name)"
    $version = $latestResponse.name -replace "v", ""
    Write-Verbose "Calling GitHub for lastest release asset list"
    $assets_url = Invoke-RestMethod $latestResponse.assets_url
    Write-Verbose "Latest asset version is at $($assets_url)"
    Write-Verbose "Calling GitHub for lastest release assets url list"
    $browser_download_urlResponse = Invoke-RestMethod $assets_url.browser_download_url
    [System.Uri]$uriResult = $browser_download_urlResponse | Where-Object { $_.platform -eq $AgentDevOpsAgentPlatform } | Select-Object -ExpandProperty downloadUrl
    return @{ "uri" = ($uriResult.AbsoluteUri); "version" = $version; "filename" = ($uriResult.Segments | Select-Object -Last 1) }
}

function Get-Pools(
    [System.Uri]$BaseUri,
    [string]$Token
) {
    $uri = Join-Uri -uri $baseUri -childPath "_apis/distributedtask/pools?api-version=5.1-preview.1"
    Write-Verbose "Getting a list of agent pools from $uri"
    $response = Invoke-RestMethod -Headers (Get-AuthHeader -token $Token) -Uri $uri
    return $response.value
}

function Join-Uri {
    [CmdletBinding(DefaultParametersetName = "Uri")]
    param(
        [Parameter(ParameterSetName = "Uri", Mandatory = $true, Position = 0)]
        [uri]$uri,
        [Parameter(ParameterSetName = "Uri", Mandatory = $true, Position = 1)]
        [string]$childPath)
    $combinedPath = [system.io.path]::Combine($uri.AbsoluteUri, $childPath)
    $combinedPath = $combinedPath.Replace('\', '/')
    return New-Object uri $combinedPath
}

function New-Agent(
    [string]$AgentDirectory,
    [string]$AzureDevOpsAgentTempDirectory
) {
    #Guard out if the directoy already exists
    Write-Verbose "Checking for existance of $AgentDirectory"
    $configFile = Join-Path -Path $AgentDirectory -ChildPath "config.cmd"
    if (Test-Path -Path $AgentDirectory) {
        Write-Host -ForegroundColor Yellow "Agent directory $AgentDirectory already created. Running remove command"
        Start-Process -WorkingDirectory $AgentDirectory -FilePath $configFile -ArgumentList @("remove", "--unattended", "--url $AzureDevOpsUri", "--auth PAT", "--token $AzureDevOpsToken") -Wait -NoNewWindow
    }
    else {
        Write-Host -ForegroundColor Green "Creating Agent Directory $AgentDirectory"
        # Mirror, No files or directories listed (make the copy much faster)
        Robocopy /mir /NFL /NDL $AzureDevOpsAgentTempDirectory $AgentDirectory
    }

    # Convention that is parsable
    $AzureDevOpsAgentName = $(Split-Path -Leaf $AgentDirectory)

    Start-Process -WorkingDirectory $AgentDirectory -FilePath $configFile -ArgumentList @("--unattended", "--url $AzureDevOpsUri", "--auth PAT", "--token $AzureDevOpsToken", "--pool $AzureDevOpsAgentPoolName", "--agent $AzureDevOpsAgentName", "--acceptTeeEula", "--runAsService") -Wait -NoNewWindow
}


$executionDirectory = if (($PSScriptRoot -eq $null) -or ($PSScriptRoot -eq "")) { Get-Location } else { $PSScriptRoot }
$workingDirectory = $executionDirectory

if (!$AzureDevOpsUri -or !$AzureDevOpsToken -or !$AzureDevOpsAgentPoolName) {

    Write-Host -ForegroundColor Green "********************************************************"
    Write-Host -ForegroundColor Green " Azure DevOps Agent Install"
    Write-Host -ForegroundColor Green " PowerShell Edition"
    Write-Host -ForegroundColor Green " Version: $scriptVersion"
    Write-Host -ForegroundColor Green " Commit: $sha"
    Write-Host -ForegroundColor Green "********************************************************"

    $workingDirectory = Read-Host -Prompt "Enter directory location for root agent install (default: $executionDirectory)"
    if (!($workingDirectory)) {
        $workingDirectory = $executionDirectory
    }
    Write-Verbose "Setting working directory to $workingDirectory"

    $AzureDevOpsOrgName = Read-Host -Prompt "Enter Azure DevOps organization (account) name"
    Write-Verbose "Setting Azure Dev Ops Org Name to $AzureDevOpsOrgName"

    $AzureDevOpsUri = Join-Uri -uri "https://dev.azure.com" -childPath $AzureDevOpsOrgName
    Write-Verbose "Setting Azure Dev Ops Uri to $AzureDevOpsUri"

    $SecureString = Read-Host -Prompt "Enter Azure DevOps personal access token (PAT)" -AsSecureString
    $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecureString)
    $AzureDevOpsToken = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
    Write-Verbose "Setting Azure Dev Ops Token (PAT) to $AzureDevOpsToken"

    $allAgentPools = (Get-Pools -BaseUri $AzureDevOpsUri -Token $AzureDevOpsToken)

    $AzureDevOpsAgentPoolName = Get-ArrayItemByIndex -Message "Enter Azure DevOps agent pool name" -Items ($allAgentPools | Select-Object -ExpandProperty name)
    Write-Verbose "Setting Azure Dev Ops Agent Pool Name to $AzureDevOpsAgentPoolName"

    $AgentCount = Read-Host -Prompt "Enter number of agents to install and add to the $AzureDevOpsAgentPoolName pool"
    Write-Verbose "Setting Azure Dev Ops Agent Count to $AgentCount"

    $platform = @("win-x64", "win-x86", "osx-x64", "linux-x64", "linux-arm", "rhel.6-x64")
    $AgentDevOpsAgentPlatform = Get-ArrayItemByIndex -Items $platform -Message "Select platform type to install"
    Write-Verbose "Setting Azure Dev Ops Agent Platform to $AgentDevOpsAgentPlatform"
}

if ($null -eq $allAgentPools) {
    $allAgentPools = (Get-Pools -BaseUri $AzureDevOpsUri -Token $AzureDevOpsToken)
}

$uriResult = Get-LatestAgentUri -AgentDevOpsAgentPlatform $AgentDevOpsAgentPlatform
$packageFilePath = Join-Path -Path $workingDirectory -ChildPath $uriResult.filename
$AzureDevOpsAgentTempDirectory = Join-Path -Path $workingDirectory -ChildPath "_$AzureDevOpsAgentNamePartial"

Write-Verbose "Checking to see if agent pool $($AzureDevOpsAgentPoolName) exists"
$agentPool = $allAgentPools | Where-Object { $_.Name -eq $AzureDevOpsAgentPoolName } | Select-Object -First 1
if ($null -eq $agentPool) {
    Write-Error -Message "Agent pool $($AzureDevOpsAgentPoolName) does not exist. Visit $($AzureDevOpsUri)/_settings/agentpools?poolId=8&_a=agents to create the pool"
    return
}

$agents = Get-Agents -BaseUri $AzureDevOpsUri -AgentPoolId $agentPool.Id -Token $AzureDevOpsToken

# Conditionally download the agent package from MS
if (!(Test-Path -Path $packageFilePath)) {
    Write-Host -ForegroundColor Green "Downloading $packageFilePath file" -NoNewline
    (New-Object System.Net.WebClient).DownloadFile(($uriResult.uri), $packageFilePath)
    Unblock-File -Path $packageFilePath
    Write-Host -ForegroundColor Green " done."
}
else {
    Write-Host -ForegroundColor Green "$packageFilePath has already been downloaded"
}

# Conditionally unpack the package
if (!(Test-Path -Path $AzureDevOpsAgentTempDirectory)) {
    Expand-Archive -Path $packageFilePath -DestinationPath $AzureDevOpsAgentTempDirectory
    Set-Content -Value $uriResult.version -Path (Join-Path -Path $AzureDevOpsAgentTempDirectory -ChildPath version)
}

1..$AgentCount | ForEach-Object {
    $count = "$($_)".PadLeft("$AgentCount".Length + 1, "0")

    $agentName = Get-AgentName -AgentDirectory "agent$Count"
    Write-Verbose "Using agent name $agentName"

    if ($agents | Where-Object { $_.Name -eq $agentName } ) {
        Write-Warning "Skipping installation of agent $agentName. It is already configured on the $($AzureDevOpsUri) instance."
        return
    }
    $agentDirectory = (Join-Path -Path $workingDirectory -ChildPath $agentName)
    Write-Verbose "Starting installation to $agentDirectory"
    New-Agent -AgentDirectory $agentDirectory -AzureDevOpsAgentTempDirectory $AzureDevOpsAgentTempDirectory
}

commit: 6608f9c (2021-10-19 13:35:21-06:00) by Eric Williams Updates script names / remove shell script