├── .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 |
--------------------------------------------------------------------------------