├── .gitattributes ├── .gitignore ├── .gitmodules ├── .vscode └── launch.json ├── Build.ps1 ├── Clean.ps1 ├── LICENSE ├── Manage.ps1 ├── README.md ├── Release.ps1 ├── RemoveAgents.ps1 ├── Scale.ps1 ├── autoscalingApp ├── .gitignore ├── AgentsMonitor │ ├── AgentsMonitor.sln │ ├── AutoScaler │ │ ├── App.config │ │ ├── AutoScaler.csproj │ │ ├── Functions.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ ├── AssemblyInfo.cs │ │ │ └── webjob-publish-settings.json │ │ └── packages.config │ ├── AzureDevOps.Operations │ │ ├── AzureDevOps.Operations.csproj │ │ ├── AzureDevOps.Operations.csproj.DotSettings │ │ ├── Classes │ │ │ ├── Checker.cs │ │ │ ├── Constants.cs │ │ │ ├── Operations.cs │ │ │ └── Retrieve.cs │ │ ├── Helpers │ │ │ ├── DataPreparation.cs │ │ │ ├── Decisions.cs │ │ │ ├── DynamicProps.cs │ │ │ ├── GetData.cs │ │ │ ├── GetTypedSetting.cs │ │ │ ├── LeaveTheBuilding.cs │ │ │ ├── Mockable │ │ │ │ └── Clock.cs │ │ │ ├── Properties.cs │ │ │ └── SettingsChecker.cs │ │ ├── Models │ │ │ ├── AgentPools.cs │ │ │ ├── Agents.cs │ │ │ ├── JobRequests.cs │ │ │ ├── Partials │ │ │ │ ├── LinkSelf.cs │ │ │ │ └── Links.cs │ │ │ └── ScaleSetVirtualMachineStripped.cs │ │ ├── Properties │ │ │ └── AssemblyInfo.cs │ │ ├── app.config │ │ └── packages.config │ ├── TableStorageClient │ │ ├── Classes │ │ │ ├── CommonTasks.cs │ │ │ └── TableOperations.cs │ │ ├── Interfaces │ │ │ └── ITableOperations.cs │ │ ├── Models │ │ │ └── ScaleEventEntity.cs │ │ ├── Properties │ │ │ └── AssemblyInfo.cs │ │ ├── TableStorageClient.csproj │ │ ├── app.config │ │ └── packages.config │ └── Tests │ │ └── AzureDevOps.Operations.Tests │ │ ├── AzureDevOps.Operations.Tests.csproj │ │ ├── Classes │ │ ├── RetrieveTests.cs │ │ └── TestInitilizers.cs │ │ ├── Data │ │ ├── TestData │ │ │ ├── Agents │ │ │ │ └── allAgents.json │ │ │ ├── GetPoolId │ │ │ │ ├── pools-fail.json │ │ │ │ └── pools-success.json │ │ │ └── JobRequests │ │ │ │ ├── jobs-0-running-1-demands.json │ │ │ │ ├── jobs-0-running-no-demands.json │ │ │ │ ├── jobs-0-running.json │ │ │ │ ├── jobs-1-running.json │ │ │ │ ├── jobs-3-running-2-demands.json │ │ │ │ └── jobs-3-running.json │ │ └── TestsConstants.cs │ │ ├── Helpers │ │ ├── DataPreparationTests.cs │ │ ├── DecisionsTest.cs │ │ ├── DynamicPropsTests.cs │ │ └── PropertiesTests.cs │ │ ├── Properties │ │ └── AssemblyInfo.cs │ │ ├── TestsHelpers │ │ └── HelperMethods.cs │ │ ├── app.config │ │ └── packages.config ├── README.md └── arm-template │ ├── README.md │ ├── azuredeploy.json │ └── azuredeploy.parameters.json ├── builds ├── build.yaml └── clean.yaml ├── config └── small-image.json ├── docs ├── README.md ├── autoscaler-app-build.md ├── autoscaler-app-release.md ├── deploy-Agent.md └── image-Refresh-Build.md ├── functions ├── helpers.psm1 └── password-helpers.psm1 └── scripts └── AddAgentToVM.ps1 /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | 33 | # NUNIT 34 | *.VisualState.xml 35 | TestResult.xml 36 | 37 | # Build Results of an ATL Project 38 | [Dd]ebugPS/ 39 | [Rr]eleasePS/ 40 | dlldata.c 41 | 42 | # DNX 43 | project.lock.json 44 | project.fragment.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # Visual Studio code coverage results 113 | *.coverage 114 | *.coveragexml 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc 265 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vsts-image-generation"] 2 | path = vsts-image-generation 3 | url = https://github.com/akuryan/vsts-image-generation.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "PowerShell", 9 | "request": "launch", 10 | "name": "PowerShell Launch Current File w/Args Prompt", 11 | "script": "${file}", 12 | "args": [ 13 | "${command:SpecifyScriptArgs}" 14 | ], 15 | "cwd": "${file}" 16 | }, 17 | { 18 | "type": "PowerShell", 19 | "request": "launch", 20 | "name": "PowerShell Launch Current File", 21 | "script": "${file}", 22 | "args": [], 23 | "cwd": "${file}" 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /Build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | $Location = $env:Location, 4 | $PackerFile = $env:Packerfile, 5 | $ClientId = $env:ClientId, 6 | $ClientSecret = $env:ClientSecret, 7 | $TenantId = $env:TenantId, 8 | $SubscriptionId = $env:SubscriptionId, 9 | $ObjectId = $env:ObjectId, 10 | $ManagedImageResourceGroupName = $env:ManagedImageResourceGroupName, 11 | $ManagedImageName = $env:ManagedImageName, 12 | [switch]$InstallPrerequisites, 13 | [switch]$EnforceAzureRm, 14 | #if true - will keep resources in Azure for investigation 15 | [switch]$abortPackerOnError 16 | ) 17 | 18 | #importing module for password generation for installer user 19 | Import-Module $PSScriptRoot\functions\password-helpers.psm1 20 | 21 | Set-StrictMode -Version Latest 22 | $ErrorActionPreference = "Stop" 23 | 24 | if ($InstallPrerequisites) { 25 | "Installing prerequisites" 26 | Set-ExecutionPolicy Bypass -Scope Process -Force 27 | Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) 28 | 29 | "Install Packer" 30 | choco install packer -y --ignore-checksums --force 31 | "Install Git" 32 | choco install git -y 33 | } 34 | 35 | if ($EnforceAzureRm) { 36 | "Install AzureRM PowerShell commands" 37 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force 38 | Install-Module AzureRM -AllowClobber -Force 39 | Import-Module AzureRM 40 | } 41 | 42 | Get-AzureRmResourceGroup -Name $ManagedImageResourceGroupName -ErrorVariable notPresent -ErrorAction SilentlyContinue 43 | if ( -Not $notPresent) { 44 | "Cleaning up previous image versions" 45 | Remove-AzureRmImage -ResourceGroupName $ManagedImageResourceGroupName -ImageName $ManagedImageName -Force 46 | } 47 | 48 | "Build Image" 49 | if ($env:BUILD_REPOSITORY_LOCALPATH) { 50 | Set-Location $env:BUILD_REPOSITORY_LOCALPATH 51 | } 52 | 53 | $commitId = $(git log --pretty=format:'%H' -n 1) 54 | Write-Host "CommitId: $commitId"; 55 | 56 | $installerUserPwd = Get-RandomCharacters -length 5 -characters 'abcdefghiklmnoprstuvwxyz'; 57 | $installerUserPwd += Get-RandomCharacters -length 1 -characters 'ABCDEFGHKLMNOPRSTUVWXYZ'; 58 | $installerUserPwd += Get-RandomCharacters -length 1 -characters '1234567890'; 59 | $installerUserPwd += Get-RandomCharacters -length 1 -characters '!"§$%&/()=?}][{@#*+'; 60 | $installerUserPwd = Scramble-String $installerUserPwd 61 | 62 | 63 | if ($abortPackerOnError) { 64 | packer build ` 65 | -var "commit_id=$commitId" ` 66 | -var "client_id=$ClientId" ` 67 | -var "client_secret=$ClientSecret" ` 68 | -var "tenant_id=$TenantId" ` 69 | -var "subscription_id=$SubscriptionId" ` 70 | -var "object_id=$ObjectId" ` 71 | -var "location=$Location" ` 72 | -var "managed_image_resource_group_name=$ManagedImageResourceGroupName" ` 73 | -var "managed_image_name=$ManagedImageName" ` 74 | -var "install_password=$installerUserPwd" ` 75 | -on-error=abort ` 76 | $PackerFile 77 | } else { 78 | packer build ` 79 | -var "commit_id=$commitId" ` 80 | -var "client_id=$ClientId" ` 81 | -var "client_secret=$ClientSecret" ` 82 | -var "tenant_id=$TenantId" ` 83 | -var "subscription_id=$SubscriptionId" ` 84 | -var "object_id=$ObjectId" ` 85 | -var "location=$Location" ` 86 | -var "managed_image_resource_group_name=$ManagedImageResourceGroupName" ` 87 | -var "managed_image_name=$ManagedImageName" ` 88 | -var "install_password=$installerUserPwd" ` 89 | $PackerFile 90 | } 91 | 92 | 93 | if ($LASTEXITCODE -eq 1){ 94 | Write-Error "Packer build faild" 95 | exit 1 96 | } -------------------------------------------------------------------------------- /Clean.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [string]$ManagedImageName, 4 | [string]$ManagedImageResourceGroupName, 5 | [string]$AgentPoolResourceGroup, 6 | [switch]$RemovePackerResourceGroups, 7 | [switch]$RemoveManagedImages, 8 | [switch]$RemoveAgentPoolResourceGroup 9 | ) 10 | 11 | Set-StrictMode -Version Latest 12 | $ErrorActionPreference = "Stop" 13 | 14 | if ( $RemovePackerResourceGroups) { 15 | "Removing all temporary Packer resource groups" 16 | Get-AzureRmResourceGroup | Where-Object ResourceGroupName -like packer-resource-group-* | Remove-AzureRmResourceGroup -Force 17 | } 18 | else { 19 | "Skip removing Packer resource groups" 20 | } 21 | 22 | if ( $RemoveManagedImages) { 23 | "Remove Managed Image $ManagedImageName in $ManagedImageResourceGroupName" 24 | Remove-AzureRmImage -ResourceGroupName $ManagedImageResourceGroupName -ImageName $ManagedImageName -Force 25 | } 26 | else { 27 | "Skip removing managed images" 28 | } 29 | 30 | if ( $RemoveAgentPoolResourceGroup) { 31 | "Remove agent pool resource group $AgentPoolResourceGroup" 32 | 33 | Get-AzureRmResourceGroup -Name $AgentPoolResourceGroup -ev notPresent -ea 0 34 | 35 | if (-Not $notPresent) { 36 | Remove-AzureRmResourceGroup -Name $AgentPoolResourceGroup -Force 37 | } 38 | } 39 | else { 40 | "Skip removing agent pool resource group" 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Anton Kuryan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Manage.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [string]$resourcesBaseName, 4 | [Parameter(Mandatory=$true)] 5 | $Action 6 | 7 | ) 8 | 9 | Import-Module $PSScriptRoot\functions\helpers.psm1 10 | $ResourceGroup = GenerateResourceGroupName -baseName $resourcesBaseName; 11 | $ScaleSet = GenerateVmssName -baseName $resourcesBaseName; 12 | 13 | Set-StrictMode -Version Latest 14 | $ErrorActionPreference = "Stop" 15 | 16 | Get-AzureRmResourceGroup -Name $ResourceGroup -ErrorVariable notPresent -ErrorAction SilentlyContinue | Out-Null 17 | if ( $notPresent) { 18 | "Resource group $ResourceGroup does not exist. Exiting script" 19 | exit 20 | } 21 | 22 | try { 23 | Get-AzureRmVmss -ResourceGroupName $ResourceGroup -VMScaleSetName $ScaleSet | Out-Null 24 | } 25 | catch { 26 | "Scale set $ScaleSet does not exist. Exiting script" 27 | exit 28 | } 29 | 30 | If ($Action -eq "Start") { 31 | Start-AzureRmVmss -ResourceGroupName $ResourceGroup -VMScaleSetName $ScaleSet 32 | } 33 | ElseIf ($Action -eq "Stop") { 34 | Stop-AzureRmVmss -ResourceGroupName $ResourceGroup -VMScaleSetName $ScaleSet -Force 35 | } 36 | Else { 37 | Write-Error "Unrecognized action $Action" 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Self hosted Azure DevOps agents 2 | Scripts to build and deploy VMs to be used as hosted agents for VSTS, based on great work of Wouter de Kort - see [this repository](https://github.com/WouterDeKort/VSTSHostedAgentPool) and his [blog posts series](https://wouterdekort.com/2018/02/25/build-your-own-hosted-vsts-agent-cloud-part-1-build/). 3 | 4 | Since it took big amount of efforts to put the repository to this state - I decided to create own public repository, without maintaining a fork relations with original repository. 5 | 6 | ## Setting up 7 | 8 | 1. Create your own packer image description. I am using [own fork](https://github.com/akuryan/vsts-image-generation) of https://github.com/Microsoft/azure-pipelines-image-generation as a submodule, as I do not need all the features Microsoft adds to their's build agents. 9 | 10 | 1. Build image locally (in future, you will be able to do it via agent, but, if you do not have private agents – msft one’s would not allow you to run 4-7 hours long job, as far as I know). It is done via [Build.ps1](https://github.com/akuryan/self-hosted-azure-devops-agents/blob/master/Build.ps1) with parameters (for automating it as a Build pipeline at Azure DevOps – there is a https://github.com/akuryan/self-hosted-azure-devops-agents/blob/master/builds/build.yaml - in fact, just one step). See further for parameters. After you have an own hosted agent running - you could create a build on it to [refresh an image](./docs/image-Refresh-Build.md). 11 | 12 | 1. After image is built – you can deploy your new agents via https://github.com/akuryan/self-hosted-azure-devops-agents/blob/master/Release.ps1 - this you could do already at Microsoft agents. See further for parameters and see [description of release pipeline](./docs/deploy-Agent.md) 13 | 14 | When this is done – you can build and deploy autoscaling application https://github.com/akuryan/self-hosted-azure-devops-agents/tree/master/autoscalingApp (there is an arm template and little bit of description at my blog https://dobryak.org/self-hosted-agents-at-azure-devops-a-little-cost-saving-trick/ ). 15 | 16 | While you are working on Autoscaling app – you can use https://github.com/akuryan/self-hosted-azure-devops-agents/blob/master/Manage.ps1 to be executed on schedule to save little bit on costs. 17 | 18 | ### Build.ps1 parameters 19 | 20 | ```Location``` - in which datacenter image for VMSS shall be built 21 | 22 | ```PackerFile``` - packer file path to use 23 | 24 | ```ClientId``` - Client ID for your Azure Service Principle 25 | 26 | ```ClientSecret``` - Client Secret for your Azure Service Principle 27 | 28 | ```TenantId``` - Tenant ID for your Azure Service Principle 29 | 30 | ```SubscriptionId``` - Subscription ID 31 | 32 | ```ObjectId``` - Object ID for your Azure Service Principle 33 | 34 | ```ManagedImageResourceGroupName``` - resource group, where image will be stored 35 | 36 | ```ManagedImageName``` - Image name prefix; it will be postfixed with build number. 37 | 38 | ```InstallPrerequisites``` - switch, should script install packer and git on environment, where it is executed 39 | 40 | ```EnforceAzureRm``` - switch, should script install latest AzureRM module 41 | 42 | ```abortPackerOnError``` - switch, specifies, if packer resources should be kept online if there was an error during packer build. 43 | 44 | ### Release.ps1 parameters 45 | 46 | ```VMUser``` - username to access VM via RDP or any other allowed mean of connection; during Azure DevOps build could be specified just a variable. 47 | 48 | ```VMUserPassword``` - password for ```VMUser```; during Azure DevOps build could be specified just a variable. 49 | 50 | ```VMName``` - virtual machines prefix name; could not be longer than 9 symbols 51 | 52 | ```ManagedImageResourceGroupName``` - resource group name, where image will be stored. 53 | 54 | ```ManagedImageName``` - name for the image 55 | 56 | ```Location``` - Azure datacenter location for an image, defaults to West Europe 57 | 58 | ```resourcesBaseName``` - base name for resources; other resource names would be constructed by adding postfixes to this base name 59 | 60 | ```VSTSToken``` - your Azure DevOps personal access token [see this](https://docs.microsoft.com/en-gb/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops) 61 | 62 | ```VSTSUrl``` - your Azure DevOps url 63 | 64 | ```pipRg``` - resource group name for Public IP address. By default, resource group, which hosts VM Scale Set, will be destroyed, during redeployment of VMSS (Virtual Machines Scale Set). So, if you want to keep Public IP address to yourself - put it in separate resource group. 65 | 66 | ```vmssCapacity``` - amount of Virtual Machines in VMSS 67 | 68 | ```vmssSkuName``` - VMSS SKU Name; default value set to "Standard_D4s_v3" 69 | 70 | ```vstsPoolName``` - pool name to add agents too in Azure DevOps; default value set to "Default" 71 | 72 | ```vstsAgentPackageUri``` - URL to download Azure DevOps agents package for deployment; it is auto-updating, so shall not be the latest one here; have default value specified 73 | 74 | ```vmssDiskStorageAccount``` - disk accounts to be used by VMs in VMSS; defaults to "StandardSSD_LRS", which means that it is Standard SSD drive (IMHO, good balance between cost and speed) 75 | 76 | ```attachDataDisk``` - specifies, if we should provision a data disk; defaults to ```false```, as current image will be built with 256 GiB, which is enough. 77 | 78 | ```vmssDataDiskSize``` - if ```attachDataDisk``` is set to ```true```, then this parameter specifies size in GiB for data disk to be attached (one pays for size); also, on this disk work folder of agent will be installed as well. 79 | 80 | ```attachNsg``` - specifies, if Network Security Group (NSG) shall be attached to VMSS 81 | 82 | ```allowedIps``` - Provide an address range using CIDR notation (e.g. 192.168.99.0/24); an IP address (e.g. 192.168.99.0); or a list of address ranges or IP addresses (e.g. 192.168.99.0/24,10.0.0.0/24,44.66.0.0/24) 83 | 84 | ```allowedPorts``` - Provide a single port, such as 80; a port range, such as 1024-65535; or a comma-separated list of single ports and/or port ranges, such as 80,1024-65535. This specifies on which ports traffic will be allowed or denied by this rule. Provide an asterisk (*) to allow traffic on any port. 85 | 86 | ```deployToExistingVnet``` - defines, if we shall deploy to existing VNet or to provision new VNet 87 | 88 | ```subnetName``` - if ```deployToExistingVnet``` is set to ```true```, then here valid and existing subnet name shall be provided 89 | 90 | ```vnetName``` - if ```deployToExistingVnet``` is set to ```true```, then here valid and existing vnet name shall be provided 91 | 92 | ```vnetResourceGroupName``` - if ```deployToExistingVnet``` is set to ```true```, then here valid and existing resource group name, which holds vnet shall be provided. 93 | -------------------------------------------------------------------------------- /RemoveAgents.ps1: -------------------------------------------------------------------------------- 1 | 2 | [CmdletBinding()] 3 | Param( 4 | [string]$VSTSToken = $env:VSTSToken, 5 | [string]$VSTSUrl = $env:VSTSUrl, 6 | $agentPoolPattern = "AgentVM" 7 | ) 8 | 9 | $ErrorActionPreference = "Stop" 10 | Set-StrictMode -Version Latest 11 | 12 | $apiVersion = "3.0-preview.1" 13 | 14 | $base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f "", $VSTSToken))) 15 | 16 | $uri = "${VSTSUrl}/_apis/distributedtask/pools?api-version=${apiVersion}" 17 | 18 | "Calling $uri" 19 | 20 | $allPoolsResult = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo)} 21 | $allPoolsResult 22 | 23 | foreach ($poolRec in $allPoolsResult.value) { 24 | 25 | "Processing Agent Pool $poolRec.name" 26 | 27 | # Get agents of an agent pool (Request method: Get): 28 | $uri = "${VSTSUrl}/_apis/distributedtask/pools/$( $poolRec.id )/agents?api-version=${apiVersion}" 29 | $thisPoolResult = Invoke-RestMethod -Uri $uri -Method Get -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo)} 30 | 31 | foreach ($agentRec in $thisPoolResult.value) { 32 | 33 | "Processing Agent $agentRec.name" 34 | 35 | if ($agentRec.name -match $agentPoolPattern) { 36 | if ($agentRec.status -eq "offline") { 37 | Write-Host "Deleting Agent '$( $agentRec.name )'" -ForegroundColor Red 38 | 39 | #Delete an agent from an agent pool (Request method: Delete): 40 | $uri = "${VSTSUrl}/_apis/distributedtask/pools/$( $poolRec.id )/agents/$( $agentRec.id )?api-version=${apiVersion}" 41 | Invoke-RestMethod -Uri $uri -Method Delete -ContentType "application/json" -Headers @{Authorization = ("Basic {0}" -f $base64AuthInfo)} 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Scale.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [string]$AgentPoolResourceGroup = $env:AgentPoolResourceGroup, 4 | $Capacity = $env:vmssCapacity 5 | ) 6 | 7 | Set-StrictMode -Version Latest 8 | 9 | "Get current scale set" 10 | $vmss = Get-AzureRmVmss -ResourceGroupName $AgentPoolResourceGroup -VMScaleSetName "ScaleSet" 11 | 12 | "Set and update the capacity of your scale set" 13 | $vmss.sku.capacity = $Capacity 14 | Update-AzureRmVmss -ResourceGroupName $AgentPoolResourceGroup -Name "ScaleSet" -VirtualMachineScaleSet $vmss -------------------------------------------------------------------------------- /autoscalingApp/.gitignore: -------------------------------------------------------------------------------- 1 | WebJob/* 2 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AgentsMonitor.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28306.52 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoScaler", "AutoScaler\AutoScaler.csproj", "{7D87E52D-BB3C-4959-8B78-E6990209288F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureDevOps.Operations", "AzureDevOps.Operations\AzureDevOps.Operations.csproj", "{BD529953-84B8-4B37-A4B8-E8E3C8721DAC}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{28F0831F-5EE1-478A-9363-6B1639FF2EC1}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureDevOps.Operations.Tests", "Tests\AzureDevOps.Operations.Tests\AzureDevOps.Operations.Tests.csproj", "{98E36565-AC08-4CA8-8193-8628A1403EE8}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TableStorageClient", "TableStorageClient\TableStorageClient.csproj", "{0B44F66C-56C5-4495-A222-3867250F8648}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {7D87E52D-BB3C-4959-8B78-E6990209288F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {7D87E52D-BB3C-4959-8B78-E6990209288F}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {7D87E52D-BB3C-4959-8B78-E6990209288F}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {7D87E52D-BB3C-4959-8B78-E6990209288F}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {BD529953-84B8-4B37-A4B8-E8E3C8721DAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {BD529953-84B8-4B37-A4B8-E8E3C8721DAC}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {BD529953-84B8-4B37-A4B8-E8E3C8721DAC}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {BD529953-84B8-4B37-A4B8-E8E3C8721DAC}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {98E36565-AC08-4CA8-8193-8628A1403EE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {98E36565-AC08-4CA8-8193-8628A1403EE8}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {98E36565-AC08-4CA8-8193-8628A1403EE8}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {98E36565-AC08-4CA8-8193-8628A1403EE8}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {0B44F66C-56C5-4495-A222-3867250F8648}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {0B44F66C-56C5-4495-A222-3867250F8648}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {0B44F66C-56C5-4495-A222-3867250F8648}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {0B44F66C-56C5-4495-A222-3867250F8648}.Release|Any CPU.Build.0 = Release|Any CPU 38 | EndGlobalSection 39 | GlobalSection(SolutionProperties) = preSolution 40 | HideSolutionNode = FALSE 41 | EndGlobalSection 42 | GlobalSection(NestedProjects) = preSolution 43 | {98E36565-AC08-4CA8-8193-8628A1403EE8} = {28F0831F-5EE1-478A-9363-6B1639FF2EC1} 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {9F8C7C13-7D8C-4EEE-A04D-C5552182C6D3} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AutoScaler/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AutoScaler/Functions.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Classes; 2 | using Microsoft.Azure.WebJobs; 3 | 4 | namespace AutoScaler 5 | { 6 | public static class Functions 7 | { 8 | /// 9 | /// Deprovisioning trigger shall run less frequently than provisioning one 10 | /// 11 | /// 12 | [Singleton] 13 | public static void DeprovisionTrigger([TimerTrigger("0 */15 * * * *", RunOnStartup = true)] 14 | TimerInfo timer) 15 | { 16 | Checker.AgentsQueue(false); 17 | } 18 | 19 | /// 20 | /// Provision more agents shall be running more frequently to allow faster agents provisioning 21 | /// 22 | /// 23 | [Singleton] 24 | public static void ProvisionTrigger([TimerTrigger("0 */3 * * * *", RunOnStartup = true)] 25 | TimerInfo timer) 26 | { 27 | Checker.AgentsQueue(true); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AutoScaler/Program.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Helpers; 2 | using Microsoft.Azure.WebJobs; 3 | using System.Net; 4 | 5 | namespace AutoScaler 6 | { 7 | internal static class Program 8 | { 9 | private static void Main() 10 | { 11 | //little bit of security 12 | //enabling TLS 1.2 13 | ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; 14 | //ban using extremely insecure SSL v3 15 | ServicePointManager.SecurityProtocol &= ~SecurityProtocolType.Ssl3; 16 | //added limits to connection amounts 17 | ServicePointManager.DefaultConnectionLimit = 50; 18 | 19 | //check all required settings 20 | SettingsChecker.CheckAllSettings(); 21 | 22 | var config = new JobHostConfiguration(); 23 | config.UseTimers(); 24 | 25 | if (config.IsDevelopment) 26 | { 27 | config.UseDevelopmentSettings(); 28 | } 29 | 30 | var host = new JobHost(config); 31 | host.RunAndBlock(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AutoScaler/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("AutoScaler")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("AutoScaler")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("7d87e52d-bb3c-4959-8b78-e6990209288f")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Revision and Build Numbers 33 | // by using the '*' as shown below: 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AutoScaler/Properties/webjob-publish-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schemastore.org/schemas/json/webjob-publish-settings.json", 3 | "webJobName": "AutoScaler", 4 | "runMode": "Continuous" 5 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AutoScaler/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/AzureDevOps.Operations.csproj.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | CSharp72 -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Classes/Checker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.Net.Http; 4 | using AzureDevOps.Operations.Helpers; 5 | 6 | namespace AzureDevOps.Operations.Classes 7 | { 8 | /// 9 | /// Class to hold all checks logic 10 | /// 11 | public static class Checker 12 | { 13 | private static Retrieve _dataRetrieveCache; 14 | 15 | internal static Retrieve DataRetriever 16 | { 17 | get 18 | { 19 | if (_dataRetrieveCache != null) 20 | { 21 | return _dataRetrieveCache; 22 | } 23 | var organizationName = ConfigurationManager.AppSettings[Constants.AzureDevOpsInstanceSettingName]; 24 | var accessToken = ConfigurationManager.AppSettings[Constants.AzureDevOpsPatSettingName]; 25 | var httpClient = new HttpClient(); 26 | 27 | _dataRetrieveCache = new Retrieve(organizationName, accessToken, httpClient); 28 | return _dataRetrieveCache; 29 | } 30 | } 31 | /// 32 | /// Checks agent queue and decides, if we need to provision or not 33 | /// 34 | /// describes function which called us 35 | public static void AgentsQueue(bool areWeCheckingToStartVm) 36 | { 37 | var maxAgentsCount = DataRetriever.GetAllAccessibleAgents(Properties.AgentsPoolId); 38 | 39 | if (maxAgentsCount == 0) 40 | { 41 | Console.WriteLine($"There is 0 agents assigned to pool with id {Properties.AgentsPoolId}. Could not proceed, exiting..."); 42 | LeaveTheBuilding.Exit(DataRetriever); 43 | } 44 | 45 | var onlineAgentsCount = 0; 46 | var countNullable = DataRetriever.GetOnlineAgentsCount(Properties.AgentsPoolId); 47 | if (countNullable == null) 48 | { 49 | //something went wrong 50 | Console.WriteLine("Could not retrieve amount of agents online, exiting..."); 51 | LeaveTheBuilding.Exit(DataRetriever); 52 | } 53 | else 54 | { 55 | onlineAgentsCount = countNullable.Value; 56 | } 57 | 58 | var waitingJobsCount = DataRetriever.GetCurrentJobsRunningCount(Properties.AgentsPoolId); 59 | 60 | if (waitingJobsCount == onlineAgentsCount && !new DynamicProps().WeAreInsideBusinessTime) 61 | { 62 | //nothing to do here 63 | return; 64 | } 65 | 66 | Operations.WorkWithVmss(onlineAgentsCount, maxAgentsCount, areWeCheckingToStartVm); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Classes/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDevOps.Operations.Classes 2 | { 3 | public static class Constants 4 | { 5 | /// 6 | /// Defines error exit code 7 | /// 8 | public const int ErrorExitCode = -1; 9 | public const string AzureDevOpsApiVersion = "4.1"; 10 | /// 11 | /// Agents Pool Name 12 | /// 13 | public const string AgentsPoolNameSettingName = "Agents_PoolName"; 14 | /// 15 | /// Agents Pool ID 16 | /// 17 | public const string AgentsPoolIdSettingName = "Agents_PoolId"; 18 | /// 19 | /// Azure DevOps instance to authenticate against 20 | /// 21 | public const string AzureDevOpsInstanceSettingName = "Azure_DevOpsInstance"; 22 | /// 23 | /// Public Access Token for Azure DevOps instance 24 | /// 25 | public const string AzureDevOpsPatSettingName = "Azure_DevOpsPAT"; 26 | //azure service principle 27 | /// 28 | /// Client ID of Azure Service Principle 29 | /// 30 | public const string AzureServicePrincipleClientIdSettingName = "Azure_ServicePrincipleClientId"; 31 | /// 32 | /// Client Secret of Azure Service Principle 33 | /// 34 | public const string AzureServicePrincipleClientSecretSettingName = "Azure_ServicePrincipleClientSecret"; 35 | /// 36 | /// Tenant ID for Azure Service Principle 37 | /// 38 | public const string AzureServicePrincipleTenantIdSettingName = "Azure_ServicePrincipleTenantId"; 39 | //vmss data 40 | /// 41 | /// Defines Azure subscription ID where VMSS resides 42 | /// 43 | public const string AzureSubscriptionIdSettingName = "Azure_SubscriptionId"; 44 | /// 45 | /// Defines resource group name in which VMSS with agents resides 46 | /// 47 | public const string AzureVmssResourceGroupSettingName = "Azure_VMSS_resourceGroupName"; 48 | /// 49 | /// Defines VMSS name 50 | /// 51 | public const string AzureVmssNameSettingName = "Azure_VMSS_Name"; 52 | /// 53 | /// Defines if we are executing test run (so, no actual changes will be done to VMSS agents 54 | /// 55 | public const string DryRunSettingName = "DryRunExecution"; 56 | /// 57 | /// Holds connection string for Azure Storage for logging of (de)provisioning 58 | /// 59 | public const string AzureStorageConnectionStringName = "Azure_Storage_ConnectionString"; 60 | /// 61 | /// Holds pointer to Azure Storage Table for tracking actions 62 | /// 63 | public const string AzureStorageTrackingTableSettingName = "Azure_Storage_ActionsTracking_TableName"; 64 | /// 65 | /// if tracking table name is not set in appSettings - it will default to this 66 | /// 67 | public const string AzureStorageDefaultTrackingTableName = "DefaultTrackingTable"; 68 | /// 69 | /// Setting name to retrieve Business Hours 70 | /// 71 | public const string BusinessHoursRangeSettingName = "BusinessHours_range"; 72 | /// 73 | /// Setting name to retrieve Business days 74 | /// 75 | public const string BusinessHoursDaysSettingName = "BusinessHours_days"; 76 | /// 77 | /// Setting name to retrieve minimal amount of agents running during business time 78 | /// 79 | public const string BusinessHoursAgentsAmountSettingName = "BusinessHours_agents"; 80 | /// 81 | /// String, that allows to identify agent name, demanded by allocated job in 82 | /// 83 | public const string AgentNameMarker = "Agent.Name -equals "; 84 | } 85 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Classes/Operations.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Helpers; 2 | using AzureDevOps.Operations.Models; 3 | using Microsoft.Azure.Management.Compute.Fluent; 4 | using Microsoft.Azure.Management.Fluent; 5 | using Microsoft.Azure.Management.ResourceManager.Fluent; 6 | using Microsoft.Azure.Management.ResourceManager.Fluent.Authentication; 7 | using Microsoft.Azure.Management.ResourceManager.Fluent.Core; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Configuration; 11 | using System.Linq; 12 | using TableStorageClient.Models; 13 | 14 | namespace AzureDevOps.Operations.Classes 15 | { 16 | public static class Operations 17 | { 18 | /// 19 | /// Here we will proceed working with VMSS ((de)provision additional agents, keep current agents count) 20 | /// 21 | /// 22 | /// 23 | /// Describes, which functions calls out - provisioning or deprovisioning 24 | public static void WorkWithVmss(int onlineAgents, int maxAgentsInPool, bool areWeCheckingToStartVmInVmss) 25 | { 26 | //working with VMSS 27 | var vmss = GetVirtualMachinesScaleSet(Properties.VmScaleSetResourceGroupName, Properties.VmScaleSetName); 28 | var virtualMachines = vmss.VirtualMachines.List() 29 | //there could be failed VMs during provisioning 30 | .Where(vm => !vm.Inner.ProvisioningState.Equals("Failed", StringComparison.OrdinalIgnoreCase)) 31 | .Select(vmssVm => new ScaleSetVirtualMachineStripped 32 | { 33 | VmInstanceId = vmssVm.InstanceId, 34 | VmName = vmssVm.ComputerName, 35 | VmInstanceState = vmssVm.PowerState 36 | }).ToArray(); 37 | 38 | //get jobs again to check, if we could deallocate a VM in VMSS 39 | //(if it is running a job - it is not wise to deallocate it) 40 | //since getting VMMS is potentially lengthy operation - we could need this) 41 | var currentJobs = Checker.DataRetriever.GetRunningJobs(Properties.AgentsPoolId); 42 | var amountOfAgents = Decisions.HowMuchAgents(currentJobs.Length, onlineAgents, maxAgentsInPool); 43 | var addMoreAgents = amountOfAgents > 0; 44 | 45 | if (amountOfAgents == 0) 46 | { 47 | //nevertheless - should we (de)provision agents: we are at boundaries 48 | Console.WriteLine("Should not add/remove more agents..."); 49 | return; 50 | } 51 | 52 | //further I need to work with positive numbers only 53 | amountOfAgents = Math.Abs(amountOfAgents); 54 | 55 | if (addMoreAgents != areWeCheckingToStartVmInVmss) 56 | { 57 | //target event is not the same as source one 58 | return; 59 | } 60 | 61 | //I wish this record to be processed on it's own; it is just tracking 62 | RecordDataInTable(addMoreAgents, amountOfAgents); 63 | 64 | if (addMoreAgents) 65 | { 66 | AllocateVms(DataPreparation.GetVmsForAllocation(currentJobs, virtualMachines, amountOfAgents), vmss); 67 | } 68 | else 69 | { 70 | DeallocationWorkWithScaleSet(virtualMachines, currentJobs, vmss, amountOfAgents); 71 | } 72 | } 73 | 74 | private static AzureCredentials Credentials() 75 | { 76 | var clientId = ConfigurationManager.AppSettings[Constants.AzureServicePrincipleClientIdSettingName]; 77 | var clientSecret = ConfigurationManager.AppSettings[Constants.AzureServicePrincipleClientSecretSettingName]; 78 | var tenantId = ConfigurationManager.AppSettings[Constants.AzureServicePrincipleTenantIdSettingName]; 79 | //maybe in future I'll need to extend this one to allow other then Global Azure environment 80 | return SdkContext.AzureCredentialsFactory.FromServicePrincipal(clientId, clientSecret, tenantId, AzureEnvironment.AzureGlobalCloud); 81 | } 82 | 83 | private static IVirtualMachineScaleSet GetVirtualMachinesScaleSet(string rgName, 84 | string virtualMachinesScaleSetName) 85 | { 86 | var credentials = Credentials(); 87 | 88 | var azure = Azure 89 | .Configure() 90 | .WithLogLevel(HttpLoggingDelegatingHandler.Level.Basic) 91 | .Authenticate(credentials) 92 | .WithSubscription(ConfigurationManager.AppSettings[Constants.AzureSubscriptionIdSettingName]); 93 | var virtualMachineScaleSet = azure.VirtualMachineScaleSets.GetByResourceGroup(rgName, virtualMachinesScaleSetName); 94 | if (virtualMachineScaleSet != null) 95 | { 96 | return virtualMachineScaleSet; 97 | } 98 | Console.WriteLine($"Could not retrieve Virtual Machines Scale Set with name {virtualMachinesScaleSetName} in resource group {rgName}. Exiting..."); 99 | LeaveTheBuilding.Exit(Checker.DataRetriever); 100 | 101 | return null; 102 | } 103 | 104 | /// 105 | /// This method will perform all changes to scale set 106 | /// 107 | private static void DeallocationWorkWithScaleSet( 108 | ScaleSetVirtualMachineStripped[] virtualMachinesStripped, 109 | JobRequest[] executingJobs, 110 | IVirtualMachineScaleSet scaleSet, int agentsLimit) 111 | { 112 | Console.WriteLine("Deallocating VMs"); 113 | //we need to downscale, only running VMs shall be selected here 114 | var vmInstancesCollection = Decisions.CollectInstanceIdsToDeallocate(virtualMachinesStripped.Where(vm => vm.VmInstanceState.Equals(PowerState.Running)), executingJobs); 115 | DeallocateVms(vmInstancesCollection, scaleSet, agentsLimit); 116 | 117 | //if we are deprovisioning - it is some time to do some housekeeping as well 118 | if (Properties.IsDryRun) 119 | { 120 | return; 121 | } 122 | 123 | HouseKeeping(scaleSet); 124 | } 125 | 126 | private static void DeallocateVms(IEnumerable vmInstances, IVirtualMachineScaleSet scaleSet, int agentsCountToDeallocate) 127 | { 128 | var virtualMachinesCounter = 0; 129 | foreach (var vmInstance in vmInstances) 130 | { 131 | if (virtualMachinesCounter >= agentsCountToDeallocate) 132 | { 133 | break; 134 | } 135 | Console.WriteLine($"Deallocating VM with instance ID {vmInstance}"); 136 | virtualMachinesCounter++; 137 | if (Decisions.IsVmExecutingJob(vmInstance.VmName)) 138 | { 139 | //this VM just got job assigned, so we should not deallocate it 140 | continue; 141 | } 142 | 143 | if (Properties.IsDryRun) 144 | { 145 | continue; 146 | } 147 | 148 | scaleSet.VirtualMachines.Inner.BeginDeallocateWithHttpMessagesAsync(Properties.VmScaleSetResourceGroupName, Properties.VmScaleSetName, 149 | vmInstance.VmInstanceId); 150 | } 151 | } 152 | 153 | /// 154 | /// If there is a failed VM in VMSS - we can reimage them 155 | /// 156 | /// 157 | private static async void HouseKeeping(IVirtualMachineScaleSet scaleSet) 158 | { 159 | var failedVms = scaleSet.VirtualMachines.List().Where(vm => 160 | vm.Inner.ProvisioningState.Equals("Failed", StringComparison.OrdinalIgnoreCase)).ToArray(); 161 | 162 | if (!failedVms.Any()) 163 | { 164 | return; 165 | } 166 | Console.WriteLine("We have some failed VMs and will try to reimage them async"); 167 | foreach (var virtualMachineScaleSetVm in failedVms) 168 | { 169 | await virtualMachineScaleSetVm.ReimageAsync(); 170 | } 171 | } 172 | 173 | private static void AllocateVms(IEnumerable virtualMachinesStripped, IVirtualMachineScaleSet scaleSet) 174 | { 175 | Console.WriteLine("Starting more VMs"); 176 | foreach (var virtualMachineStripped in virtualMachinesStripped) 177 | { 178 | Console.WriteLine($"Starting VM {virtualMachineStripped.VmName} with id {virtualMachineStripped.VmInstanceId}"); 179 | if (!Properties.IsDryRun) 180 | { 181 | scaleSet.VirtualMachines.Inner.BeginStartWithHttpMessagesAsync(Properties.VmScaleSetResourceGroupName, Properties.VmScaleSetName, 182 | virtualMachineStripped.VmInstanceId); 183 | } 184 | } 185 | } 186 | 187 | private static async void RecordDataInTable(bool isProvisioning, int agentsCount) 188 | { 189 | var storageConnectionString = ConfigurationManager.AppSettings[Constants.AzureStorageConnectionStringName]; 190 | 191 | if (string.IsNullOrWhiteSpace(storageConnectionString)) 192 | { 193 | Console.WriteLine("Connection string is not defined for Azure Storage"); 194 | //connection string for Azure Storage is not defined 195 | return; 196 | } 197 | 198 | if (Properties.ActionsTrackingOperations == null) 199 | { 200 | Console.WriteLine($"Could not connect to Azure Storage Table {Properties.StorageTableName}"); 201 | return; 202 | } 203 | 204 | var entity = new ScaleEventEntity(Properties.VmScaleSetName) { IsProvisioningEvent = isProvisioning, AmountOfVms = agentsCount }; 205 | 206 | await Properties.ActionsTrackingOperations.InsertOrReplaceEntityAsync(entity); 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Classes/Retrieve.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Helpers; 2 | using AzureDevOps.Operations.Models; 3 | using System; 4 | using System.Linq; 5 | using System.Net.Http; 6 | 7 | namespace AzureDevOps.Operations.Classes 8 | { 9 | public sealed class Retrieve : IDisposable 10 | { 11 | /// 12 | /// Organization agentsPoolName in Azure DevOps 13 | /// 14 | private string AzureDevOpsOrganizationName { get; } 15 | 16 | /// 17 | /// PAT (Personal Access Token) to access Azure DevOps 18 | /// 19 | private string AzureDevOpsPersonalAccessToken { get; } 20 | 21 | /// 22 | /// Needed for mocking and testing 23 | /// 24 | private readonly HttpClient _localHttpClient; 25 | 26 | public Retrieve(string orgName, string token, HttpClient httpClient) 27 | { 28 | AzureDevOpsOrganizationName = orgName; 29 | AzureDevOpsPersonalAccessToken = token; 30 | _localHttpClient = httpClient; 31 | } 32 | /// 33 | /// Starting string for an URL 34 | /// 35 | private const string AzureDevOpsUrl = "https://dev.azure.com"; 36 | /// 37 | /// API used to retrieve running tasks 38 | /// 39 | private const string TasksBaseUrl = "_apis/distributedtask/pools"; 40 | 41 | /// 42 | /// Retrieves pool id basing on Name 43 | /// 44 | /// 45 | /// 46 | public int? GetPoolId(string agentsPoolName) 47 | { 48 | var url = $"{AzureDevOpsUrl}/{AzureDevOpsOrganizationName}/{TasksBaseUrl}"; 49 | 50 | var allPools = GetData.DownloadSerializedJsonData(url, AzureDevOpsPersonalAccessToken, _localHttpClient); 51 | 52 | if (allPools == null) 53 | { 54 | return null; 55 | } 56 | 57 | foreach (var agentPool in allPools.Pools) 58 | { 59 | if (!agentPool.Name.Equals(agentsPoolName, StringComparison.OrdinalIgnoreCase)) 60 | { 61 | continue; 62 | } 63 | 64 | if (agentPool.Id != null) 65 | { 66 | return (int)agentPool.Id.Value; 67 | } 68 | } 69 | 70 | return null; 71 | } 72 | 73 | /// 74 | /// Gets all agents, which is online now 75 | /// 76 | /// 77 | /// 78 | public int? GetOnlineAgentsCount(int agentsPoolId) 79 | { 80 | const string requiredStatus = "online"; 81 | 82 | var allAgents = GetAllAgentsRunningNow(agentsPoolId); 83 | 84 | return allAgents?.AllAgents.Count(agent => agent.Status.Equals(requiredStatus, StringComparison.OrdinalIgnoreCase)); 85 | } 86 | 87 | /// 88 | /// Gets all possible agents in current pool count (this shall be set on provisioning time) 89 | /// 90 | /// 91 | /// 92 | public int GetAllAccessibleAgents(int agentsPoolId) 93 | { 94 | //gets agents in all statuses assigned to pool; maybe need to check VMSS size instead?? 95 | var allAgents = GetAllAgentsRunningNow(agentsPoolId); 96 | if (allAgents?.Count != null) 97 | { 98 | return (int)allAgents.Count.Value; 99 | } 100 | 101 | return 0; 102 | } 103 | 104 | /// 105 | /// Gets count of current running jobs (where result is null) 106 | /// 107 | /// 108 | /// 109 | public int GetCurrentJobsRunningCount(int agentsPoolId) 110 | { 111 | var allJobsRequests = GetRunningJobs(agentsPoolId); 112 | 113 | //count amount of jobs without result - they are running 114 | return allJobsRequests?.Length ?? 0; 115 | } 116 | 117 | private Agents GetAllAgentsRunningNow(int agentsPoolId) 118 | { 119 | var url = $"{AzureDevOpsUrl}/{AzureDevOpsOrganizationName}/{TasksBaseUrl}/{agentsPoolId}/agents"; 120 | return GetData.DownloadSerializedJsonData(url, AzureDevOpsPersonalAccessToken, _localHttpClient); 121 | } 122 | 123 | /// 124 | /// Gets actually running now jobs 125 | /// 126 | /// 127 | /// 128 | public JobRequest[] GetRunningJobs(int agentsPoolId) 129 | { 130 | var url = $"{AzureDevOpsUrl}/{AzureDevOpsOrganizationName}/{TasksBaseUrl}/{agentsPoolId}/jobrequests"; 131 | return GetData.DownloadSerializedJsonData(url, AzureDevOpsPersonalAccessToken, _localHttpClient)?.AllJobRequests?.Where(x => x.Result == null).ToArray(); 132 | } 133 | 134 | public void Dispose() 135 | { 136 | _localHttpClient?.Dispose(); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Helpers/DataPreparation.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Models; 2 | using Microsoft.Azure.Management.Compute.Fluent; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using AzureDevOps.Operations.Classes; 7 | 8 | namespace AzureDevOps.Operations.Helpers 9 | { 10 | public static class DataPreparation 11 | { 12 | /// 13 | /// Collects Virtual Machines to be started in Virtual Machines Scale Set 14 | /// 15 | /// Currently running jobs 16 | /// Stripped data about VM in VMSS 17 | /// Amount of agents to allocate 18 | /// 19 | public static IEnumerable GetVmsForAllocation(JobRequest[] runningJobs, IEnumerable virtualMachines, int agentsToAllocateCount) 20 | { 21 | var virtualMachinesCollectionEnumerated = virtualMachines.ToList(); 22 | var vmsToStart = virtualMachinesCollectionEnumerated 23 | .Where(vm => CollectDemandedAgentNames(runningJobs).Contains(vm.VmName)).ToList(); 24 | 25 | //remove already added values 26 | foreach (var virtualMachine in vmsToStart) 27 | { 28 | virtualMachinesCollectionEnumerated.Remove(virtualMachine); 29 | } 30 | 31 | agentsToAllocateCount = agentsToAllocateCount - vmsToStart.Count; 32 | //we do not need to start extra VMs, if there is some of them starting already 33 | agentsToAllocateCount = agentsToAllocateCount - 34 | virtualMachinesCollectionEnumerated 35 | .Count(vm => vm.VmInstanceState.Equals(PowerState.Starting)); 36 | agentsToAllocateCount = agentsToAllocateCount < 0 ? 0 : agentsToAllocateCount; 37 | 38 | //out of deallocated VMs - select needed amount of agents 39 | vmsToStart.AddRange(virtualMachinesCollectionEnumerated 40 | .Where(vm => vm.VmInstanceState.Equals(PowerState.Deallocated)) 41 | .Take(agentsToAllocateCount).ToList()); 42 | 43 | return vmsToStart; 44 | } 45 | 46 | /// 47 | /// Collects all agent names, which are demanded by scheduled jobs 48 | /// 49 | /// 50 | /// 51 | public static string[] CollectDemandedAgentNames(JobRequest[] scheduledJobs) 52 | { 53 | return (from job in scheduledJobs 54 | where job.Demands != null 55 | let agentNameIndex = Array.FindIndex(job.Demands, x => x.ToLower().StartsWith(Constants.AgentNameMarker.ToLower())) 56 | where agentNameIndex >= 0 57 | select job.Demands[agentNameIndex].Replace(Constants.AgentNameMarker, string.Empty) 58 | into agentName 59 | where !string.IsNullOrWhiteSpace(agentName) 60 | select agentName) 61 | .ToArray(); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Helpers/Decisions.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Classes; 2 | using AzureDevOps.Operations.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace AzureDevOps.Operations.Helpers 8 | { 9 | public static class Decisions 10 | { 11 | /// 12 | /// Decides how much agents must be added/stopped 13 | /// 14 | /// Amount of current jobs running and/or waiting 15 | /// Amount of online agents now 16 | /// Maximum accessible agents in current pool 17 | /// Count of agents to be added (positive) or stopped (negative) 18 | public static int HowMuchAgents(int jobs, int agentsCount, int maxAgents) 19 | { 20 | if (agentsCount == maxAgents && jobs >= agentsCount) 21 | { 22 | //there is more jobs than we could have agents deployed 23 | return 0; 24 | } 25 | 26 | var amountOfAgents = jobs - agentsCount; 27 | 28 | var dynamicProperties = new DynamicProps(); 29 | 30 | if (dynamicProperties.WeAreInsideBusinessTime && amountOfAgents <= 0) 31 | { 32 | if (agentsCount <= Properties.AmountOfAgents) 33 | { 34 | return Properties.AmountOfAgents - agentsCount; 35 | } 36 | 37 | if (amountOfAgents < Properties.AmountOfAgents) 38 | { 39 | //we need to deprovision agents in business time 40 | return amountOfAgents + Properties.AmountOfAgents; 41 | } 42 | } 43 | 44 | if (dynamicProperties.WeAreInsideBusinessTime && amountOfAgents > 0 && agentsCount < Properties.AmountOfAgents) 45 | { 46 | amountOfAgents = Properties.AmountOfAgents - agentsCount > amountOfAgents 47 | ? Properties.AmountOfAgents - agentsCount 48 | : amountOfAgents; 49 | } 50 | 51 | return amountOfAgents > maxAgents ? Math.Abs(maxAgents - agentsCount) : amountOfAgents; 52 | } 53 | 54 | public static ScaleSetVirtualMachineStripped[] CollectInstanceIdsToDeallocate(IEnumerable vmScaleSetStripped, JobRequest[] jobRequests) 55 | { 56 | var busyAgentsNames = jobRequests.Select(job => job.ReservedAgent?.Name).ToArray(); 57 | 58 | return vmScaleSetStripped 59 | .Where(scaleSetVirtualMachineStripped => !busyAgentsNames.Contains(scaleSetVirtualMachineStripped.VmName)) 60 | .ToArray(); 61 | } 62 | 63 | /// 64 | /// Checks if VM got job assigned during 65 | /// 66 | /// 67 | /// 68 | public static bool IsVmExecutingJob(string vmName) 69 | { 70 | var currentJobs = Checker.DataRetriever.GetRunningJobs(Properties.AgentsPoolId); 71 | return currentJobs.Select(job => job.ReservedAgent?.Name).Contains(vmName); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Helpers/DynamicProps.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Helpers.Mockable; 2 | 3 | namespace AzureDevOps.Operations.Helpers 4 | { 5 | /// 6 | /// Checks dynamic properties 7 | /// 8 | public class DynamicProps 9 | { 10 | /// 11 | /// checks, if we are situated inside business times, defined in settings 12 | /// 13 | public bool WeAreInsideBusinessTime 14 | { 15 | get 16 | { 17 | if (!Properties.BusinessRuntimeDefined) 18 | { 19 | //if business requirements is not defined - then we are not inside them, actually :D 20 | return Properties.BusinessRuntimeDefined; 21 | } 22 | 23 | var currentTime = Clock.Now; 24 | //checks that current time falls in defined values 25 | return (currentTime.DayOfWeek >= Properties.BusinessDaysStartingDay 26 | && currentTime.DayOfWeek <= Properties.BusinessDaysLastDay 27 | && currentTime.Hour >= Properties.BussinesDayStartHour 28 | && currentTime.Hour <= Properties.BussinesDayEndHour); 29 | 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Helpers/GetData.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Classes; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using System.Web; 8 | 9 | namespace AzureDevOps.Operations.Helpers 10 | { 11 | /// 12 | /// Accesses data from Azure DevOps server 13 | /// 14 | internal static class GetData 15 | { 16 | /// 17 | /// Get data from Azure DevOps server and deserialize it 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | internal static T DownloadSerializedJsonData(string url, string accessToken, HttpClient client) where T : new() 25 | { 26 | client.DefaultRequestHeaders.Accept.Clear(); 27 | client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 28 | var encodedAuth = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{string.Empty}:{accessToken}")); 29 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", encodedAuth); 30 | 31 | var uriBuilder = new UriBuilder(url); 32 | //if we have query string in incoming URL - parse it 33 | var query = HttpUtility.ParseQueryString(uriBuilder.Query); 34 | //append api-version for which our models are built 35 | query["api-version"] = Constants.AzureDevOpsApiVersion; 36 | uriBuilder.Query = query.ToString(); 37 | 38 | var response = client.GetAsync(uriBuilder.Uri).Result; 39 | string jsonData; 40 | 41 | if (response.IsSuccessStatusCode) 42 | { 43 | jsonData = response.Content.ReadAsStringAsync().Result; 44 | } 45 | else 46 | { 47 | //handle non error?? 48 | return new T(); 49 | } 50 | 51 | return JsonConvert.DeserializeObject(jsonData); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Helpers/GetTypedSetting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | 4 | namespace AzureDevOps.Operations.Helpers 5 | { 6 | public static class GetTypedSetting 7 | { 8 | /// 9 | /// Got this example from https://dejanstojanovic.net/aspnet/2015/may/reading-config-value-to-a-proper-data-type/ 10 | /// Returns proper data type from config 11 | /// 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static T GetSetting(string key, T defaultValue = default(T)) where T : IConvertible 17 | { 18 | var val = ConfigurationManager.AppSettings[key] ?? string.Empty; 19 | var result = defaultValue; 20 | if (string.IsNullOrEmpty(val)) 21 | { 22 | return result; 23 | } 24 | var typeDefault = default(T); 25 | if (typeof(T) == typeof(string)) 26 | { 27 | typeDefault = (T)(object)string.Empty; 28 | } 29 | result = (T)Convert.ChangeType(val, typeDefault.GetTypeCode()); 30 | return result; 31 | } 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Helpers/LeaveTheBuilding.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using AzureDevOps.Operations.Classes; 3 | 4 | namespace AzureDevOps.Operations.Helpers 5 | { 6 | public static class LeaveTheBuilding 7 | { 8 | public static void Exit(Retrieve dataRetriever) 9 | { 10 | dataRetriever.Dispose(); 11 | Environment.Exit(Constants.ErrorExitCode); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Helpers/Mockable/Clock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureDevOps.Operations.Helpers.Mockable 4 | { 5 | /// 6 | /// Allows to mock DateTime - thus, test code, which is relying on DateTime objects 7 | /// 8 | public static class Clock 9 | { 10 | public static DateTime Now => _nowImplementation(); 11 | 12 | private static Func _nowImplementation = () => DateTime.Now; 13 | 14 | /// 15 | /// Provides indirect access to NowImplementation of 16 | /// 17 | public static class TestApi 18 | { 19 | // ReSharper disable once MemberHidesStaticFromOuterClass 20 | public static Func Now 21 | { 22 | set => _nowImplementation = value; 23 | } 24 | 25 | public static void Reset() 26 | { 27 | _nowImplementation = () => DateTime.Now; 28 | } 29 | } 30 | 31 | } 32 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Helpers/Properties.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Classes; 2 | using System; 3 | using System.Configuration; 4 | using TableStorageClient.Classes; 5 | using TableStorageClient.Models; 6 | 7 | namespace AzureDevOps.Operations.Helpers 8 | { 9 | public static class Properties 10 | { 11 | internal static string StorageTableName 12 | { 13 | get 14 | { 15 | var tableName = string.IsNullOrWhiteSpace( 16 | ConfigurationManager.AppSettings[Constants.AzureStorageTrackingTableSettingName]) 17 | ? Constants.AzureStorageDefaultTrackingTableName 18 | : ConfigurationManager.AppSettings[Constants.AzureStorageTrackingTableSettingName]; 19 | 20 | if (IsDryRun) 21 | { 22 | //appending DryRun to table name, as dry run data could not be used to train any ML models 23 | tableName = string.Concat(tableName, "DryRun"); 24 | } 25 | //removing dashes (if user set them for table name) 26 | tableName = tableName.Replace("-", string.Empty); 27 | 28 | return tableName; 29 | } 30 | } 31 | 32 | internal static bool IsDryRun => GetTypedSetting.GetSetting(Constants.DryRunSettingName); 33 | 34 | private static string StorageConnectionString => 35 | ConfigurationManager.AppSettings[Constants.AzureStorageConnectionStringName]; 36 | 37 | private static TableOperations _actionsTrackingOperations; 38 | 39 | public static TableOperations ActionsTrackingOperations 40 | { 41 | get 42 | { 43 | if (string.IsNullOrWhiteSpace(StorageConnectionString)) 44 | { 45 | //could not connect to Azure Storage, as there is no connection string defined 46 | return null; 47 | } 48 | 49 | if (_actionsTrackingOperations != null) 50 | { 51 | return _actionsTrackingOperations; 52 | } 53 | 54 | _actionsTrackingOperations = new TableOperations(StorageTableName, StorageConnectionString); 55 | return _actionsTrackingOperations; 56 | } 57 | } 58 | 59 | private static int _agentsPoolId; 60 | /// 61 | /// Stores in backing field agent pool id to minimize calls to Azure DevOps API 62 | /// 63 | internal static int AgentsPoolId 64 | { 65 | get 66 | { 67 | if (_agentsPoolId != 0) 68 | { 69 | //we have correct value in backing field (this code assumes that it is not possible to have pool ID 0) 70 | return _agentsPoolId; 71 | } 72 | 73 | _agentsPoolId = GetTypedSetting.GetSetting(Constants.AgentsPoolIdSettingName); 74 | 75 | //if poolId is not defined in settings - we need to retrieve it 76 | if (_agentsPoolId != 0) 77 | { 78 | return _agentsPoolId; 79 | } 80 | var agentsPoolName = ConfigurationManager.AppSettings[Constants.AgentsPoolNameSettingName]; 81 | var poolIdNullable = Checker.DataRetriever.GetPoolId(agentsPoolName); 82 | if (poolIdNullable == null) 83 | { 84 | //something went wrong 85 | Console.WriteLine($"Could not retrieve pool id for {agentsPoolName}, have to exit"); 86 | LeaveTheBuilding.Exit(Checker.DataRetriever); 87 | //does not makes a sense here, as we are exiting - but it makes compiler happy :) 88 | return 0; 89 | } 90 | _agentsPoolId = poolIdNullable.Value; 91 | 92 | return _agentsPoolId; 93 | } 94 | } 95 | 96 | /// 97 | /// Checks, if business runtime settings are defined 98 | /// 99 | internal static bool BusinessRuntimeDefined => !string.IsNullOrWhiteSpace( 100 | ConfigurationManager.AppSettings[Constants.BusinessHoursRangeSettingName]) 101 | && !string.IsNullOrWhiteSpace( 102 | ConfigurationManager.AppSettings[Constants.BusinessHoursDaysSettingName]) 103 | && !string.IsNullOrWhiteSpace( 104 | ConfigurationManager.AppSettings[Constants.BusinessHoursAgentsAmountSettingName]); 105 | /// 106 | /// Gets starting day for business days 107 | /// 108 | public static DayOfWeek BusinessDaysStartingDay 109 | { 110 | get 111 | { 112 | var setting = ConfigurationManager.AppSettings[Constants.BusinessHoursDaysSettingName]; 113 | if (!setting.Contains("-")) 114 | { 115 | return DayOfWeek.Monday; 116 | } 117 | var startingDayAsString = setting.Split('-')[0]; 118 | var possibleDay = DayParser(startingDayAsString); 119 | 120 | return possibleDay ?? DayOfWeek.Monday; 121 | } 122 | } 123 | /// 124 | /// Gets ending day for business days 125 | /// 126 | public static DayOfWeek BusinessDaysLastDay 127 | { 128 | get 129 | { 130 | var setting = ConfigurationManager.AppSettings[Constants.BusinessHoursDaysSettingName]; 131 | if (!setting.Contains("-")) 132 | { 133 | return DayOfWeek.Friday; 134 | } 135 | var endingDayAsString = setting.Split('-')[1]; 136 | var possibleDay = DayParser(endingDayAsString); 137 | 138 | return possibleDay ?? DayOfWeek.Friday; 139 | } 140 | } 141 | 142 | /// 143 | /// Gets starting hour of a business day 144 | /// 145 | public static int BussinesDayStartHour 146 | { 147 | get 148 | { 149 | var setting = ConfigurationManager.AppSettings[Constants.BusinessHoursRangeSettingName]; 150 | if (!setting.Contains("-")) 151 | { 152 | return 0; 153 | } 154 | 155 | var hourAsString = setting.Split('-')[0]; 156 | 157 | return int.TryParse(hourAsString, out var returnValue) ? returnValue : 0; 158 | } 159 | } 160 | 161 | /// 162 | /// Gets last hour of a business day 163 | /// 164 | public static int BussinesDayEndHour 165 | { 166 | get 167 | { 168 | var setting = ConfigurationManager.AppSettings[Constants.BusinessHoursRangeSettingName]; 169 | if (!setting.Contains("-")) 170 | { 171 | return 0; 172 | } 173 | 174 | var hourAsString = setting.Split('-')[1]; 175 | 176 | return int.TryParse(hourAsString, out var returnValue) ? returnValue : 0; 177 | } 178 | } 179 | /// 180 | /// Parses amount of agents, required during business hours 181 | /// 182 | public static int AmountOfAgents => GetTypedSetting.GetSetting(Constants.BusinessHoursAgentsAmountSettingName); 183 | 184 | public static string VmScaleSetResourceGroupName => 185 | ConfigurationManager.AppSettings[Constants.AzureVmssResourceGroupSettingName]; 186 | public static string VmScaleSetName => ConfigurationManager.AppSettings[Constants.AzureVmssNameSettingName]; 187 | 188 | private static DayOfWeek? DayParser(string day) 189 | { 190 | if (Enum.TryParse(day, true, out DayOfWeek returnValue)) 191 | { 192 | return returnValue; 193 | } 194 | else 195 | { 196 | return null; 197 | } 198 | } 199 | } 200 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Helpers/SettingsChecker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using AzureDevOps.Operations.Classes; 4 | 5 | namespace AzureDevOps.Operations.Helpers 6 | { 7 | public static class SettingsChecker 8 | { 9 | /// 10 | /// Checks that all required settings are defined; if check fails - job will exit 11 | /// 12 | public static void CheckAllSettings() 13 | { 14 | if (string.IsNullOrWhiteSpace( 15 | ConfigurationManager.AppSettings[Constants.AgentsPoolNameSettingName]) && 16 | string.IsNullOrWhiteSpace( 17 | ConfigurationManager.AppSettings[Constants.AgentsPoolIdSettingName])) 18 | { 19 | Console.WriteLine($"In AppSettings neither {Constants.AgentsPoolIdSettingName}, nor {Constants.AgentsPoolNameSettingName} is defined. Exiting..."); 20 | //log error and exit with non success exit code 21 | Environment.Exit(Constants.ErrorExitCode); 22 | } 23 | 24 | ExitIfSettingEmpty(Constants.AzureDevOpsInstanceSettingName, "Azure DevOps instance name"); 25 | ExitIfSettingEmpty(Constants.AzureDevOpsPatSettingName, "Azure DevOps PAT"); 26 | //Azure service principle settings 27 | ExitIfSettingEmpty(Constants.AzureServicePrincipleClientIdSettingName, "Azure Service Principle client ID"); 28 | ExitIfSettingEmpty(Constants.AzureServicePrincipleClientSecretSettingName, "Azure Service Principle client secret"); 29 | ExitIfSettingEmpty(Constants.AzureServicePrincipleTenantIdSettingName, "Azure Service Principle tenant id"); 30 | //azure vmss data 31 | ExitIfSettingEmpty(Constants.AzureSubscriptionIdSettingName, "Azure Subscription id"); 32 | ExitIfSettingEmpty(Constants.AzureVmssResourceGroupSettingName, "Azure VMSS RG Name"); 33 | ExitIfSettingEmpty(Constants.AzureVmssNameSettingName, "Azure VMSS Name"); 34 | } 35 | 36 | /// 37 | /// Checks setting, that it is not empty 38 | /// 39 | /// 40 | /// 41 | private static void ExitIfSettingEmpty(string settingName, string errorMessage = "Setting") 42 | { 43 | if (!string.IsNullOrWhiteSpace(ConfigurationManager.AppSettings[settingName])) 44 | { 45 | return; 46 | } 47 | Console.WriteLine($"{errorMessage} is not defined in {settingName}. Exiting..."); 48 | Environment.Exit(Constants.ErrorExitCode); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Models/AgentPools.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDevOps.Operations.Models 2 | { 3 | using AzureDevOps.Operations.Models.Partials; 4 | using Newtonsoft.Json; 5 | using System; 6 | 7 | /// 8 | /// Get all pools https://{instanceName}.visualstudio.com/_apis/distributedtask/pools?api-version=4.1 9 | /// or https://dev.azure.com/{instanceName}/_apis/distributedtask/pools?api-version=4.1 10 | /// Generated with help of https://app.quicktype.io/#l=cs&r=json2csharp 11 | /// 12 | public partial class AgentsPools 13 | { 14 | [JsonProperty("count", NullValueHandling = NullValueHandling.Ignore)] 15 | public long? Count { get; set; } 16 | 17 | [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] 18 | public Pool[] Pools { get; set; } 19 | } 20 | 21 | public partial class Pool 22 | { 23 | [JsonProperty("createdOn", NullValueHandling = NullValueHandling.Ignore)] 24 | public DateTimeOffset? CreatedOn { get; set; } 25 | 26 | [JsonProperty("autoProvision", NullValueHandling = NullValueHandling.Ignore)] 27 | public bool? AutoProvision { get; set; } 28 | 29 | [JsonProperty("autoSize", NullValueHandling = NullValueHandling.Ignore)] 30 | public bool? AutoSize { get; set; } 31 | 32 | [JsonProperty("agentCloudId")] 33 | public object AgentCloudId { get; set; } 34 | 35 | [JsonProperty("createdBy", NullValueHandling = NullValueHandling.Ignore)] 36 | public CreatedBy CreatedBy { get; set; } 37 | 38 | [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] 39 | public CreatedBy Owner { get; set; } 40 | 41 | [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] 42 | public long? Id { get; set; } 43 | 44 | [JsonProperty("scope", NullValueHandling = NullValueHandling.Ignore)] 45 | public Guid? Scope { get; set; } 46 | 47 | [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] 48 | public string Name { get; set; } 49 | 50 | [JsonProperty("isHosted", NullValueHandling = NullValueHandling.Ignore)] 51 | public bool? IsHosted { get; set; } 52 | 53 | [JsonProperty("poolType", NullValueHandling = NullValueHandling.Ignore)] 54 | public string PoolType { get; set; } 55 | 56 | [JsonProperty("size", NullValueHandling = NullValueHandling.Ignore)] 57 | public long? Size { get; set; } 58 | } 59 | 60 | public partial class CreatedBy 61 | { 62 | [JsonProperty("displayName", NullValueHandling = NullValueHandling.Ignore)] 63 | public string DisplayName { get; set; } 64 | 65 | [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] 66 | public Uri Url { get; set; } 67 | 68 | [JsonProperty("_links", NullValueHandling = NullValueHandling.Ignore)] 69 | public AvatarLinks Links { get; set; } 70 | 71 | [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] 72 | public Guid? Id { get; set; } 73 | 74 | [JsonProperty("uniqueName", NullValueHandling = NullValueHandling.Ignore)] 75 | public string UniqueName { get; set; } 76 | 77 | [JsonProperty("imageUrl", NullValueHandling = NullValueHandling.Ignore)] 78 | public Uri ImageUrl { get; set; } 79 | 80 | [JsonProperty("isContainer", NullValueHandling = NullValueHandling.Ignore)] 81 | public bool? IsContainer { get; set; } 82 | 83 | [JsonProperty("descriptor", NullValueHandling = NullValueHandling.Ignore)] 84 | public string Descriptor { get; set; } 85 | } 86 | 87 | public partial class AvatarLinks 88 | { 89 | [JsonProperty("avatar", NullValueHandling = NullValueHandling.Ignore)] 90 | public LinkSelf Avatar { get; set; } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Models/Agents.cs: -------------------------------------------------------------------------------- 1 |  2 | 3 | namespace AzureDevOps.Operations.Models 4 | { 5 | using AzureDevOps.Operations.Models.Partials; 6 | using Newtonsoft.Json; 7 | using System; 8 | 9 | /// 10 | /// Get all agents and theirs status via https://{instanceName}.visualstudio.com/_apis/distributedtask/pools/{poolId}/agents?api-version=4.1 11 | /// or https://dev.azure.com/{instanceName}/_apis/distributedtask/pools/{poolId}/agents?api-version=4.1 12 | /// Generated with help of https://app.quicktype.io/#l=cs&r=json2csharp 13 | /// 14 | public partial class Agents 15 | { 16 | [JsonProperty("count", NullValueHandling = NullValueHandling.Ignore)] 17 | public long? Count { get; set; } 18 | 19 | [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] 20 | public Agent[] AllAgents { get; set; } 21 | } 22 | 23 | public partial class Agent 24 | { 25 | [JsonProperty("_links", NullValueHandling = NullValueHandling.Ignore)] 26 | public Links Links { get; set; } 27 | 28 | [JsonProperty("maxParallelism", NullValueHandling = NullValueHandling.Ignore)] 29 | public long? MaxParallelism { get; set; } 30 | 31 | [JsonProperty("createdOn", NullValueHandling = NullValueHandling.Ignore)] 32 | public DateTimeOffset? CreatedOn { get; set; } 33 | 34 | [JsonProperty("authorization", NullValueHandling = NullValueHandling.Ignore)] 35 | public Authorization Authorization { get; set; } 36 | 37 | [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] 38 | public long? Id { get; set; } 39 | 40 | [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] 41 | public string Name { get; set; } 42 | 43 | [JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)] 44 | public string Version { get; set; } 45 | 46 | [JsonProperty("osDescription", NullValueHandling = NullValueHandling.Ignore)] 47 | public string OsDescription { get; set; } 48 | 49 | [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] 50 | public bool? Enabled { get; set; } 51 | 52 | [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] 53 | public string Status { get; set; } 54 | 55 | [JsonProperty("provisioningState", NullValueHandling = NullValueHandling.Ignore)] 56 | public string ProvisioningState { get; set; } 57 | 58 | [JsonProperty("accessPoint", NullValueHandling = NullValueHandling.Ignore)] 59 | public string AccessPoint { get; set; } 60 | } 61 | 62 | public partial class Authorization 63 | { 64 | [JsonProperty("clientId", NullValueHandling = NullValueHandling.Ignore)] 65 | public Guid? ClientId { get; set; } 66 | 67 | [JsonProperty("publicKey", NullValueHandling = NullValueHandling.Ignore)] 68 | public PublicKey PublicKey { get; set; } 69 | } 70 | 71 | public partial class PublicKey 72 | { 73 | [JsonProperty("exponent", NullValueHandling = NullValueHandling.Ignore)] 74 | public string Exponent { get; set; } 75 | 76 | [JsonProperty("modulus", NullValueHandling = NullValueHandling.Ignore)] 77 | public string Modulus { get; set; } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Models/JobRequests.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDevOps.Operations.Models 2 | { 3 | using AzureDevOps.Operations.Models.Partials; 4 | using Newtonsoft.Json; 5 | using System; 6 | 7 | /// 8 | /// List all jobs requests in current pool via request https://{instanceName}.visualstudio.com/_apis/distributedtask/pools/{poolId}/jobrequests?api-version=4.1 9 | /// or https://dev.azure.com/{instanceName}/_apis/distributedtask/pools/{poolId}/jobrequests?api-version=4.1 10 | /// Generated with help of https://app.quicktype.io/#l=cs&r=json2csharp 11 | /// 12 | public partial class JobRequests 13 | { 14 | [JsonProperty("count", NullValueHandling = NullValueHandling.Ignore)] 15 | public long? Count { get; set; } 16 | 17 | [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] 18 | public JobRequest[] AllJobRequests { get; set; } 19 | } 20 | 21 | public partial class JobRequest 22 | { 23 | [JsonProperty("requestId", NullValueHandling = NullValueHandling.Ignore)] 24 | public long? RequestId { get; set; } 25 | 26 | [JsonProperty("queueTime", NullValueHandling = NullValueHandling.Ignore)] 27 | public DateTimeOffset? QueueTime { get; set; } 28 | 29 | [JsonProperty("assignTime", NullValueHandling = NullValueHandling.Ignore)] 30 | public DateTimeOffset? AssignTime { get; set; } 31 | 32 | [JsonProperty("receiveTime", NullValueHandling = NullValueHandling.Ignore)] 33 | public DateTimeOffset? ReceiveTime { get; set; } 34 | 35 | [JsonProperty("finishTime", NullValueHandling = NullValueHandling.Ignore)] 36 | public DateTimeOffset? FinishTime { get; set; } 37 | 38 | [JsonProperty("result", NullValueHandling = NullValueHandling.Ignore)] 39 | public string Result { get; set; } 40 | 41 | [JsonProperty("serviceOwner", NullValueHandling = NullValueHandling.Ignore)] 42 | public Guid? ServiceOwner { get; set; } 43 | 44 | [JsonProperty("hostId", NullValueHandling = NullValueHandling.Ignore)] 45 | public Guid? HostId { get; set; } 46 | 47 | [JsonProperty("scopeId", NullValueHandling = NullValueHandling.Ignore)] 48 | public Guid? ScopeId { get; set; } 49 | 50 | [JsonProperty("planType", NullValueHandling = NullValueHandling.Ignore)] 51 | public string PlanType { get; set; } 52 | 53 | [JsonProperty("planId", NullValueHandling = NullValueHandling.Ignore)] 54 | public Guid? PlanId { get; set; } 55 | 56 | [JsonProperty("jobId", NullValueHandling = NullValueHandling.Ignore)] 57 | public Guid? JobId { get; set; } 58 | 59 | [JsonProperty("demands", NullValueHandling = NullValueHandling.Ignore)] 60 | public string[] Demands { get; set; } 61 | 62 | [JsonProperty("reservedAgent", NullValueHandling = NullValueHandling.Ignore)] 63 | public ReservedAgent ReservedAgent { get; set; } 64 | 65 | [JsonProperty("definition", NullValueHandling = NullValueHandling.Ignore)] 66 | public Definition Definition { get; set; } 67 | 68 | [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)] 69 | public Definition Owner { get; set; } 70 | 71 | [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] 72 | public Data Data { get; set; } 73 | 74 | [JsonProperty("poolId", NullValueHandling = NullValueHandling.Ignore)] 75 | public long? PoolId { get; set; } 76 | 77 | [JsonProperty("agentDelays", NullValueHandling = NullValueHandling.Ignore)] 78 | public object[] AgentDelays { get; set; } 79 | 80 | [JsonProperty("orchestrationId", NullValueHandling = NullValueHandling.Ignore)] 81 | public string OrchestrationId { get; set; } 82 | } 83 | 84 | public partial class Data 85 | { 86 | [JsonProperty("ParallelismTag", NullValueHandling = NullValueHandling.Ignore)] 87 | public string ParallelismTag { get; set; } 88 | } 89 | 90 | public partial class Definition 91 | { 92 | [JsonProperty("_links", NullValueHandling = NullValueHandling.Ignore)] 93 | public Links Links { get; set; } 94 | 95 | [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] 96 | public long? Id { get; set; } 97 | 98 | [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] 99 | public string Name { get; set; } 100 | } 101 | 102 | public partial class ReservedAgent 103 | { 104 | [JsonProperty("_links", NullValueHandling = NullValueHandling.Ignore)] 105 | public Links Links { get; set; } 106 | 107 | [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] 108 | public long? Id { get; set; } 109 | 110 | [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] 111 | public string Name { get; set; } 112 | 113 | [JsonProperty("version", NullValueHandling = NullValueHandling.Ignore)] 114 | public string Version { get; set; } 115 | 116 | [JsonProperty("osDescription", NullValueHandling = NullValueHandling.Ignore)] 117 | public string OsDescription { get; set; } 118 | 119 | [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] 120 | public bool? Enabled { get; set; } 121 | 122 | [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)] 123 | public string Status { get; set; } 124 | 125 | [JsonProperty("provisioningState", NullValueHandling = NullValueHandling.Ignore)] 126 | public string ProvisioningState { get; set; } 127 | 128 | [JsonProperty("accessPoint", NullValueHandling = NullValueHandling.Ignore)] 129 | public string AccessPoint { get; set; } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Models/Partials/LinkSelf.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json; 3 | 4 | namespace AzureDevOps.Operations.Models.Partials 5 | { 6 | public partial class LinkSelf 7 | { 8 | [JsonProperty("href", NullValueHandling = NullValueHandling.Ignore)] 9 | public Uri Href { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Models/Partials/Links.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace AzureDevOps.Operations.Models.Partials 4 | { 5 | public partial class Links 6 | { 7 | [JsonProperty("self", NullValueHandling = NullValueHandling.Ignore)] 8 | public LinkSelf LinkSelf { get; set; } 9 | 10 | [JsonProperty("web", NullValueHandling = NullValueHandling.Ignore)] 11 | public LinkSelf Web { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Models/ScaleSetVirtualMachineStripped.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Management.Compute.Fluent; 2 | 3 | namespace AzureDevOps.Operations.Models 4 | { 5 | /// 6 | /// This class holds only required by this projects properties of Virtual Machine from Virtual Machines Scale Set 7 | /// 8 | public class ScaleSetVirtualMachineStripped 9 | { 10 | /// 11 | /// Virtual Machine name 12 | /// 13 | public string VmName { get; set; } 14 | /// 15 | /// Virtual machine Instance Id 16 | /// 17 | public string VmInstanceId { get; set; } 18 | 19 | /// 20 | /// Holds marker if Instance is deallocated or not 21 | /// 22 | public PowerState VmInstanceState { get; set; } 23 | } 24 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("AzureDevOps.Operations")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("AzureDevOps.Operations")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("bd529953-84b8-4b37-a4b8-e8e3c8721dac")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/AzureDevOps.Operations/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/TableStorageClient/Classes/CommonTasks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.WindowsAzure.Storage; 4 | using Microsoft.WindowsAzure.Storage.Table; 5 | 6 | namespace TableStorageClient.Classes 7 | { 8 | public static class CommonTasks 9 | { 10 | public static async Task GetOrCreateTableAsync(string tableName, string storageConnectionString) 11 | { 12 | var storageAccount = CreateStorageAccountFromConnectionString(storageConnectionString); 13 | var tableClient = storageAccount.CreateCloudTableClient(); 14 | 15 | var table = tableClient.GetTableReference(tableName); 16 | 17 | await table.CreateIfNotExistsAsync(); 18 | 19 | return table; 20 | } 21 | 22 | private static CloudStorageAccount CreateStorageAccountFromConnectionString(string storageConnectionString) 23 | { 24 | return CloudStorageAccount.Parse(storageConnectionString); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/TableStorageClient/Classes/TableOperations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.WindowsAzure.Storage.Table; 4 | using TableStorageClient.Interfaces; 5 | 6 | namespace TableStorageClient.Classes 7 | { 8 | public class TableOperations:ITableOperations where T: TableEntity 9 | { 10 | public CloudTable Table { get; set; } 11 | 12 | public TableOperations(string tableName, string connectionString) 13 | { 14 | Table = CommonTasks.GetOrCreateTableAsync(tableName, connectionString).Result; 15 | } 16 | 17 | public async Task InsertOrReplaceEntityAsync(T entity) 18 | { 19 | var insertOrReplaceOperation = TableOperation.InsertOrReplace(entity); 20 | await Table.ExecuteAsync(insertOrReplaceOperation); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/TableStorageClient/Interfaces/ITableOperations.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.WindowsAzure.Storage.Table; 3 | 4 | namespace TableStorageClient.Interfaces 5 | { 6 | public interface ITableOperations where T : TableEntity 7 | { 8 | Task InsertOrReplaceEntityAsync(T entity); 9 | } 10 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/TableStorageClient/Models/ScaleEventEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.WindowsAzure.Storage.Table; 3 | 4 | namespace TableStorageClient.Models 5 | { 6 | public class ScaleEventEntity : TableEntity 7 | { 8 | /// 9 | /// we need empty parameterless constructor here 10 | /// 11 | public ScaleEventEntity() 12 | { 13 | } 14 | 15 | public ScaleEventEntity(string virtualMachinesScaleSetName) 16 | { 17 | PartitionKey = virtualMachinesScaleSetName; 18 | //for now will set row key to emptry string 19 | RowKey = DateTime.UtcNow.ToString("dd-MM-yyyyTHH:mm:ss"); 20 | } 21 | 22 | /// 23 | /// Records, if we are starting more VMs at VMSS or deprovisining existing 24 | /// 25 | public bool IsProvisioningEvent { get; set; } 26 | /// 27 | /// Records how much VMs we are (de)provisioning in given Virtual Machines Scale Set 28 | /// 29 | public int AmountOfVms { get; set; } 30 | } 31 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/TableStorageClient/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("TableStorageClient")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("TableStorageClient")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("0b44f66c-56c5-4495-a222-3867250f8648")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/TableStorageClient/TableStorageClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {0B44F66C-56C5-4495-A222-3867250F8648} 8 | Library 9 | Properties 10 | TableStorageClient 11 | TableStorageClient 12 | v4.7.2 13 | 512 14 | true 15 | 16 | 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | pdbonly 27 | true 28 | bin\Release\ 29 | TRACE 30 | prompt 31 | 4 32 | 33 | 34 | 35 | ..\packages\Microsoft.Azure.KeyVault.Core.1.0.0\lib\net40\Microsoft.Azure.KeyVault.Core.dll 36 | 37 | 38 | ..\packages\WindowsAzure.Storage.9.3.3\lib\net45\Microsoft.WindowsAzure.Storage.dll 39 | 40 | 41 | ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/TableStorageClient/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/TableStorageClient/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Classes/RetrieveTests.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Classes; 2 | using AzureDevOps.Operations.Tests.Data; 3 | using NUnit.Framework; 4 | using RichardSzalay.MockHttp; 5 | using System.IO; 6 | 7 | namespace AzureDevOps.Operations.Tests.Classes 8 | { 9 | public class RetrieveTests 10 | { 11 | [TestCase(@"..\..\Data\TestData\GetPoolId\pools-success.json", Description = "Finds pool id by name successfully")] 12 | public void GetPoolIdTest_Pool_Present(string jsonPath) 13 | { 14 | var dataRetriever = CreateRetriever(jsonPath); 15 | 16 | var poolId = dataRetriever.GetPoolId(TestsConstants.TestPoolName); 17 | 18 | Assert.IsNotNull(poolId); 19 | Assert.AreEqual(poolId.Value, TestsConstants.TestPoolId); 20 | } 21 | 22 | [TestCase(@"..\..\Data\TestData\GetPoolId\pools-fail.json", Description = "There is no pool with required name")] 23 | [TestCase(TestsConstants.FileNotExistPointer, Description = "Response was with status 200, but empty")] 24 | public void GetPoolIdTest_Pool_Not_Present(string jsonPath) 25 | { 26 | var dataRetriever = CreateRetriever(jsonPath); 27 | 28 | var poolId = dataRetriever.GetPoolId(TestsConstants.TestPoolName); 29 | 30 | Assert.IsNull(poolId); 31 | } 32 | 33 | [TestCase(@"..\..\Data\TestData\Agents\allAgents.json", Description = "Check json parsing for agents")] 34 | public void CheckAgentsRetrieval(string jsonPath) 35 | { 36 | var dataRetriever = CreateRetriever(jsonPath); 37 | 38 | var allAgents = dataRetriever.GetAllAccessibleAgents(TestsConstants.TestPoolId); 39 | var onlineAgents = dataRetriever.GetOnlineAgentsCount(TestsConstants.TestPoolId); 40 | 41 | Assert.IsNotNull(allAgents); 42 | Assert.AreEqual(allAgents, TestsConstants.AllAgentsCount); 43 | Assert.IsNotNull(onlineAgents); 44 | Assert.AreEqual(onlineAgents.Value, TestsConstants.OnlineAgentsCount); 45 | } 46 | 47 | [TestCase(@"..\..\Data\TestData\JobRequests\jobs-0-running.json", 0, Description = "There is 0 jobs running according to test JSON")] 48 | [TestCase(TestsConstants.Json1JobIsRunning, 1, Description = "There is 1 job running according to test JSON")] 49 | [TestCase(TestsConstants.FileNotExistPointer, 0, Description = "Response was with status 200, but empty")] 50 | public void CheckJobsRetrieval(string jsonPath, int runningJobs) 51 | { 52 | var dataRetriever = CreateRetriever(jsonPath); 53 | 54 | var jobsRunning = dataRetriever.GetCurrentJobsRunningCount(TestsConstants.TestPoolId); 55 | 56 | Assert.AreEqual(jobsRunning, runningJobs); 57 | } 58 | 59 | internal static Retrieve CreateRetriever(string jsonPathResponse) 60 | { 61 | var mockHttp = new MockHttpMessageHandler(); 62 | 63 | var jsonPathCombined = Path.Combine(System.AppContext.BaseDirectory, jsonPathResponse); 64 | 65 | var response = File.Exists(jsonPathCombined) ? File.ReadAllText(jsonPathCombined) : string.Empty; 66 | 67 | mockHttp.When("*").Respond("application/json", response); 68 | var client = mockHttp.ToHttpClient(); 69 | return new Retrieve(TestsConstants.TestOrganizationName, TestsConstants.TestToken, 70 | client); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Classes/TestInitilizers.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using AzureDevOps.Operations.Classes; 4 | 5 | namespace AzureDevOps.Operations.Tests.Classes 6 | { 7 | /// 8 | /// Collection of test initializers 9 | /// 10 | public static class TestInitilizers 11 | { 12 | /// 13 | /// Sets business times to be Monday to Friday from 10 o'clock till 17 o'clock 14 | /// 15 | public static void InitAppSettingsForBusinessTimesTests() 16 | { 17 | ConfigurationManager.AppSettings[Constants.BusinessHoursRangeSettingName] = "10-17"; 18 | ConfigurationManager.AppSettings[Constants.BusinessHoursDaysSettingName] = "Monday-Friday"; 19 | ConfigurationManager.AppSettings[Constants.BusinessHoursAgentsAmountSettingName] = "3"; 20 | } 21 | 22 | /// 23 | /// Parses string like 14-Dec-2018 15:15 to date time 24 | /// 25 | /// 26 | /// 27 | public static DateTime ParseDateTimeForTest(string dateTimeAsString) 28 | { 29 | return DateTime.Parse(dateTimeAsString); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Data/TestData/Agents/allAgents.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 4, 3 | "value": [ 4 | { 5 | "_links": { 6 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 7 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 8 | }, 9 | "maxParallelism": 1, 10 | "createdOn": "2018-10-04T12:39:46.81Z", 11 | "authorization": { 12 | "clientId": "E4521B4F-8E22-4BF8-A1C3-4257E895037C", 13 | "publicKey": { 14 | "exponent": "test", 15 | "modulus": "1" 16 | } 17 | }, 18 | "id": 15, 19 | "name": "vstsagent000001", 20 | "version": "2.140.2", 21 | "osDescription": "Microsoft Windows 10.0.14393", 22 | "enabled": true, 23 | "status": "online", 24 | "provisioningState": "Provisioned", 25 | "accessPoint": "VstsAccessMapping" 26 | }, 27 | { 28 | "_links": { 29 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/16" }, 30 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=16" } 31 | }, 32 | "maxParallelism": 1, 33 | "createdOn": "2018-10-11T06:55:30.66Z", 34 | "authorization": { 35 | "clientId": "E4521B4F-8E22-4BF8-A1C3-4257E895037C", 36 | "publicKey": { 37 | "exponent": "test", 38 | "modulus": "2" 39 | } 40 | }, 41 | "id": 16, 42 | "name": "vstsagent000002", 43 | "version": "2.140.2", 44 | "osDescription": "Microsoft Windows 10.0.14393", 45 | "enabled": true, 46 | "status": "online", 47 | "provisioningState": "Provisioned", 48 | "accessPoint": "VstsAccessMapping" 49 | }, 50 | { 51 | "_links": { 52 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/17" }, 53 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=17" } 54 | }, 55 | "maxParallelism": 1, 56 | "createdOn": "2018-10-11T06:55:30.817Z", 57 | "authorization": { 58 | "clientId": "E4521B4F-8E22-4BF8-A1C3-4257E895037C", 59 | "publicKey": { 60 | "exponent": "test", 61 | "modulus": "3" 62 | } 63 | }, 64 | "id": 17, 65 | "name": "vstsagent000004", 66 | "version": "2.140.2", 67 | "osDescription": "Microsoft Windows 10.0.14393", 68 | "enabled": true, 69 | "status": "online", 70 | "provisioningState": "Provisioned", 71 | "accessPoint": "VstsAccessMapping" 72 | }, 73 | { 74 | "_links": { 75 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/18" }, 76 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=18" } 77 | }, 78 | "maxParallelism": 1, 79 | "createdOn": "2018-10-11T06:55:55.05Z", 80 | "authorization": { 81 | "clientId": "E4521B4F-8E22-4BF8-A1C3-4257E895037C", 82 | "publicKey": { 83 | "exponent": "test", 84 | "modulus": "4" 85 | } 86 | }, 87 | "id": 18, 88 | "name": "vstsagent000003", 89 | "version": "2.140.2", 90 | "osDescription": "Microsoft Windows 10.0.14393", 91 | "enabled": true, 92 | "status": "offline", 93 | "provisioningState": "Provisioned", 94 | "accessPoint": "VstsAccessMapping" 95 | } 96 | ] 97 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Data/TestData/GetPoolId/pools-fail.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 7, 3 | "value": [ 4 | { 5 | "createdOn": "2016-09-02T19:14:37.35Z", 6 | "autoProvision": true, 7 | "autoSize": true, 8 | "agentCloudId": null, 9 | "createdBy": { 10 | "displayName": "[testProject]\\testAccounts", 11 | "url": "https://prented.to/be/valid/url", 12 | "_links": { "avatar": { "href": "https://picsum.photos/200/300" } }, 13 | "id": "12345678-1234-1234-1234-123456789012", 14 | "uniqueName": "vstfs:///someName", 15 | "imageUrl": "https://picsum.photos/200/300", 16 | "isContainer": true, 17 | "descriptor": "desc" 18 | }, 19 | "owner": { 20 | "displayName": "[testProject]\\testAccounts", 21 | "url": "https://prented.to/be/valid/url", 22 | "_links": { "avatar": { "href": "https://picsum.photos/200/300" } }, 23 | "id": "12345678-1234-1234-1234-123456789012", 24 | "uniqueName": "vstfs:///someName", 25 | "imageUrl": "https://picsum.photos/200/300", 26 | "isContainer": true, 27 | "descriptor": "desc" 28 | }, 29 | "id": 2, 30 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 31 | "name": "Hosted", 32 | "isHosted": true, 33 | "poolType": "automation", 34 | "size": 2 35 | }, 36 | { 37 | "createdOn": "2016-12-01T17:48:45.093Z", 38 | "autoProvision": true, 39 | "autoSize": true, 40 | "agentCloudId": null, 41 | "createdBy": { 42 | "displayName": "[testProject]\\testAccounts", 43 | "url": "https://prented.to/be/valid/url", 44 | "_links": { "avatar": { "href": "https://picsum.photos/200/300" } }, 45 | "id": "12345678-1234-1234-1234-123456789012", 46 | "uniqueName": "vstfs:///someName", 47 | "imageUrl": "https://picsum.photos/200/300", 48 | "isContainer": true, 49 | "descriptor": "desc" 50 | }, 51 | "owner": { 52 | "displayName": "[testProject]\\testAccounts", 53 | "url": "https://prented.to/be/valid/url", 54 | "_links": { "avatar": { "href": "https://picsum.photos/200/300" } }, 55 | "id": "12345678-1234-1234-1234-123456789012", 56 | "uniqueName": "vstfs:///someName", 57 | "imageUrl": "https://picsum.photos/200/300", 58 | "isContainer": true, 59 | "descriptor": "desc" 60 | }, 61 | "id": 3, 62 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 63 | "name": "Hosted Linux Preview", 64 | "isHosted": true, 65 | "poolType": "automation", 66 | "size": 2 67 | }, 68 | { 69 | "createdOn": "2017-03-16T21:02:17.127Z", 70 | "autoProvision": true, 71 | "autoSize": true, 72 | "agentCloudId": null, 73 | "createdBy": { 74 | "displayName": "Microsoft.VisualStudio.Services.TFS", 75 | "url": "https://app.vssps.visualstudio.com", 76 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 77 | "id": "00000002-0000-8888-8000-000000000000", 78 | "uniqueName": "t@t.t", 79 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 80 | "descriptor": "descr" 81 | }, 82 | "owner": { 83 | "displayName": "Microsoft.VisualStudio.Services.TFS", 84 | "url": "https://app.vssps.visualstudio.com", 85 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 86 | "id": "00000002-0000-8888-8000-000000000000", 87 | "uniqueName": "t@t.t", 88 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 89 | "descriptor": "descr" 90 | }, 91 | "id": 4, 92 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 93 | "name": "Hosted VS2017", 94 | "isHosted": true, 95 | "poolType": "automation", 96 | "size": 2 97 | }, 98 | { 99 | "createdOn": "2017-11-02T18:05:42.793Z", 100 | "autoProvision": true, 101 | "autoSize": true, 102 | "agentCloudId": null, 103 | "createdBy": { 104 | "displayName": "Microsoft.VisualStudio.Services.TFS", 105 | "url": "https://app.vssps.visualstudio.com", 106 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 107 | "id": "00000002-0000-8888-8000-000000000000", 108 | "uniqueName": "t@t.t", 109 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 110 | "descriptor": "descr" 111 | }, 112 | "owner": { 113 | "displayName": "Microsoft.VisualStudio.Services.TFS", 114 | "url": "https://app.vssps.visualstudio.com", 115 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 116 | "id": "00000002-0000-8888-8000-000000000000", 117 | "uniqueName": "t@t.t", 118 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 119 | "descriptor": "descr" 120 | }, 121 | "id": 9, 122 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 123 | "name": "Hosted macOS", 124 | "isHosted": true, 125 | "poolType": "automation", 126 | "size": 2 127 | }, 128 | { 129 | "createdOn": "2018-07-24T07:29:32.67Z", 130 | "autoProvision": true, 131 | "autoSize": true, 132 | "agentCloudId": null, 133 | "createdBy": { 134 | "displayName": "Microsoft.VisualStudio.Services.TFS", 135 | "url": "https://app.vssps.visualstudio.com", 136 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 137 | "id": "00000002-0000-8888-8000-000000000000", 138 | "uniqueName": "t@t.t", 139 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 140 | "descriptor": "descr" 141 | }, 142 | "owner": { 143 | "displayName": "Microsoft.VisualStudio.Services.TFS", 144 | "url": "https://app.vssps.visualstudio.com", 145 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 146 | "id": "00000002-0000-8888-8000-000000000000", 147 | "uniqueName": "t@t.t", 148 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 149 | "descriptor": "descr" 150 | }, 151 | "id": 10, 152 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 153 | "name": "Hosted Ubuntu 1604", 154 | "isHosted": true, 155 | "poolType": "automation", 156 | "size": 2 157 | }, 158 | { 159 | "createdOn": "2018-08-09T20:10:05.173Z", 160 | "autoProvision": true, 161 | "autoSize": true, 162 | "agentCloudId": null, 163 | "createdBy": { 164 | "displayName": "Microsoft.VisualStudio.Services.TFS", 165 | "url": "https://app.vssps.visualstudio.com", 166 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 167 | "id": "00000002-0000-8888-8000-000000000000", 168 | "uniqueName": "t@t.t", 169 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 170 | "descriptor": "descr" 171 | }, 172 | "owner": { 173 | "displayName": "Microsoft.VisualStudio.Services.TFS", 174 | "url": "https://app.vssps.visualstudio.com", 175 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 176 | "id": "00000002-0000-8888-8000-000000000000", 177 | "uniqueName": "t@t.t", 178 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 179 | "descriptor": "descr" 180 | }, 181 | "id": 11, 182 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 183 | "name": "Hosted Windows Container", 184 | "isHosted": true, 185 | "poolType": "automation", 186 | "size": 2 187 | } 188 | ] 189 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Data/TestData/GetPoolId/pools-success.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 7, 3 | "value": [ 4 | { 5 | "createdOn": "2016-09-02T19:14:37.35Z", 6 | "autoProvision": true, 7 | "autoSize": true, 8 | "agentCloudId": null, 9 | "createdBy": { 10 | "displayName": "[testProject]\\testAccounts", 11 | "url": "https://prented.to/be/valid/url", 12 | "_links": { "avatar": { "href": "https://picsum.photos/200/300" } }, 13 | "id": "12345678-1234-1234-1234-123456789012", 14 | "uniqueName": "vstfs:///someName", 15 | "imageUrl": "https://picsum.photos/200/300", 16 | "isContainer": true, 17 | "descriptor": "desc" 18 | }, 19 | "owner": { 20 | "displayName": "[testProject]\\testAccounts", 21 | "url": "https://prented.to/be/valid/url", 22 | "_links": { "avatar": { "href": "https://picsum.photos/200/300" } }, 23 | "id": "12345678-1234-1234-1234-123456789012", 24 | "uniqueName": "vstfs:///someName", 25 | "imageUrl": "https://picsum.photos/200/300", 26 | "isContainer": true, 27 | "descriptor": "desc" 28 | }, 29 | "id": 2, 30 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 31 | "name": "Hosted", 32 | "isHosted": true, 33 | "poolType": "automation", 34 | "size": 2 35 | }, 36 | { 37 | "createdOn": "2016-12-01T17:48:45.093Z", 38 | "autoProvision": true, 39 | "autoSize": true, 40 | "agentCloudId": null, 41 | "createdBy": { 42 | "displayName": "[testProject]\\testAccounts", 43 | "url": "https://prented.to/be/valid/url", 44 | "_links": { "avatar": { "href": "https://picsum.photos/200/300" } }, 45 | "id": "12345678-1234-1234-1234-123456789012", 46 | "uniqueName": "vstfs:///someName", 47 | "imageUrl": "https://picsum.photos/200/300", 48 | "isContainer": true, 49 | "descriptor": "desc" 50 | }, 51 | "owner": { 52 | "displayName": "[testProject]\\testAccounts", 53 | "url": "https://prented.to/be/valid/url", 54 | "_links": { "avatar": { "href": "https://picsum.photos/200/300" } }, 55 | "id": "12345678-1234-1234-1234-123456789012", 56 | "uniqueName": "vstfs:///someName", 57 | "imageUrl": "https://picsum.photos/200/300", 58 | "isContainer": true, 59 | "descriptor": "desc" 60 | }, 61 | "id": 3, 62 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 63 | "name": "Hosted Linux Preview", 64 | "isHosted": true, 65 | "poolType": "automation", 66 | "size": 2 67 | }, 68 | { 69 | "createdOn": "2017-03-16T21:02:17.127Z", 70 | "autoProvision": true, 71 | "autoSize": true, 72 | "agentCloudId": null, 73 | "createdBy": { 74 | "displayName": "Microsoft.VisualStudio.Services.TFS", 75 | "url": "https://app.vssps.visualstudio.com", 76 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 77 | "id": "00000002-0000-8888-8000-000000000000", 78 | "uniqueName": "t@t.t", 79 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 80 | "descriptor": "descr" 81 | }, 82 | "owner": { 83 | "displayName": "Microsoft.VisualStudio.Services.TFS", 84 | "url": "https://app.vssps.visualstudio.com", 85 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 86 | "id": "00000002-0000-8888-8000-000000000000", 87 | "uniqueName": "t@t.t", 88 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 89 | "descriptor": "descr" 90 | }, 91 | "id": 4, 92 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 93 | "name": "Hosted VS2017", 94 | "isHosted": true, 95 | "poolType": "automation", 96 | "size": 2 97 | }, 98 | { 99 | "createdOn": "2017-11-02T18:05:42.793Z", 100 | "autoProvision": true, 101 | "autoSize": true, 102 | "agentCloudId": null, 103 | "createdBy": { 104 | "displayName": "Microsoft.VisualStudio.Services.TFS", 105 | "url": "https://app.vssps.visualstudio.com", 106 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 107 | "id": "00000002-0000-8888-8000-000000000000", 108 | "uniqueName": "t@t.t", 109 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 110 | "descriptor": "descr" 111 | }, 112 | "owner": { 113 | "displayName": "Microsoft.VisualStudio.Services.TFS", 114 | "url": "https://app.vssps.visualstudio.com", 115 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 116 | "id": "00000002-0000-8888-8000-000000000000", 117 | "uniqueName": "t@t.t", 118 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 119 | "descriptor": "descr" 120 | }, 121 | "id": 9, 122 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 123 | "name": "Hosted macOS", 124 | "isHosted": true, 125 | "poolType": "automation", 126 | "size": 2 127 | }, 128 | { 129 | "createdOn": "2018-07-24T07:29:32.67Z", 130 | "autoProvision": true, 131 | "autoSize": true, 132 | "agentCloudId": null, 133 | "createdBy": { 134 | "displayName": "Microsoft.VisualStudio.Services.TFS", 135 | "url": "https://app.vssps.visualstudio.com", 136 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 137 | "id": "00000002-0000-8888-8000-000000000000", 138 | "uniqueName": "t@t.t", 139 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 140 | "descriptor": "descr" 141 | }, 142 | "owner": { 143 | "displayName": "Microsoft.VisualStudio.Services.TFS", 144 | "url": "https://app.vssps.visualstudio.com", 145 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 146 | "id": "00000002-0000-8888-8000-000000000000", 147 | "uniqueName": "t@t.t", 148 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 149 | "descriptor": "descr" 150 | }, 151 | "id": 10, 152 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 153 | "name": "Hosted Ubuntu 1604", 154 | "isHosted": true, 155 | "poolType": "automation", 156 | "size": 2 157 | }, 158 | { 159 | "createdOn": "2018-08-09T20:10:05.173Z", 160 | "autoProvision": true, 161 | "autoSize": true, 162 | "agentCloudId": null, 163 | "createdBy": { 164 | "displayName": "Microsoft.VisualStudio.Services.TFS", 165 | "url": "https://app.vssps.visualstudio.com", 166 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 167 | "id": "00000002-0000-8888-8000-000000000000", 168 | "uniqueName": "t@t.t", 169 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 170 | "descriptor": "descr" 171 | }, 172 | "owner": { 173 | "displayName": "Microsoft.VisualStudio.Services.TFS", 174 | "url": "https://app.vssps.visualstudio.com", 175 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/descr" } }, 176 | "id": "00000002-0000-8888-8000-000000000000", 177 | "uniqueName": "t@t.t", 178 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=00000002-0000-8888-8000-000000000000", 179 | "descriptor": "descr" 180 | }, 181 | "id": 11, 182 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 183 | "name": "Hosted Windows Container", 184 | "isHosted": true, 185 | "poolType": "automation", 186 | "size": 2 187 | }, 188 | { 189 | "createdOn": "2018-10-04T12:14:35.33Z", 190 | "autoProvision": true, 191 | "autoSize": true, 192 | "agentCloudId": null, 193 | "createdBy": { 194 | "displayName": "Anton Kuryan", 195 | "url": "https://app.vssps.visualstudio.com", 196 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/sadsa" } }, 197 | "id": "c6b5ba88-76e4-4ecc-9c85-54ceb8a1a908", 198 | "uniqueName": "t@t.t", 199 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=c6b5ba88-76e4-4ecc-9c85-54ceb8a1a908", 200 | "descriptor": "sadsa" 201 | }, 202 | "owner": { 203 | "displayName": "Anton Kuryan", 204 | "url": "https://app.vssps.visualstudio.com", 205 | "_links": { "avatar": { "href": "https://dev.azure.com/testProject/_apis/GraphProfile/MemberAvatars/sadsa" } }, 206 | "id": "c6b5ba88-76e4-4ecc-9c85-54ceb8a1a908", 207 | "uniqueName": "t@t.t", 208 | "imageUrl": "https://dev.azure.com/testProject/_api/_common/identityImage?id=c6b5ba88-76e4-4ecc-9c85-54ceb8a1a908", 209 | "descriptor": "sadsa" 210 | }, 211 | "id": 12, 212 | "scope": "5b7412e3-cf20-4e24-866e-b973c13cfc2d", 213 | "name": "testPool", 214 | "isHosted": false, 215 | "poolType": "automation", 216 | "size": 4 217 | } 218 | ] 219 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Data/TestData/JobRequests/jobs-0-running-1-demands.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 4, 3 | "value": [ 4 | { 5 | "requestId": 12627, 6 | "queueTime": "2018-11-27T13:55:12.8366667Z", 7 | "assignTime": "2018-11-27T13:55:13.3966667Z", 8 | "receiveTime": "2018-11-27T13:55:17.6650079Z", 9 | "finishTime": "2018-11-27T13:56:33.38Z", 10 | "result": "succeeded", 11 | "serviceOwner": "12345678-0000-8888-8000-000000000000", 12 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 13 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 14 | "planType": "Release", 15 | "planId": "12345678-3e0d-462b-aca6-4d33747a5893", 16 | "jobId": "12345678-bf3a-420b-b0b9-8f3410d9099b", 17 | "demands": [ 18 | "Agent.Version -gtVersion 2.120.0", 19 | "Agent.Name -equals someAgent" 20 | ], 21 | "reservedAgent": { 22 | "_links": { 23 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 24 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 25 | }, 26 | "id": 15, 27 | "name": "vstsagent000001", 28 | "version": "2.140.2", 29 | "osDescription": "Microsoft Windows 10.0.14393", 30 | "enabled": true, 31 | "status": "online", 32 | "provisioningState": "Provisioned", 33 | "accessPoint": "VstsAccessMapping" 34 | }, 35 | "definition": { 36 | "_links": { 37 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?definitionId=1" }, 38 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/definitions/1" } 39 | }, 40 | "id": 1, 41 | "name": "Deploy release" 42 | }, 43 | "owner": { 44 | "_links": { 45 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?releaseId=104&_a=release-summary" }, 46 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/releases/104" } 47 | }, 48 | "id": 276, 49 | "name": "Release-104 / Deploy" 50 | }, 51 | "data": { "ParallelismTag": "Private" }, 52 | "poolId": 12, 53 | "agentDelays": [], 54 | "orchestrationId": "0121321312312312024440c3e0d462baca64d33747a5893_e05219abbf3a420bb0b98f3410d9099b" 55 | }, 56 | { 57 | "requestId": 12626, 58 | "queueTime": "2018-11-27T13:46:08.0066667Z", 59 | "assignTime": "2018-11-27T13:46:08.63Z", 60 | "receiveTime": "2018-11-27T13:46:11.040178Z", 61 | "finishTime": "2018-11-27T13:54:39.8329651Z", 62 | "result": "succeeded", 63 | "serviceOwner": "12345678-6065-48ca-87d9-7f5672854ef7", 64 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 65 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 66 | "planType": "Build", 67 | "planId": "12345678-0045-4d74-84db-5fd7dbaf4ed8", 68 | "jobId": "12345678-1c7a-5b21-02e1-d41a394e29c9", 69 | "demands": [ "msbuild", "visualstudio", "vstest", "java", "node.js", "Agent.Version -gtVersion 2.119.1" ], 70 | "reservedAgent": { 71 | "_links": { 72 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 73 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 74 | }, 75 | "id": 15, 76 | "name": "vstsagent000001", 77 | "version": "2.140.2", 78 | "osDescription": "Microsoft Windows 10.0.14393", 79 | "enabled": true, 80 | "status": "online", 81 | "provisioningState": "Provisioned", 82 | "accessPoint": "VstsAccessMapping" 83 | }, 84 | "definition": { 85 | "_links": { 86 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/definition?definitionId=23" }, 87 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Definitions/23" } 88 | }, 89 | "id": 23, 90 | "name": "web app" 91 | }, 92 | "owner": { 93 | "_links": { 94 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/results?buildId=7930" }, 95 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Builds/7930" } 96 | }, 97 | "id": 7930, 98 | "name": "9.0.2.7930" 99 | }, 100 | "data": { "ParallelismTag": "Private" }, 101 | "poolId": 12, 102 | "agentDelays": [], 103 | "orchestrationId": "e3e6358sadsadsae-0045-4d74-84db-5fd7dbaf4ed8_df143ba0-1c7a-5b21-02e1-d41a394e29c9" 104 | }, 105 | { 106 | "requestId": 12620, 107 | "queueTime": "2018-11-27T11:49:41.1233333Z", 108 | "assignTime": "2018-11-27T11:49:41.1533333Z", 109 | "receiveTime": "2018-11-27T11:49:45.1693788Z", 110 | "finishTime": "2018-11-27T11:51:02.7633333Z", 111 | "result": "succeeded", 112 | "serviceOwner": "12345678-0000-8888-8000-000000000000", 113 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 114 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 115 | "planType": "Release", 116 | "planId": "12345678-aea4-4249-a436-199e31ab75c1", 117 | "jobId": "12345678-1788-4b13-81e4-8d7d7f31cfa3", 118 | "demands": [ "Agent.Version -gtVersion 2.120.0" ], 119 | "reservedAgent": { 120 | "_links": { 121 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 122 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 123 | }, 124 | "id": 15, 125 | "name": "vstsagent000001", 126 | "version": "2.140.2", 127 | "osDescription": "Microsoft Windows 10.0.14393", 128 | "enabled": true, 129 | "status": "online", 130 | "provisioningState": "Provisioned", 131 | "accessPoint": "VstsAccessMapping" 132 | }, 133 | "definition": { 134 | "_links": { 135 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?definitionId=1" }, 136 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/definitions/1" } 137 | }, 138 | "id": 1, 139 | "name": "Deploy release" 140 | }, 141 | "owner": { 142 | "_links": { 143 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?releaseId=103&_a=release-summary" }, 144 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/releases/103" } 145 | }, 146 | "id": 270, 147 | "name": "Release-103 / Deploy" 148 | }, 149 | "data": { "ParallelismTag": "Private" }, 150 | "poolId": 12, 151 | "agentDelays": [], 152 | "orchestrationId": "8fc62f92aea44249a4asdasdasdasdasdasd36199e31ab75c1_4cb5148117884b1381e48d7d7f31cfa3" 153 | }, 154 | { 155 | "requestId": 12615, 156 | "queueTime": "2018-11-27T11:34:54.03Z", 157 | "assignTime": "2018-11-27T11:34:54.7Z", 158 | "receiveTime": "2018-11-27T11:34:57.6340669Z", 159 | "finishTime": "2018-11-27T11:38:39.3702451Z", 160 | "result": "succeeded", 161 | "serviceOwner": "12345678-6065-48ca-87d9-7f5672854ef7", 162 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 163 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 164 | "planType": "Build", 165 | "planId": "12345678-4444-450e-bb88-617106e01869", 166 | "jobId": "12345678-1c7a-5b21-02e1-d41a394e29c9", 167 | "demands": [ "msbuild", "visualstudio", "vstest", "java", "Agent.Version -gtVersion 2.119.1" ], 168 | "reservedAgent": { 169 | "_links": { 170 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 171 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 172 | }, 173 | "id": 15, 174 | "name": "vstsagent000001", 175 | "version": "2.140.2", 176 | "osDescription": "Microsoft Windows 10.0.14393", 177 | "enabled": true, 178 | "status": "online", 179 | "provisioningState": "Provisioned", 180 | "accessPoint": "VstsAccessMapping" 181 | }, 182 | "definition": { 183 | "_links": { 184 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/definition?definitionId=22" }, 185 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Definitions/22" } 186 | }, 187 | "id": 22, 188 | "name": "Analyze PR" 189 | }, 190 | "owner": { 191 | "_links": { 192 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/results?buildId=7922" }, 193 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Builds/7922" } 194 | }, 195 | "id": 7922, 196 | "name": "7922" 197 | }, 198 | "data": { "ParallelismTag": "Private" }, 199 | "poolId": 12, 200 | "agentDelays": [], 201 | "orchestrationId": "9a6a0084-4444-450easdasdasdasd-bb88-617106e01869_df143ba0-1c7a-5b21-02e1-d41a394e29c9" 202 | } 203 | ] 204 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Data/TestData/JobRequests/jobs-0-running-no-demands.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 4, 3 | "value": [ 4 | { 5 | "requestId": 12627, 6 | "queueTime": "2018-11-27T13:55:12.8366667Z", 7 | "assignTime": "2018-11-27T13:55:13.3966667Z", 8 | "receiveTime": "2018-11-27T13:55:17.6650079Z", 9 | "finishTime": "2018-11-27T13:56:33.38Z", 10 | "result": "succeeded", 11 | "serviceOwner": "12345678-0000-8888-8000-000000000000", 12 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 13 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 14 | "planType": "Release", 15 | "planId": "12345678-3e0d-462b-aca6-4d33747a5893", 16 | "jobId": "12345678-bf3a-420b-b0b9-8f3410d9099b", 17 | "reservedAgent": { 18 | "_links": { 19 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 20 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 21 | }, 22 | "id": 15, 23 | "name": "vstsagent000001", 24 | "version": "2.140.2", 25 | "osDescription": "Microsoft Windows 10.0.14393", 26 | "enabled": true, 27 | "status": "online", 28 | "provisioningState": "Provisioned", 29 | "accessPoint": "VstsAccessMapping" 30 | }, 31 | "definition": { 32 | "_links": { 33 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?definitionId=1" }, 34 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/definitions/1" } 35 | }, 36 | "id": 1, 37 | "name": "Deploy release" 38 | }, 39 | "owner": { 40 | "_links": { 41 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?releaseId=104&_a=release-summary" }, 42 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/releases/104" } 43 | }, 44 | "id": 276, 45 | "name": "Release-104 / Deploy" 46 | }, 47 | "data": { "ParallelismTag": "Private" }, 48 | "poolId": 12, 49 | "agentDelays": [], 50 | "orchestrationId": "0121321312312312024440c3e0d462baca64d33747a5893_e05219abbf3a420bb0b98f3410d9099b" 51 | }, 52 | { 53 | "requestId": 12626, 54 | "queueTime": "2018-11-27T13:46:08.0066667Z", 55 | "assignTime": "2018-11-27T13:46:08.63Z", 56 | "receiveTime": "2018-11-27T13:46:11.040178Z", 57 | "finishTime": "2018-11-27T13:54:39.8329651Z", 58 | "result": "succeeded", 59 | "serviceOwner": "12345678-6065-48ca-87d9-7f5672854ef7", 60 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 61 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 62 | "planType": "Build", 63 | "planId": "12345678-0045-4d74-84db-5fd7dbaf4ed8", 64 | "jobId": "12345678-1c7a-5b21-02e1-d41a394e29c9", 65 | "reservedAgent": { 66 | "_links": { 67 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 68 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 69 | }, 70 | "id": 15, 71 | "name": "vstsagent000001", 72 | "version": "2.140.2", 73 | "osDescription": "Microsoft Windows 10.0.14393", 74 | "enabled": true, 75 | "status": "online", 76 | "provisioningState": "Provisioned", 77 | "accessPoint": "VstsAccessMapping" 78 | }, 79 | "definition": { 80 | "_links": { 81 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/definition?definitionId=23" }, 82 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Definitions/23" } 83 | }, 84 | "id": 23, 85 | "name": "web app" 86 | }, 87 | "owner": { 88 | "_links": { 89 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/results?buildId=7930" }, 90 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Builds/7930" } 91 | }, 92 | "id": 7930, 93 | "name": "9.0.2.7930" 94 | }, 95 | "data": { "ParallelismTag": "Private" }, 96 | "poolId": 12, 97 | "agentDelays": [], 98 | "orchestrationId": "e3e6358sadsadsae-0045-4d74-84db-5fd7dbaf4ed8_df143ba0-1c7a-5b21-02e1-d41a394e29c9" 99 | }, 100 | { 101 | "requestId": 12620, 102 | "queueTime": "2018-11-27T11:49:41.1233333Z", 103 | "assignTime": "2018-11-27T11:49:41.1533333Z", 104 | "receiveTime": "2018-11-27T11:49:45.1693788Z", 105 | "finishTime": "2018-11-27T11:51:02.7633333Z", 106 | "result": "succeeded", 107 | "serviceOwner": "12345678-0000-8888-8000-000000000000", 108 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 109 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 110 | "planType": "Release", 111 | "planId": "12345678-aea4-4249-a436-199e31ab75c1", 112 | "jobId": "12345678-1788-4b13-81e4-8d7d7f31cfa3", 113 | "reservedAgent": { 114 | "_links": { 115 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 116 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 117 | }, 118 | "id": 15, 119 | "name": "vstsagent000001", 120 | "version": "2.140.2", 121 | "osDescription": "Microsoft Windows 10.0.14393", 122 | "enabled": true, 123 | "status": "online", 124 | "provisioningState": "Provisioned", 125 | "accessPoint": "VstsAccessMapping" 126 | }, 127 | "definition": { 128 | "_links": { 129 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?definitionId=1" }, 130 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/definitions/1" } 131 | }, 132 | "id": 1, 133 | "name": "Deploy release" 134 | }, 135 | "owner": { 136 | "_links": { 137 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?releaseId=103&_a=release-summary" }, 138 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/releases/103" } 139 | }, 140 | "id": 270, 141 | "name": "Release-103 / Deploy" 142 | }, 143 | "data": { "ParallelismTag": "Private" }, 144 | "poolId": 12, 145 | "agentDelays": [], 146 | "orchestrationId": "8fc62f92aea44249a4asdasdasdasdasdasd36199e31ab75c1_4cb5148117884b1381e48d7d7f31cfa3" 147 | }, 148 | { 149 | "requestId": 12615, 150 | "queueTime": "2018-11-27T11:34:54.03Z", 151 | "assignTime": "2018-11-27T11:34:54.7Z", 152 | "receiveTime": "2018-11-27T11:34:57.6340669Z", 153 | "finishTime": "2018-11-27T11:38:39.3702451Z", 154 | "result": "succeeded", 155 | "serviceOwner": "12345678-6065-48ca-87d9-7f5672854ef7", 156 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 157 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 158 | "planType": "Build", 159 | "planId": "12345678-4444-450e-bb88-617106e01869", 160 | "jobId": "12345678-1c7a-5b21-02e1-d41a394e29c9", 161 | "reservedAgent": { 162 | "_links": { 163 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 164 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 165 | }, 166 | "id": 15, 167 | "name": "vstsagent000001", 168 | "version": "2.140.2", 169 | "osDescription": "Microsoft Windows 10.0.14393", 170 | "enabled": true, 171 | "status": "online", 172 | "provisioningState": "Provisioned", 173 | "accessPoint": "VstsAccessMapping" 174 | }, 175 | "definition": { 176 | "_links": { 177 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/definition?definitionId=22" }, 178 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Definitions/22" } 179 | }, 180 | "id": 22, 181 | "name": "Analyze PR" 182 | }, 183 | "owner": { 184 | "_links": { 185 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/results?buildId=7922" }, 186 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Builds/7922" } 187 | }, 188 | "id": 7922, 189 | "name": "7922" 190 | }, 191 | "data": { "ParallelismTag": "Private" }, 192 | "poolId": 12, 193 | "agentDelays": [], 194 | "orchestrationId": "9a6a0084-4444-450easdasdasdasd-bb88-617106e01869_df143ba0-1c7a-5b21-02e1-d41a394e29c9" 195 | } 196 | ] 197 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Data/TestData/JobRequests/jobs-0-running.json: -------------------------------------------------------------------------------- 1 | { 2 | "count": 4, 3 | "value": [ 4 | { 5 | "requestId": 12627, 6 | "queueTime": "2018-11-27T13:55:12.8366667Z", 7 | "assignTime": "2018-11-27T13:55:13.3966667Z", 8 | "receiveTime": "2018-11-27T13:55:17.6650079Z", 9 | "finishTime": "2018-11-27T13:56:33.38Z", 10 | "result": "succeeded", 11 | "serviceOwner": "12345678-0000-8888-8000-000000000000", 12 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 13 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 14 | "planType": "Release", 15 | "planId": "12345678-3e0d-462b-aca6-4d33747a5893", 16 | "jobId": "12345678-bf3a-420b-b0b9-8f3410d9099b", 17 | "demands": [ "Agent.Version -gtVersion 2.120.0" ], 18 | "reservedAgent": { 19 | "_links": { 20 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 21 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 22 | }, 23 | "id": 15, 24 | "name": "vstsagent000001", 25 | "version": "2.140.2", 26 | "osDescription": "Microsoft Windows 10.0.14393", 27 | "enabled": true, 28 | "status": "online", 29 | "provisioningState": "Provisioned", 30 | "accessPoint": "VstsAccessMapping" 31 | }, 32 | "definition": { 33 | "_links": { 34 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?definitionId=1" }, 35 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/definitions/1" } 36 | }, 37 | "id": 1, 38 | "name": "Deploy release" 39 | }, 40 | "owner": { 41 | "_links": { 42 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?releaseId=104&_a=release-summary" }, 43 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/releases/104" } 44 | }, 45 | "id": 276, 46 | "name": "Release-104 / Deploy" 47 | }, 48 | "data": { "ParallelismTag": "Private" }, 49 | "poolId": 12, 50 | "agentDelays": [], 51 | "orchestrationId": "0121321312312312024440c3e0d462baca64d33747a5893_e05219abbf3a420bb0b98f3410d9099b" 52 | }, 53 | { 54 | "requestId": 12626, 55 | "queueTime": "2018-11-27T13:46:08.0066667Z", 56 | "assignTime": "2018-11-27T13:46:08.63Z", 57 | "receiveTime": "2018-11-27T13:46:11.040178Z", 58 | "finishTime": "2018-11-27T13:54:39.8329651Z", 59 | "result": "succeeded", 60 | "serviceOwner": "12345678-6065-48ca-87d9-7f5672854ef7", 61 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 62 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 63 | "planType": "Build", 64 | "planId": "12345678-0045-4d74-84db-5fd7dbaf4ed8", 65 | "jobId": "12345678-1c7a-5b21-02e1-d41a394e29c9", 66 | "demands": [ "msbuild", "visualstudio", "vstest", "java", "node.js", "Agent.Version -gtVersion 2.119.1" ], 67 | "reservedAgent": { 68 | "_links": { 69 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 70 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 71 | }, 72 | "id": 15, 73 | "name": "vstsagent000001", 74 | "version": "2.140.2", 75 | "osDescription": "Microsoft Windows 10.0.14393", 76 | "enabled": true, 77 | "status": "online", 78 | "provisioningState": "Provisioned", 79 | "accessPoint": "VstsAccessMapping" 80 | }, 81 | "definition": { 82 | "_links": { 83 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/definition?definitionId=23" }, 84 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Definitions/23" } 85 | }, 86 | "id": 23, 87 | "name": "web app" 88 | }, 89 | "owner": { 90 | "_links": { 91 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/results?buildId=7930" }, 92 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Builds/7930" } 93 | }, 94 | "id": 7930, 95 | "name": "9.0.2.7930" 96 | }, 97 | "data": { "ParallelismTag": "Private" }, 98 | "poolId": 12, 99 | "agentDelays": [], 100 | "orchestrationId": "e3e6358sadsadsae-0045-4d74-84db-5fd7dbaf4ed8_df143ba0-1c7a-5b21-02e1-d41a394e29c9" 101 | }, 102 | { 103 | "requestId": 12620, 104 | "queueTime": "2018-11-27T11:49:41.1233333Z", 105 | "assignTime": "2018-11-27T11:49:41.1533333Z", 106 | "receiveTime": "2018-11-27T11:49:45.1693788Z", 107 | "finishTime": "2018-11-27T11:51:02.7633333Z", 108 | "result": "succeeded", 109 | "serviceOwner": "12345678-0000-8888-8000-000000000000", 110 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 111 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 112 | "planType": "Release", 113 | "planId": "12345678-aea4-4249-a436-199e31ab75c1", 114 | "jobId": "12345678-1788-4b13-81e4-8d7d7f31cfa3", 115 | "demands": [ "Agent.Version -gtVersion 2.120.0" ], 116 | "reservedAgent": { 117 | "_links": { 118 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 119 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 120 | }, 121 | "id": 15, 122 | "name": "vstsagent000001", 123 | "version": "2.140.2", 124 | "osDescription": "Microsoft Windows 10.0.14393", 125 | "enabled": true, 126 | "status": "online", 127 | "provisioningState": "Provisioned", 128 | "accessPoint": "VstsAccessMapping" 129 | }, 130 | "definition": { 131 | "_links": { 132 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?definitionId=1" }, 133 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/definitions/1" } 134 | }, 135 | "id": 1, 136 | "name": "Deploy release" 137 | }, 138 | "owner": { 139 | "_links": { 140 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_release?releaseId=103&_a=release-summary" }, 141 | "self": { "href": "https://testOrganization.vsrm.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/Release/releases/103" } 142 | }, 143 | "id": 270, 144 | "name": "Release-103 / Deploy" 145 | }, 146 | "data": { "ParallelismTag": "Private" }, 147 | "poolId": 12, 148 | "agentDelays": [], 149 | "orchestrationId": "8fc62f92aea44249a4asdasdasdasdasdasd36199e31ab75c1_4cb5148117884b1381e48d7d7f31cfa3" 150 | }, 151 | { 152 | "requestId": 12615, 153 | "queueTime": "2018-11-27T11:34:54.03Z", 154 | "assignTime": "2018-11-27T11:34:54.7Z", 155 | "receiveTime": "2018-11-27T11:34:57.6340669Z", 156 | "finishTime": "2018-11-27T11:38:39.3702451Z", 157 | "result": "succeeded", 158 | "serviceOwner": "12345678-6065-48ca-87d9-7f5672854ef7", 159 | "hostId": "12345678-cf20-4e24-866e-b973c13cfc2d", 160 | "scopeId": "12345678-5f3c-4cf4-a7b7-43adc4eee405", 161 | "planType": "Build", 162 | "planId": "12345678-4444-450e-bb88-617106e01869", 163 | "jobId": "12345678-1c7a-5b21-02e1-d41a394e29c9", 164 | "demands": [ "msbuild", "visualstudio", "vstest", "java", "Agent.Version -gtVersion 2.119.1" ], 165 | "reservedAgent": { 166 | "_links": { 167 | "self": { "href": "https://dev.azure.com/testOrganization/_apis/distributedtask/pools/12/agents/15" }, 168 | "web": { "href": "https://dev.azure.com/testOrganization/_admin/_AgentPool#_a=agents&poolId=12&agentId=15" } 169 | }, 170 | "id": 15, 171 | "name": "vstsagent000001", 172 | "version": "2.140.2", 173 | "osDescription": "Microsoft Windows 10.0.14393", 174 | "enabled": true, 175 | "status": "online", 176 | "provisioningState": "Provisioned", 177 | "accessPoint": "VstsAccessMapping" 178 | }, 179 | "definition": { 180 | "_links": { 181 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/definition?definitionId=22" }, 182 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Definitions/22" } 183 | }, 184 | "id": 22, 185 | "name": "Analyze PR" 186 | }, 187 | "owner": { 188 | "_links": { 189 | "web": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_build/results?buildId=7922" }, 190 | "self": { "href": "https://testOrganization.visualstudio.com/12345678-5f3c-4cf4-a7b7-43adc4eee405/_apis/build/Builds/7922" } 191 | }, 192 | "id": 7922, 193 | "name": "7922" 194 | }, 195 | "data": { "ParallelismTag": "Private" }, 196 | "poolId": 12, 197 | "agentDelays": [], 198 | "orchestrationId": "9a6a0084-4444-450easdasdasdasd-bb88-617106e01869_df143ba0-1c7a-5b21-02e1-d41a394e29c9" 199 | } 200 | ] 201 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Data/TestsConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureDevOps.Operations.Tests.Data 2 | { 3 | public static class TestsConstants 4 | { 5 | internal const string TestOrganizationName = "testOrganization"; 6 | internal const string TestToken = "testToken"; 7 | internal const string TestPoolName = "testPool"; 8 | 9 | internal const int TestPoolId = 12; 10 | internal const int AllAgentsCount = 4; 11 | internal const int OnlineAgentsCount = 3; 12 | 13 | internal const string FileNotExistPointer = @"\fileNotExists"; 14 | /// 15 | /// Points to JSON with 1 running job 16 | /// 17 | internal const string Json1JobIsRunning = @"..\..\Data\TestData\JobRequests\jobs-1-running.json"; 18 | /// 19 | /// Point to JSON with 3 running jobs 20 | /// 21 | internal const string Json3JobIsRunning = @"..\..\Data\TestData\JobRequests\jobs-3-running.json"; 22 | /// 23 | /// Points to JSON with 3 waiting jobs and 2 of them have agent name demand 24 | /// 25 | internal const string Json_3_jobs_2_demands = @"..\..\Data\TestData\JobRequests\jobs-3-running-2-demands.json"; 26 | /// 27 | /// Points to JSON with 0 waiting jobs, where 1 have defined demand for an agent name 28 | /// 29 | internal const string Json_0_jobs_1_demands = @"..\..\Data\TestData\JobRequests\jobs-0-running-1-demands.json"; 30 | 31 | internal const string Json_0_jobs_NO_demands = @"..\..\Data\TestData\JobRequests\jobs-0-running-no-demands.json"; 32 | } 33 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Helpers/DataPreparationTests.cs: -------------------------------------------------------------------------------- 1 | using AzureDevOps.Operations.Helpers; 2 | using AzureDevOps.Operations.Models; 3 | using AzureDevOps.Operations.Tests.Data; 4 | using AzureDevOps.Operations.Tests.TestsHelpers; 5 | using Microsoft.Azure.Management.Compute.Fluent; 6 | using NUnit.Framework; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace AzureDevOps.Operations.Tests.Helpers 11 | { 12 | public static class DataPreparationTests 13 | { 14 | /// 15 | /// Test 16 | /// 17 | /// 18 | /// 19 | /// 20 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json1JobIsRunning, 0)] 21 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json3JobIsRunning, 0)] 22 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json_3_jobs_2_demands, 2)] 23 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json_0_jobs_1_demands, 0)] 24 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json_0_jobs_NO_demands, 0)] 25 | public static void TestRetrievalOfDemandedAgentsNames(int agentPoolId, string jsonData, int expectedCountOfDemandCollection) 26 | { 27 | var scheduledJobs = HelperMethods.GetSimulatedJobRequests(agentPoolId, jsonData); 28 | Assert.AreEqual(expectedCountOfDemandCollection, DataPreparation.CollectDemandedAgentNames(scheduledJobs).Length); 29 | } 30 | 31 | /// 32 | /// Tests Virtual Machines allocation 33 | /// 34 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json1JobIsRunning, 1, 1)] 35 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json3JobIsRunning, 3, 3)] 36 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json_3_jobs_2_demands, 3, 3)] 37 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json_0_jobs_1_demands, 0, 0)] 38 | [TestCase(TestsConstants.TestPoolId, TestsConstants.Json_0_jobs_NO_demands, 0, 0)] 39 | public static void TestVirtualMachinesAllocationMethod(int agentPoolId, string jsonData, int amountOfAgentsToAllocate, int expectedAmountOfAgents) 40 | { 41 | var vmScaleSetData = GenerateTestData().ToArray(); 42 | var scheduledJobs = HelperMethods.GetSimulatedJobRequests(agentPoolId, jsonData); 43 | var vmsToStart = 44 | DataPreparation.GetVmsForAllocation(scheduledJobs, vmScaleSetData, amountOfAgentsToAllocate); 45 | Assert.AreEqual(expectedAmountOfAgents, vmsToStart.Count()); 46 | } 47 | /// 48 | /// Tests specific names are present in VMs collection 49 | /// 50 | [Test] 51 | public static void TestVirtualMachinesSpecificNames() 52 | { 53 | var vmScaleSetData = GenerateTestData().ToArray(); 54 | var scheduledJobs = HelperMethods.GetSimulatedJobRequests(TestsConstants.TestPoolId, TestsConstants.Json_3_jobs_2_demands); 55 | var vmsToStart = 56 | DataPreparation.GetVmsForAllocation(scheduledJobs, vmScaleSetData, 3).ToArray(); 57 | Assert.AreEqual(3, vmsToStart.Count()); 58 | //ensures that demanded agents are selected for upscaling 59 | Assert.IsTrue(vmsToStart.Any(vm => vm.VmName.Equals("Agent"))); 60 | Assert.IsTrue(vmsToStart.Any(vm => vm.VmName.Equals("Agent1"))); 61 | 62 | //check, that there is different objects in the end 63 | Assert.AreEqual(vmsToStart.Count(), vmsToStart.Distinct().Count()); 64 | } 65 | 66 | private static IEnumerable GenerateTestData() 67 | { 68 | var testArray = new ScaleSetVirtualMachineStripped[3]; 69 | var testValid = new ScaleSetVirtualMachineStripped 70 | { 71 | VmName = "Agent", 72 | VmInstanceId = "205", 73 | VmInstanceState = PowerState.Deallocated 74 | }; 75 | 76 | testArray[0] = testValid; 77 | testValid = new ScaleSetVirtualMachineStripped 78 | { 79 | VmName = "Agent1", 80 | VmInstanceId = "210", 81 | VmInstanceState = PowerState.Deallocated 82 | }; 83 | 84 | testArray[1] = testValid; 85 | testValid = new ScaleSetVirtualMachineStripped 86 | { 87 | VmName = "Agent2", 88 | VmInstanceId = "215", 89 | VmInstanceState = PowerState.Deallocated 90 | }; 91 | 92 | testArray[2] = testValid; 93 | return HelperMethods.GetTestData(10, testArray); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Helpers/DynamicPropsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | using AzureDevOps.Operations.Classes; 3 | using AzureDevOps.Operations.Helpers; 4 | using AzureDevOps.Operations.Helpers.Mockable; 5 | using AzureDevOps.Operations.Tests.Classes; 6 | using NUnit.Framework; 7 | 8 | namespace AzureDevOps.Operations.Tests.Helpers 9 | { 10 | public class DynamicPropsTests 11 | { 12 | [Test] 13 | public void SettingsIsNotDefined() 14 | { 15 | ConfigurationManager.AppSettings[Constants.BusinessHoursRangeSettingName] = ""; 16 | ConfigurationManager.AppSettings[Constants.BusinessHoursDaysSettingName] = ""; 17 | ConfigurationManager.AppSettings[Constants.BusinessHoursAgentsAmountSettingName] = ""; 18 | var dynamicProp = new DynamicProps(); 19 | Assert.IsFalse(dynamicProp.WeAreInsideBusinessTime); 20 | } 21 | 22 | //Monday 23 | [TestCase("10-Dec-2018 09:15", false)] 24 | [TestCase("10-Dec-2018 10:15", true)] 25 | [TestCase("10-Dec-2018 12:15", true)] 26 | [TestCase("10-Dec-2018 18:15", false)] 27 | //Tuesday 28 | [TestCase("11-Dec-2018 09:15", false)] 29 | [TestCase("11-Dec-2018 10:15", true)] 30 | [TestCase("11-Dec-2018 12:15", true)] 31 | [TestCase("11-Dec-2018 18:15", false)] 32 | //Wednesday 33 | [TestCase("12-Dec-2018 09:15", false)] 34 | [TestCase("12-Dec-2018 10:15", true)] 35 | [TestCase("12-Dec-2018 12:15", true)] 36 | [TestCase("12-Dec-2018 18:15", false)] 37 | //Thursday 38 | [TestCase("13-Dec-2018 09:15", false)] 39 | [TestCase("13-Dec-2018 10:15", true)] 40 | [TestCase("13-Dec-2018 12:15", true)] 41 | [TestCase("13-Dec-2018 18:15", false)] 42 | //Friday 43 | [TestCase("14-Dec-2018 09:15", false)] 44 | [TestCase("14-Dec-2018 10:15", true)] 45 | [TestCase("14-Dec-2018 12:15", true)] 46 | [TestCase("14-Dec-2018 18:15", false)] 47 | //Saturday 48 | [TestCase("15-Dec-2018 09:15", false)] 49 | [TestCase("15-Dec-2018 10:15", false)] 50 | [TestCase("15-Dec-2018 12:15", false)] 51 | [TestCase("15-Dec-2018 18:15", false)] 52 | //Sunday 53 | [TestCase("16-Dec-2018 09:15", false)] 54 | [TestCase("16-Dec-2018 10:15", false)] 55 | [TestCase("16-Dec-2018 12:15", false)] 56 | [TestCase("16-Dec-2018 18:15", false)] 57 | public void CheckingValuesDefinitions(string testDateTime, bool expectedResult) 58 | { 59 | TestInitilizers.InitAppSettingsForBusinessTimesTests(); 60 | var dynamicProp = new DynamicProps(); 61 | Clock.TestApi.Now = () => TestInitilizers.ParseDateTimeForTest(testDateTime); 62 | 63 | Assert.AreEqual(dynamicProp.WeAreInsideBusinessTime, expectedResult); 64 | 65 | Clock.TestApi.Reset(); 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Helpers/PropertiesTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using AzureDevOps.Operations.Classes; 4 | using AzureDevOps.Operations.Helpers; 5 | using NUnit.Framework; 6 | 7 | namespace AzureDevOps.Operations.Tests.Helpers 8 | { 9 | public static class PropertiesTests 10 | { 11 | [TestCase("Monday-Friday", DayOfWeek.Monday, DayOfWeek.Friday)] 12 | [TestCase("Monday-Monday", DayOfWeek.Monday, DayOfWeek.Monday)] 13 | [TestCase("Monday-Tuesday", DayOfWeek.Monday, DayOfWeek.Tuesday)] 14 | [TestCase("Monday-Wednesday", DayOfWeek.Monday, DayOfWeek.Wednesday)] 15 | [TestCase("Monday-Thursday", DayOfWeek.Monday, DayOfWeek.Thursday)] 16 | [TestCase("Monday-Saturday", DayOfWeek.Monday, DayOfWeek.Saturday)] 17 | [TestCase("Monday-Sunday", DayOfWeek.Monday, DayOfWeek.Sunday)] 18 | public static void BusinessDaysParserTests(string testString, DayOfWeek startingDayExpected, 19 | DayOfWeek endingDayExpected) 20 | { 21 | ConfigurationManager.AppSettings[Constants.BusinessHoursDaysSettingName] = testString; 22 | Assert.IsTrue(Properties.BusinessDaysStartingDay == startingDayExpected); 23 | Assert.IsTrue(Properties.BusinessDaysLastDay == endingDayExpected); 24 | } 25 | 26 | [TestCase("10-17", 10, 17)] 27 | [TestCase("22-23", 22, 23)] 28 | public static void BusinessHoursParserTests(string testString, int expectedStartingHour, int expectedEndHour) 29 | { 30 | ConfigurationManager.AppSettings[Constants.BusinessHoursRangeSettingName] = testString; 31 | Assert.AreEqual(Properties.BussinesDayStartHour, expectedStartingHour); 32 | Assert.AreEqual(Properties.BussinesDayEndHour, expectedEndHour); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("AzureDevOps.Operations.Tests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("AzureDevOps.Operations.Tests")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("98e36565-ac08-4ca8-8193-8628a1403ee8")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/TestsHelpers/HelperMethods.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using AzureDevOps.Operations.Models; 3 | using AzureDevOps.Operations.Tests.Classes; 4 | using AzureDevOps.Operations.Tests.Data; 5 | using Microsoft.Azure.Management.Compute.Fluent; 6 | 7 | namespace AzureDevOps.Operations.Tests.TestsHelpers 8 | { 9 | /// 10 | /// This class contains different helper methods for tests 11 | /// 12 | public static class HelperMethods 13 | { 14 | public static JobRequest[] GetSimulatedJobRequests(int poolId = TestsConstants.TestPoolId, 15 | string jsonData = TestsConstants.Json1JobIsRunning) 16 | { 17 | var dataRetriever = RetrieveTests.CreateRetriever(jsonData); 18 | return dataRetriever.GetRunningJobs(poolId); 19 | } 20 | 21 | /// 22 | /// Generates stripped VMSS list to work with; allows to generate to amount which is needed and add custom data to the collection 23 | /// 24 | /// 25 | /// 26 | /// 27 | internal static List GetTestData(int testListSize, ScaleSetVirtualMachineStripped[] addedData = null) 28 | { 29 | var vmScaleSetData = new List(); 30 | if (addedData != null) 31 | { 32 | vmScaleSetData.AddRange(addedData); 33 | } 34 | 35 | for (var counter = 0; counter < testListSize; counter++) 36 | { 37 | vmScaleSetData.Add(new ScaleSetVirtualMachineStripped 38 | { 39 | VmName = $"vm{counter}", 40 | VmInstanceId = $"{counter}", 41 | VmInstanceState = PowerState.Running 42 | }); 43 | } 44 | 45 | return vmScaleSetData; 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /autoscalingApp/AgentsMonitor/Tests/AzureDevOps.Operations.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /autoscalingApp/README.md: -------------------------------------------------------------------------------- 1 | # Purpose 2 | 3 | This folder holds Azure Web App job for monitoring and autoscaling Azure Agents Virtual Machines Scale Set. Usage and purpose is described [here](https://dobryak.org/self-hosted-agents-at-azure-devops-a-little-cost-saving-trick/) 4 | 5 | # Known bugs 6 | 7 | 1. Since VM deprovisioning in VMSS is not immediate process - when VM are already deprovisioning, agent service is still running and report to the queue as being online and could receive job to be performed. This job, sadly, will fail. 8 | 9 | ## Implementation 10 | 11 | Azure Web app job checks Azure DevOps account specified and monitors queue. As soon as there is waiting job - new VM in VMSS shall be started. 12 | If queue is empty and we have more than 1 VM running in VMSS - extra VMs shall be stopped. If there is 1 VM in VMSS and it is running for 1 hour without a jobs - it shall be deprovisioned as well. 13 | 14 | Job shall check maximum amount of possible private agents in account and ensure that VMSS have this amount of VMs provisioned (faster startup times, though we'll pay extra for disk drives). 15 | 16 | Job shall log start/stop attempts in external storage for future ML model training. 17 | 18 | Job is written on C#, as it is easier than Powershell for me :) 19 | 20 | ## Settings 21 | 22 | Currently, settings are set in App.config (or one can use ARM template in arm-template folder and deploy them as appsettings in Azure Web app). 23 | 24 | ```Agents_PoolName``` - specify pool name to monitor here (or set ```Agents_PoolId```) 25 | 26 | ```Agents_PoolId``` - if ```Agents_PoolName``` is not specified, set ID here 27 | 28 | ```Azure_DevOpsInstance``` - specify you Azure Devops instance name (first segment after hostname https://dev.azure.com/). Example: ```https://dev.azure.com/testusername/``` - here your instance name is ```testusername``` 29 | 30 | ```Azure_DevOpsPAT``` - personal access token for Azure DevOps. See https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=vsts 31 | 32 | ```Azure_ServicePrincipleClientId``` - Azure Service Principle ID (must have at least Contribute on VMSS which hosts agents) 33 | 34 | ```Azure_ServicePrincipleClientSecret``` - Azure Service Principle Client Secret 35 | 36 | ```Azure_ServicePrincipleTenantId``` - Azure Service Principle Tenant ID 37 | 38 | ```Azure_SubscriptionId``` - Azure Subscription, where VMSS with agents is hosted 39 | 40 | ```Azure_VMSS_resourceGroupName``` - resource group name, which hosts VMSS 41 | 42 | ```Azure_VMSS_Name``` - Azure VMSS name 43 | 44 | ```DryRunExecution``` - if set to true, then now actual (de)provision actions will be taken against VMSS; ```Azure_Storage_ActionsTracking_TableName``` will be appended with ```DryRun``` to not mangle with actual run data 45 | 46 | ```Azure_Storage_ConnectionString``` - Azure Storage connection string to store runtime data for monitoring and possible ML usage (who knows). If empty - data will not be stored 47 | 48 | ```Azure_Storage_ActionsTracking_TableName``` - table name to store runtime data in. If empty and connection string ```Azure_Storage_ConnectionString``` specified - then it will default to ```DefaultTrackingTable```. 49 | 50 | ```BusinessHours_range``` - if here business hours is specified, then at this time (in timezone of web app) minimal amount of agents, specified in ```BusinessHours_agents``` will be kept online. 51 | 52 | ```BusinessHours_days``` - on days specified here in between hours specified at ```BusinessHours_range``` minimal amount of agents, specified in ```BusinessHours_agents``` will be kept online. Values must be formalized and only range accepted (first day, followed by dash, and last day). Example: ```Monday-Friday``` 53 | 54 | ```BusinessHours_agents``` - on days specified in ```BusinessHours_days``` in between hours specified at ```BusinessHours_range``` minimal amount of agents count will be kept online. 55 | 56 | As will all Web Jobs - you need to specify connection strings to Azure Storage (they are used behind the scenes for logging and time tracking). 57 | 58 | They must be specified in following connection strings: ```AzureWebJobsDashboard``` and ```AzureWebJobsStorage```. 59 | 60 | ## Deployment 61 | 62 | After building of ```~\AgentsMonitor\AgentsMonitor.sln``` all required binaries will be outputted to ```~\WebJob\AutoScaler``` (paths are given relative to current dir ```autoscalingApp```). To publish them as Azure WebJob - just deliver them to web app, deployed with [ARM template in this repository](./arm-template/) to the path ```wwwroot/App_Data/jobs/continuous/AutoScaler``` and runtime of web app will take care of the rest. Also, this webjob could be executed locally, given you have provided it with access to Azure Storage (could be emulated one via Azure Storage Emulator), as it is a requirement for timers. 63 | 64 | ## CI/CD setup 65 | 66 | [Prepare build](../docs/autoscaler-app-build.md) 67 | 68 | [Prepare release](../docs/autoscaler-app-release.md) -------------------------------------------------------------------------------- /autoscalingApp/arm-template/README.md: -------------------------------------------------------------------------------- 1 | # Deploys web app with required settings defined 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ## Description 11 | This template allows you to deploy an app service plan and a basic Windows web app with storage account, required to execute autoscaler app. Deploys to shared web app to minify spending - so, you'll need either to define bigger web app or find a way to ping it once in 5 minutes 12 | 13 | ## Example configuration for deployment 14 | 15 | I did some preparations for publishing via Visual Studio or MsDeploy, but, for my deployments - I'll use just build in Release, which will output all required files to a folder ~\autoscalingApp\WebJob\ - then, I'll use simple MsDeploy command to deliver it on web app: 16 | 17 | ```cmd 18 | "$(msdeploy.Path)" -allowUntrusted="True" -enableRule:DoNotDeleteRule -verb:sync -source:contentPath="~\autoscalingApp\WebJob\" -dest:contentPath="$(deploy.iisSiteName)/App_Data/jobs/continuous/",computerName="https://$(deploy.iisSiteName).scm.azurewebsites.net:443/msdeploy.axd?site=$(deploy.iisSiteName)",username="$(user)",password="%userPwd%",authType="Basic" 19 | ``` -------------------------------------------------------------------------------- /autoscalingApp/arm-template/azuredeploy.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "environmentTag": { "value": "production"}, 3 | "platformTag": { "value": "devops-automation"}, 4 | "appTag": { "value": "azure-devops"}, 5 | "storageAccountName": { "value": "genuniquestor"}, 6 | "webAppName": { "value": "GEN-UNIQUE"}, 7 | "webAppServicePlanName": { "value": "GEN-UNIQUE"}, 8 | "appInsightsInstrumentationKey": { "value": "GRAB-YOUR-OWN"}, 9 | "AzureDevOpsPoolName": { "value": "GRAB-YOUR-OWN-OR-DEFINE-POOL-ID"}, 10 | "AzureDevOpsPoolId": {"value": 0}, 11 | "AzureDevOpsInstanceName": {"value": "SET-YOURS-HERE"}, 12 | "AzureDevOpsPAT": {"value": "SET-YOURS-HERE"}, 13 | "AzureServicePrincipleId": {"value": "SET-YOURS-HERE"}, 14 | "AzureServicePrincipleSecret": {"value": "SET-YOURS-HERE"}, 15 | "AzureServicePrincipleTenant": {"value": "SET-YOURS-HERE"}, 16 | "AzureSubscriptionId": {"value": "SET-YOURS-HERE"}, 17 | "AzureVMSSResourceGroup": {"value": "SET-YOURS-HERE"}, 18 | "AzureVMSSName": {"value": "SET-YOURS-HERE"}, 19 | "IsDryRun": {"value": true}, 20 | "businessHours": {"value": "10-17"}, 21 | "businessDays": {"value": "Monday-Friday"}, 22 | "businessAgentsAmount": {"value": "2"} 23 | } -------------------------------------------------------------------------------- /builds/build.yaml: -------------------------------------------------------------------------------- 1 | queue: 2 | name: AzureHostedAgents 3 | timeoutInMinutes: 600 4 | 5 | steps: 6 | - task: AzurePowerShell@2 7 | inputs: 8 | azureConnectionType: 'ConnectedServiceNameARM' 9 | azureSubscription: '$(AzureConnectionName)' 10 | ScriptPath: 'Build.ps1' 11 | ScriptArguments: '-Location "$(Location)" -PackerFile "$(PackerFile)" -ClientId "$(ClientId)" -ClientSecret "$(ClientSecret)" -TenantId "$(TenantId)" -SubscriptionId "$(SubscriptionId)" -ObjectId "$(ObjectId)" -ManagedImageResourceGroupName "$(ManagedImageResourceGroupName)" -ManagedImageName "$(ManagedImageName)-$(Build.BuildNumber)" -InstallPrerequisites:$(InstallPrerequisites) -EnforceAzureRm:$(EnforceAzureRm) -abortPackerOnError:$(abortPackerOnError)' 12 | azurePowerShellVersion: 'LatestVersion' -------------------------------------------------------------------------------- /builds/clean.yaml: -------------------------------------------------------------------------------- 1 | queue: 2 | name: Hosted VS2017 3 | demands: azureps 4 | 5 | steps: 6 | - task: AzurePowerShell@2 7 | inputs: 8 | azureConnectionType: 'ConnectedServiceNameARM' 9 | azureSubscription: 'Azure Connection' 10 | ScriptPath: 'Clean.ps1' 11 | ScriptArguments: '-RemovePackerResourceGroups:$(RemovePackerResourceGroups) -RemoveManagedImages:$(RemoveManagedImages) -RemoveAgentPoolResourceGroup:$(RemoveAgentPoolResourceGroup) -ManagedImageName "$(ManagedImageName)" -ManagedImageResourceGroupName "$(ManagedImageResourceGroupName)" -AgentPoolResourceGroup "$(AgentPoolResourceGroup)"' 12 | azurePowerShellVersion: 'LatestVersion' 13 | -------------------------------------------------------------------------------- /config/small-image.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "client_id": "{{env `ARM_CLIENT_ID`}}", 4 | "client_secret": "{{env `ARM_CLIENT_SECRET`}}", 5 | "subscription_id": "{{env `ARM_SUBSCRIPTION_ID`}}", 6 | "tenant_id": "{{env `ARM_TENANT_ID`}}", 7 | "object_id": "{{env `ARM_OBJECT_ID`}}", 8 | "location": "{{env `ARM_RESOURCE_LOCATION`}}", 9 | "managed_image_resource_group_name": "{{env `ARM_IMAGE_RESOURCE_GROUP_NAME`}}", 10 | "managed_image_name": "{{env `ARM_IMAGE_NAME`}}" 11 | }, 12 | "builders": [{ 13 | "type": "azure-arm", 14 | "client_id": "{{user `client_id`}}", 15 | "client_secret": "{{user `client_secret`}}", 16 | "subscription_id": "{{user `subscription_id`}}", 17 | "object_id": "{{user `object_id`}}", 18 | "tenant_id": "{{user `tenant_id`}}", 19 | "location": "{{user `location`}}", 20 | "vm_size": "{{user `vm_size`}}", 21 | "managed_image_resource_group_name": "{{user `managed_image_resource_group_name`}}", 22 | "managed_image_name": "{{user `managed_image_name`}}", 23 | "os_type": "Windows", 24 | "image_publisher": "MicrosoftWindowsServer", 25 | "image_offer": "WindowsServer", 26 | "image_sku": "2016-Datacenter", 27 | "communicator": "winrm", 28 | "winrm_use_ssl": "true", 29 | "winrm_insecure": "true", 30 | "winrm_timeout": "4h", 31 | "winrm_username": "packer" 32 | }], 33 | "provisioners": [{ 34 | "type": "powershell", 35 | "inline": [ 36 | "if( Test-Path $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml ){ rm $Env:SystemRoot\\windows\\system32\\Sysprep\\unattend.xml -Force}", 37 | "& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit", 38 | "while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10 } else { break } }" 39 | ] 40 | }] 41 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Content 2 | 3 | - [Configure image refresh build](./image-Refresh-Build.md) 4 | 5 | - [Configure agent release](./deploy-Agent.md) 6 | 7 | - [Build autoscaler app for release](./autoscaler-app-build.md) 8 | 9 | - [Release autoscaler app](./autoscaler-app-release.md) -------------------------------------------------------------------------------- /docs/autoscaler-app-build.md: -------------------------------------------------------------------------------- 1 | # How to 2 | 3 | This build is not using modern yaml pipeline. Eventually, I will code it down to yaml. Also, there is a reason why I do not code it down to yaml - we are using SonarQube to perform static analysis. 4 | 5 | So, here is steps: 6 | 7 | 1. `Nuget` Restore; path to solution is stored at var `$(slnFilePath)`; packages are residing on Nuget.org 8 | 9 | 1. `Visual Studio Build` to build solution at `$(slnFilePath)` with platform set to `$(BuildPlatform)` and Configuration set to `$(BuildConfiguration)` 10 | 11 | 1. `Visual Studio Test` with default settings 12 | 13 | 1. `Copy Files` from `$(base.App.Folder.Name)\WebJob` with content `**` to `$(Build.ArtifactStagingDirectory)\WebJob\$(webJob.autoscaler.path)` 14 | 15 | 1. `Archive Files` from `$(Build.ArtifactStagingDirectory)\WebJob` to zip `$(Build.ArtifactStagingDirectory)/autoScaler.zip` 16 | 17 | 1. `Publish Build Artifacts` from `$(Build.ArtifactStagingDirectory)/autoScaler.zip` with name `AutoScalerWebJob` -------------------------------------------------------------------------------- /docs/autoscaler-app-release.md: -------------------------------------------------------------------------------- 1 | # How to 2 | 3 | This release gets it's artifacts from [autoscaler app build](./autoscaler-app-build.md) and contains 1 job with 3 steps: 4 | 5 | 1. `Azure PowerShell` which invokes inline script to stop web job: `Invoke-AzureRmResourceAction -ResourceGroupName $(webjob.rg.name) -ResourceType Microsoft.Web/sites/continuouswebjobs -ResourceName $(webjob.webapp.name)/$(webJob.autoscaler.name) -Action Stop -Force -ApiVersion 2018-02-01` 6 | 7 | 1. `Azure App Service Deploy` which deploys artifact `$(System.DefaultWorkingDirectory)/_Azure DevOps Webjobs/AutoScalerWebJob/autoScaler.zip` to web app 8 | 9 | 1. `Azure PowerShell` which invokes inline script to start web job: `Invoke-AzureRmResourceAction -ResourceGroupName $(webjob.rg.name) -ResourceType Microsoft.Web/sites/continuouswebjobs -ResourceName $(webjob.webapp.name)/$(webJob.autoscaler.name) -Action Start -Force -ApiVersion 2018-02-01` 10 | 11 | This release have only shared variables defined under name `Azure Web jobs` (they are reused at [agent release](./deploy-Agent.md)) and contains following vars: 12 | 13 | - `webJob.autoscaler.name` - name of web job 14 | 15 | - `webjob.rg.name` - resource group, where web app, hosting web job resides 16 | 17 | - `webjob.webapp.name` - web app name, where web job should be deployed -------------------------------------------------------------------------------- /docs/deploy-Agent.md: -------------------------------------------------------------------------------- 1 | # How to 2 | 3 | Shall be executed at Microsoft hosted agents, at least `Hosted VS2017`. Also, it is tied up with autoscaler job as well. 4 | 5 | I create it as release pipeline to ease deploynig previous releases, in case current one goes rogue. It have 2 artifacts to be used: 6 | 7 | 1. Agent image build (build, [described here](.\image-Refresh-Build.md)) - image name retrieved from it. 8 | 9 | 1. This repository, as it holds `Release.ps1` used for release, `RemoveAgents.ps1` used to remove agents from pool and `Manage.ps1` used to stop VMs in VMSS. Maybe it is wiser to publish it as build artifact as well... 10 | 11 | ## Job configuration 12 | 13 | This release have one job, running at `Hosted VS2017` pool with following steps: 14 | 15 | 1. `Azure PowerShell` to stop autoscaler web job, which executes following inline script: `Invoke-AzureRmResourceAction -ResourceGroupName $(webjob.rg.name) -ResourceType Microsoft.Web/sites/continuouswebjobs -ResourceName $(webjob.webapp.name)/$(webJob.autoscaler.name) -Action Stop -Force -ApiVersion 2018-02-01` 16 | 17 | 1. `Azure PowerShell` to stop VMs in VMSS, which executes following script: `$(System.DefaultWorkingDirectory)/repositoryArtifactNameGoesHere/Manage.ps1` with arguments `-resourcesBaseName $(resourcesBaseName) -Action Stop` 18 | 19 | 1. `Powershell` to cleanup agent pool at Azure DevOps, which executes `$(System.DefaultWorkingDirectory)/repositoryArtifactNameGoesHere/RemoveAgents.ps1` with arguments `-VSTSToken $(VSTSToken) -VSTSUrl $(VSTSUrl) -agentPoolPattern $(VMName)` 20 | 21 | 1. `Azure PowerShell` to actually create new VMSS from image built previously - execute script `$(System.DefaultWorkingDirectory)/repositoryArtifactNameGoesHere/Release.ps1` with params `-VMUser $(VMUser) -VMUserPassword $(VMUserPassword) -VMName $(VMName) -ManagedImageResourceGroupName $(ManagedImageResourceGroupName) -ManagedImageName $(vmssImageName) -resourcesBaseName $(resourcesBaseName) -VSTSToken $(VSTSToken) -VSTSUrl $(VSTSUrl) -pipRg $(pipRg) -vstsPoolName $(vstsPoolName) -vmssCapacity $(vmssCapacity) -vmssSkuName $(vmssSkuName) -vstsAgentPackageUri $(vstsAgentLink) -vmssDiskStorageAccount $(azureDiskType) -attachDataDisk $(attachDataDiskParam) -allowedIps "$(allowedIps)" -deployToExistingVnet $(deployToExistingVnet) -subnetName "$(subnetName)" -vnetName "$(vnetName)" -vnetResourceGroupName "$(vnetResourceGroupName)"` 22 | 23 | 1. `Azure PowerShell` to start autoscaler webjob, which launched inline script: `Invoke-AzureRmResourceAction -ResourceGroupName $(webjob.rg.name) -ResourceType Microsoft.Web/sites/continuouswebjobs -ResourceName $(webjob.webapp.name)/$(webJob.autoscaler.name) -Action Start -Force -ApiVersion 2018-02-01` 24 | 25 | ## Variables configuration 26 | 27 | I add following variables at this release (see [..\README.md] for description): 28 | 29 | - `allowedIps`, `attachDataDiskParam`, `azureDiskType`, `vmssImageName` (I set it equal to `$(ManagedImageName)-$(Release.Artifacts.agentImageBuild.BuildNumber)`, where `agentImageBuild` is name of my artifact from agent image build), `vstsAgentLink` 30 | 31 | I add shared variables `Agent Image data` from [.\image-Refresh-Build.md] and `Azure Web jobs` from [.\autoscaler-app-release.md] 32 | 33 | That what's needed to be done to deploy new agents. 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /docs/image-Refresh-Build.md: -------------------------------------------------------------------------------- 1 | # How to 2 | 3 | Shall be executed on your own hosted agent at pool, specified in `vstsPoolName` var, as it is lengthy process. 4 | 5 | 1. Create New build pipeline based on YAML configuration; point it for yaml at [/builds/build.yaml](../builds/build.yaml) 6 | 7 | 1. At variables, create following variables to be used by this build ([variables description](../README.md#buildps1-parameters)): `abortPackerOnError`, `EnforceAzureRm`, `InstallPrerequisites` (those variables are exclusive for a build) 8 | 9 | 1. Create shared variable group (I call it `Agent Image data`) and add there following vars: `AzureConnectionName` (here I store connection name for Azure, which is set up at `Service Connections` of my Azure DevOps project), `ClientId`, `ClientSecret`, `Location`, `ManagedImageName`, `ManagedImageResourceGroupName`, `ObjectId`, `Packerfile`, `SubscriptionId`, `TenantId`, `VMName`, `VMUser`, `VMUserPassword`, `VSTSToken`, `VSTSUrl`, `pipRg` (I put there value `$(ManagedImageResourceGroupName)` to ensure that it is not recreated on each deployment), `resourcesBaseName`, `vmssCapacity`, `vmssSkuName`, `vstsPoolName`, `deployToExistingVnet`, `subnetName`, `vnetName`, `vnetResourceGroupName`. Part of those variable are used by release as well. 10 | 11 | ## You are good to go :) -------------------------------------------------------------------------------- /functions/helpers.psm1: -------------------------------------------------------------------------------- 1 | function SetCustomTagOnResource { 2 | param ( 3 | $resourceId, 4 | #Get-AzureRmResource does not fetches us resource name :( 5 | $resourceName 6 | ) 7 | 8 | process { 9 | Write-Verbose "Starting tags settings for resource $resourceId"; 10 | $azureResourceInfo = Get-AzureRmResource -ResourceId $resourceId -ev resourceNotPresent -ea 0; 11 | #do not why, but resource retrieval fails sometimes 12 | if ($resourceNotPresent) { 13 | Write-Verbose "Could not get resource for $resourceId"; 14 | } 15 | else 16 | { 17 | $rType = $azureResourceInfo.resourceType; 18 | $rRgName = $azureResourceInfo.ResourceGroupName; 19 | Write-Verbose "Settings tags for $resourceId named $resourceName, belonging to type $rType in resource group $rRgName"; 20 | Set-AzureRmResource -Tag @{ billingCategory="DevProductivity"; environment="Dev"; resourceType="AzureDevOps" } -ResourceName $resourceName -ResourceType $rType -ResourceGroupName $rRgName -Force; 21 | } 22 | 23 | Write-Verbose "Ended tags settings" 24 | } 25 | } 26 | 27 | 28 | function GenerateResourceGroupName { 29 | param ( 30 | $baseName 31 | ) 32 | 33 | $generatedName = $baseName + "-rg"; 34 | Write-Verbose "GenerateResourceGroupName: resource group name is $generatedName"; 35 | return $generatedName; 36 | } 37 | 38 | function GenerateVmssName { 39 | param ( 40 | $baseName 41 | ) 42 | 43 | $generatedName = $baseName + "-vmss"; 44 | Write-Verbose "GenerateVmssName: VMSS name is $generatedName"; 45 | return $generatedName; 46 | } -------------------------------------------------------------------------------- /functions/password-helpers.psm1: -------------------------------------------------------------------------------- 1 | # Grabbed this from https://activedirectoryfaq.com/2017/08/creating-individual-random-passwords/ - nice way for password generation 2 | 3 | function Get-RandomCharacters($length, $characters) { 4 | $random = 1..$length | ForEach-Object { Get-Random -Maximum $characters.length } 5 | $private:ofs="" 6 | return [String]$characters[$random] 7 | } 8 | 9 | function Scramble-String([string]$inputString){ 10 | $characterArray = $inputString.ToCharArray() 11 | $scrambledStringArray = $characterArray | Get-Random -Count $characterArray.Length 12 | $outputString = -join $scrambledStringArray 13 | return $outputString 14 | } -------------------------------------------------------------------------------- /scripts/AddAgentToVM.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [parameter(Mandatory=$true)] 4 | [String[]] 5 | $VSTSToken, 6 | [parameter(Mandatory=$true)] 7 | [String[]] 8 | $VSTSUrl, 9 | $windowsLogonAccount, 10 | $windowsLogonPassword, 11 | $poolName = "Default", 12 | $vstsAgentPackageUri = "https://vstsagentpackage.azureedge.net/agent/2.140.2/vsts-agent-win-x64-2.140.2.zip", 13 | $prepareDataDisks = $true 14 | ) 15 | 16 | $ErrorActionPreference="Stop"; 17 | 18 | If(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]"Administrator")) 19 | { 20 | throw "Run command in Administrator PowerShell Prompt" 21 | }; 22 | 23 | if(-NOT (Test-Path $env:SystemDrive\'vstsagent')) 24 | { 25 | mkdir $env:SystemDrive\'vstsagent' 26 | }; 27 | 28 | Set-Location $env:SystemDrive\'vstsagent'; 29 | 30 | for($i=1; $i -lt 100; $i++) 31 | { 32 | $destFolder="A"+$i.ToString(); 33 | if(-NOT (Test-Path ($destFolder))) 34 | { 35 | mkdir $destFolder; 36 | Set-Location $destFolder; 37 | break; 38 | } 39 | }; 40 | 41 | $agentZip="$PWD\agent.zip"; 42 | 43 | $DefaultProxy=[System.Net.WebRequest]::DefaultWebProxy; 44 | $WebClient=New-Object Net.WebClient; 45 | $Uri=$vstsAgentPackageUri; 46 | 47 | if($DefaultProxy -and (-not $DefaultProxy.IsBypassed($Uri))) 48 | { 49 | $WebClient.Proxy = New-Object Net.WebProxy($DefaultProxy.GetProxy($Uri).OriginalString, $True); 50 | }; 51 | 52 | $WebClient.DownloadFile($Uri, $agentZip); 53 | Add-Type -AssemblyName System.IO.Compression.FileSystem;[System.IO.Compression.ZipFile]::ExtractToDirectory($agentZip, "$PWD"); 54 | 55 | #will default to directly attached disk, if data disk is not there 56 | $agentWorkFolder = "C:\w" 57 | 58 | if ($prepareDataDisks) { 59 | $disks = Get-Disk | Where-Object partitionstyle -eq 'raw' | Sort-Object number 60 | 61 | $letters = 70..89 | ForEach-Object { [char]$_ } 62 | $count = 0 63 | $label = "datadisk" 64 | 65 | foreach ($disk in $disks) { 66 | $driveLetter = $letters[$count].ToString() 67 | $disk | 68 | Initialize-Disk -PartitionStyle MBR -PassThru | 69 | New-Partition -UseMaximumSize -DriveLetter $driveLetter | 70 | Format-Volume -FileSystem NTFS -NewFileSystemLabel $label.$count -Confirm:$false -Force 71 | #we have a data disk - so, we will use it :) 72 | $agentWorkFolder = $driveLetter + ":\w"; 73 | $count++ 74 | } 75 | } 76 | 77 | if(-NOT (Test-Path ($agentWorkFolder))) { 78 | mkdir $agentWorkFolder; 79 | } 80 | 81 | .\config.cmd --unattended ` 82 | --url $VSTSUrl ` 83 | --auth PAT ` 84 | --token $VSTSToken ` 85 | --pool $poolName ` 86 | --agent $env:COMPUTERNAME ` 87 | --replace ` 88 | --runasservice ` 89 | --work $agentWorkFolder ` 90 | --windowsLogonAccount $windowsLogonAccount ` 91 | --windowsLogonPassword $windowsLogonPassword 92 | 93 | Remove-Item $agentZip; 94 | 95 | --------------------------------------------------------------------------------