├── ARMRunbookScripts ├── AADDSdevopssetup.ps1 ├── AADDSinputValidation.ps1 ├── createDevopsPipeline.sh ├── createServicePrincipal.ps1 ├── devopssetup.ps1 ├── inputValidation.ps1 └── static │ ├── AzureModules.zip │ ├── msft-wvd-saas-api.zip │ └── msft-wvd-saas-web.zip ├── Modules └── ARM │ ├── StorageAccounts │ ├── Parameters │ │ └── parameters.json │ ├── Pipeline │ │ └── pipeline.yml │ ├── Scripts │ │ └── git_placeholder.md │ ├── Tests │ │ └── module.tests.ps1 │ ├── deploy.json │ └── readme.md │ ├── UserCreation │ ├── Parameters │ │ └── users.parameters.json │ └── scripts │ │ └── createUsers.ps1 │ ├── VirtualMachines │ ├── Parameters │ │ ├── parameters.json │ │ └── wvd.parameters.json │ ├── Pipeline │ │ ├── pipeline.jobs.post.yml │ │ └── pipeline.yml │ ├── Scripts │ │ ├── Get-PipelineOutputs.ps1 │ │ └── InitializeDisksWindows.ps1 │ ├── Tests │ │ └── module.tests.ps1 │ ├── deploy.json │ └── readme.md │ ├── WvdApplicationGroups │ ├── Parameters │ │ └── parameters.json │ ├── Pipeline │ │ └── pipeline.yml │ ├── Scripts │ │ └── git_placeholder.md │ ├── Tests │ │ └── module.tests.ps1 │ ├── deploy.json │ └── readme.md │ ├── WvdApplications │ ├── Parameters │ │ └── parameters.json │ ├── Pipeline │ │ └── pipeline.yml │ ├── Scripts │ │ └── git_placeholder.md │ ├── Tests │ │ └── module.tests.ps1 │ ├── deploy.json │ └── readme.md │ ├── WvdHostPools │ ├── Parameters │ │ └── parameters.json │ ├── Pipeline │ │ └── pipeline.yml │ ├── Scripts │ │ └── git_placeholder.md │ ├── Tests │ │ └── module.tests.ps1 │ ├── deploy.json │ └── readme.md │ └── WvdWorkspaces │ ├── Parameters │ └── parameters.json │ ├── Pipeline │ └── pipeline.yml │ ├── Scripts │ └── git_placeholder.md │ ├── Tests │ └── module.tests.ps1 │ ├── deploy.json │ └── readme.md ├── NewSubAADDSSetup ├── README.md └── deploy.json ├── QS-WVD ├── Parameters │ └── DoNotRemove.md ├── Scripts │ ├── Invoke-StorageAccountPostDeployment.ps1 │ └── New-PipelineParameterSetup.ps1 ├── pipeline.yml ├── static │ ├── appliedParameters.template.psd1 │ └── templates │ │ └── pipelineInput │ │ ├── azfiles.parameters.template.json │ │ ├── fslogix.parameters.template.json │ │ ├── keyvault.parameters.template.json │ │ ├── storageaccount.parameters.template.json │ │ ├── wvdapplication.parameters.template.json │ │ ├── wvdapplicationgroup01.parameters.template.json │ │ ├── wvddesktoppapplicationgroup.parameters.template.json │ │ ├── wvdhostpool.parameters.template.json │ │ ├── wvdprofiles-storageaccount-01.parameters.template.json │ │ ├── wvdsessionhost.parameters.template.json │ │ └── wvdworkspace.parameters.template.json └── variables.template.yml ├── README.md ├── SharedDeploymentFunctions ├── Add-CustomParameters.ps1 ├── Invoke-GeneralDeployment.ps1 └── Storage │ ├── Compress-WVDCSEContent.ps1 │ ├── Export-WVDCSEContentToBlob.ps1 │ └── Import-WVDSoftware.ps1 ├── Uploads ├── Configuration.zip └── WVDScripts │ ├── 001-AzFiles │ ├── AzFilesHybrid.psd1 │ ├── AzFilesHybrid.psm1 │ ├── CopyToPSPath.ps1 │ ├── Eula.txt │ ├── PsExec.exe │ ├── PsExec64.exe │ ├── Pstools.chm │ ├── cse_run.ps1 │ ├── psversion.txt │ └── setup.ps1 │ ├── 002-FSLogix │ ├── Install-FSLogix.ps1 │ ├── Set-FSLogix.ps1 │ ├── Set-NTFSPermissions.ps1 │ └── cse_run.ps1 │ ├── 003-NotepadPP │ └── cse_run.ps1 │ ├── 004-Teams │ └── cse_run.ps1 │ ├── downloads.parameters.json │ └── scriptExtensionMasterInstaller.ps1 └── deploy.json /ARMRunbookScripts/createDevopsPipeline.sh: -------------------------------------------------------------------------------- 1 | # Create a new build pipeline in the newly created DevOps project, based on the YAML file that was pulled from the GitHub repository. 2 | # After completion of this script, the pipeline will automatically start running. 3 | # 4 | # Input parameters: 5 | # [Required] ${1} 6 | # [Required] ${2} 7 | # [Required] ${3} 8 | # [Required] ${4} 9 | 10 | 11 | az login --identity 12 | secret="$(az keyvault secret show --name 'azurePassword' --vault-name ${4} --query '[value]' -o tsv)" 13 | az logout 14 | 15 | az login -u ${3} -p $secret 16 | 17 | az extension add --name azure-devops 18 | az pipelines create --name "WVD QuickStart" --organization "https://dev.azure.com/${1}" --project ${2} --repository ${2} --repository-type "tfsgit" --branch "master" --yml-path "QS-WVD/pipeline.yml" 19 | -------------------------------------------------------------------------------- /ARMRunbookScripts/static/AzureModules.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/ARMRunbookScripts/static/AzureModules.zip -------------------------------------------------------------------------------- /ARMRunbookScripts/static/msft-wvd-saas-api.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/ARMRunbookScripts/static/msft-wvd-saas-api.zip -------------------------------------------------------------------------------- /ARMRunbookScripts/static/msft-wvd-saas-web.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/ARMRunbookScripts/static/msft-wvd-saas-web.zip -------------------------------------------------------------------------------- /Modules/ARM/StorageAccounts/Parameters/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "storageAccountName": { 6 | "value": "sbxstoracctest" 7 | }, 8 | "storageAccountKind": { 9 | "value": "StorageV2" 10 | }, 11 | "storageAccountSku": { 12 | "value": "Standard_LRS" 13 | }, 14 | "storageAccountAccessTier": { 15 | "value": "Hot" 16 | }, 17 | // "azureFilesIdentityBasedAuthentication": { 18 | // "value": { 19 | // "directoryServiceOptions": "Azure AD DS" 20 | // } 21 | // }, 22 | // "roleAssignments": { 23 | // "value": [ 24 | // { 25 | // "roleDefinitionIdOrName": "Owner", 26 | // "principalIds": [ 27 | // "a09e8c01-1797-4839-ab3b-8759c951f71b", // WVDKnights (group) 28 | // "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 29 | // ] 30 | // }, 31 | // { 32 | // "roleDefinitionIdOrName": "Reader", 33 | // "principalIds": [ 34 | // "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 35 | // ] 36 | // }, 37 | // { 38 | // "roleDefinitionIdOrName": "/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11", 39 | // "principalIds": [ 40 | // "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 41 | // ] 42 | // } 43 | // ] 44 | // }, 45 | "blobContainers": { 46 | "value": [ 47 | { 48 | "name": "wvdscripts", 49 | "publicAccess": "Container", //Container, Blob, None 50 | "roleAssignments": [ 51 | { 52 | "roleDefinitionIdOrName": "Owner", 53 | "principalIds": [ 54 | "a09e8c01-1797-4839-ab3b-8759c951f71b", // WVDKnights (group) 55 | "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 56 | ] 57 | }, 58 | // { 59 | // "roleDefinitionIdOrName": "Reader", 60 | // "principalIds": [ 61 | // "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 62 | // ] 63 | // }, 64 | // { 65 | // "roleDefinitionIdOrName": "/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11", 66 | // "principalIds": [ 67 | // "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 68 | // ] 69 | // } 70 | ] 71 | }, 72 | { 73 | "name": "wvdsoftware", 74 | "publicAccess": "Container", //Container, Blob, None 75 | "roleAssignments": [] 76 | } 77 | ] 78 | }, 79 | "fileShares": { 80 | "value": [ 81 | { 82 | "name": "wvdprofiles", 83 | "shareQuota": "5120", 84 | "roleAssignments": [ 85 | { 86 | "roleDefinitionIdOrName": "Storage File Data SMB Share Contributor", 87 | "principalIds": [ 88 | "a09e8c01-1797-4839-ab3b-8759c951f71b", // WVDKnights (group) 89 | "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 90 | ] 91 | }, 92 | { 93 | "roleDefinitionIdOrName": "Owner", 94 | "principalIds": [ 95 | "a09e8c01-1797-4839-ab3b-8759c951f71b", // WVDKnights (group) 96 | "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 97 | ] 98 | }, 99 | // { 100 | // "roleDefinitionIdOrName": "Reader", 101 | // "principalIds": [ 102 | // "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 103 | // ] 104 | // }, 105 | // { 106 | // "roleDefinitionIdOrName": "/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11", 107 | // "principalIds": [ 108 | // "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 109 | // ] 110 | // } 111 | ] 112 | }, 113 | { 114 | "name": "wvdprofiles2", 115 | "shareQuota": "5120", 116 | "roleAssignments": [ 117 | // { 118 | // "roleDefinitionIdOrName": "Owner", 119 | // "principalIds": [ 120 | // "a09e8c01-1797-4839-ab3b-8759c951f71b" 121 | // //, // WVDKnights (group) 122 | // //"cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 123 | // ] 124 | // } 125 | //, 126 | // { 127 | // "roleDefinitionIdOrName": "Reader", 128 | // "principalIds": [ 129 | // "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 130 | // ] 131 | // }, 132 | // { 133 | // "roleDefinitionIdOrName": "/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11", 134 | // "principalIds": [ 135 | // "cb9df0b6-cc86-4982-9266-a38f68e68200" // AlsehrTestUser0 (user) 136 | // ] 137 | // } 138 | ] 139 | } 140 | ] 141 | } 142 | } 143 | } -------------------------------------------------------------------------------- /Modules/ARM/StorageAccounts/Pipeline/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(moduleName) 2 | 3 | variables: 4 | - template: /Modules/ARM/.global/global.variables.yml 5 | - name: moduleName 6 | value: StorageAccounts 7 | - name: removeDeployment 8 | value: false 9 | - name: moduleVersion 10 | value: 1.0.1 11 | - name: versionOption 12 | value: patch # major, minor, patch, or custom 13 | 14 | trigger: 15 | batch: true 16 | branches: 17 | include: 18 | - master 19 | paths: 20 | include: 21 | - Modules/ARM/.global/* 22 | - Modules/ARM/StorageAccounts/* 23 | exclude: 24 | - /readme.md 25 | 26 | stages: 27 | - stage: Validation 28 | jobs: 29 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.validate.yml 30 | parameters: 31 | moduleName: '$(moduleName)' 32 | resourceGroupName: '$(resourceGroupName)' 33 | modulePath: '$(modulesPath)/$(moduleName)' 34 | parameterFilePaths: 35 | - $(modulePath)/Parameters/parameters.json 36 | 37 | - stage: Deployment 38 | jobs: 39 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.deploy.yml 40 | parameters: 41 | moduleName: '$(moduleName)' 42 | resourceGroupName: '$(resourceGroupName)' 43 | modulePath: '$(modulesPath)/$(moduleName)' 44 | parameterFilePaths: 45 | - $(modulePath)/Parameters/parameters.json 46 | 47 | - stage: Publishing 48 | jobs: 49 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.publish.yml 50 | parameters: 51 | moduleName: ${{lower( variables.moduleName )}} 52 | versionOption: $(versionOption) 53 | moduleVersion: $(moduleVersion) 54 | 55 | - stage: Removal 56 | dependsOn: Deployment 57 | condition: and(succeededOrFailed(), eq( variables['removeDeployment'], 'true')) 58 | jobs: 59 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.remove.yml -------------------------------------------------------------------------------- /Modules/ARM/StorageAccounts/Scripts/git_placeholder.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/Modules/ARM/StorageAccounts/Scripts/git_placeholder.md -------------------------------------------------------------------------------- /Modules/ARM/StorageAccounts/Tests/module.tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .NOTES 3 | ============================================================================================== 4 | Copyright(c) Microsoft Corporation. All rights reserved. 5 | 6 | File: module.tests.ps1 7 | 8 | Purpose: Pester - Test ARM Templates 9 | 10 | Version: 2.0.1 - 15th June 2020 - Microsoft Services 11 | ============================================================================================== 12 | 13 | .SYNOPSIS 14 | This script contains functionality used to test ARM template synatax. 15 | 16 | .DESCRIPTION 17 | This script contains functionality used to test ARM template synatax. 18 | 19 | Deployment steps of the script are outlined below. 20 | 1) Test Template File Syntax 21 | 2) Test Parameter File Syntax 22 | 3) Test Template and Parameter File Compatibility 23 | #> 24 | 25 | #region Parameters 26 | 27 | #Requires -Version 7 28 | 29 | param ( 30 | [Parameter()][string]$script:ParameterFilePath = 'Parameters', 31 | [Parameter()][string]$script:TemplateFileName = 'deploy.json' 32 | ) 33 | 34 | #endregion 35 | 36 | #region Collect parameter files for TestCases 37 | 38 | $Parameters = @() 39 | $ParameterFiles = (Get-ChildItem (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $ParameterFilePath -AdditionalChildPath "*parameters.json") -Recurse).Name 40 | ForEach ($ParameterFile in $ParameterFiles) { 41 | $Parameters += @{ 42 | ParameterFileName = $ParameterFile 43 | } 44 | } 45 | 46 | #endregion 47 | 48 | #region Tests 49 | 50 | Describe "Template: $(Split-Path $($(Get-Item $PSScriptRoot).Parent.FullName) -Leaf)" -Tags Unit { 51 | 52 | Context "Template File Syntax" { 53 | 54 | It "JSON template file ($TemplateFileName) exists" { 55 | if(-not (Get-ChildItem $($(Get-Item $PSScriptRoot).Parent.FullName) $TemplateFileName)) { 56 | Write-Host " [-] Template file ($TemplateFileName) does not exist." 57 | exit 58 | } 59 | (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $TemplateFileName) | Should -Exist 60 | } 61 | 62 | It "Template file ($TemplateFileName) converts from JSON and has all expected properties" { 63 | $ExpectedProperties = '$schema', 64 | 'contentVersion', 65 | 'parameters', 66 | 'variables', 67 | 'resources', 68 | 'functions', 69 | 'outputs' | Sort-Object 70 | $TemplateProperties = (Get-Content (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $TemplateFileName) ` 71 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 72 | | Get-Member -MemberType NoteProperty ` 73 | | Sort-Object -Property Name ` 74 | | ForEach-Object Name 75 | $TemplateProperties | Should -Be $ExpectedProperties 76 | } 77 | } 78 | 79 | Context "Parameter File Syntax" { 80 | 81 | It "Parameter file () does contain all expected properties" -TestCases $Parameters { 82 | Param ($ParameterFileName) 83 | 84 | $ExpectedProperties = '$schema', 85 | 'contentVersion', 86 | 'parameters' | Sort-Object 87 | $templateFileProperties = (Get-Content (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $ParameterFilePath -AdditionalChildPath $ParameterFileName) ` 88 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 89 | | Get-Member -MemberType NoteProperty ` 90 | | Sort-Object -Property Name ` 91 | | ForEach-Object Name 92 | $templateFileProperties | Should -Be $ExpectedProperties 93 | } 94 | } 95 | 96 | Context "Template and Parameter Compatibility" { 97 | 98 | It "Count of required parameters in template file ($TemplateFileName) is equal or less than count of all parameters in parameters file ()" -TestCases $Parameters { 99 | Param ($ParameterFileName) 100 | 101 | $requiredParametersInTemplateFile = (Get-Content (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $TemplateFileName) ` 102 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 103 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 104 | | Sort-Object -Property Name ` 105 | | ForEach-Object Name 106 | $allParametersInParametersFile = (Get-Content (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $ParameterFilePath -AdditionalChildPath $ParameterFileName) ` 107 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 108 | | Sort-Object -Property Name ` 109 | | ForEach-Object Name 110 | if ($requiredParametersInTemplateFile.Count -gt $allParametersInParametersFile.Count) { 111 | Write-Host " [-] Required parameters are: $requiredParametersInTemplateFile" 112 | $requiredParametersInTemplateFile.Count | Should -Not -BeGreaterThan $allParametersInParametersFile.Count 113 | } 114 | } 115 | 116 | It "All parameters in parameters file () exist in template file ($TemplateFileName)" -TestCases $Parameters { 117 | Param ($ParameterFileName) 118 | 119 | $allParametersInTemplateFile = (Get-Content (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $TemplateFileName) ` 120 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 121 | | Sort-Object -Property Name ` 122 | | ForEach-Object Name 123 | $allParametersInParametersFile = (Get-Content (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $ParameterFilePath -AdditionalChildPath $ParameterFileName) ` 124 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 125 | | Sort-Object -Property Name ` 126 | | ForEach-Object Name 127 | $result = @($allParametersInParametersFile | Where-Object { $allParametersInTemplateFile -notcontains $_ }) 128 | if ($result) { 129 | Write-Host " [-] Following parameter does not exist: $result" 130 | } 131 | @($allParametersInParametersFile | Where-Object { $allParametersInTemplateFile -notcontains $_ }).Count | Should -Be 0 132 | } 133 | 134 | It "All required parameters in template file ($TemplateFileName) existing in parameters file ()" -TestCases $Parameters { 135 | Param ($ParameterFileName) 136 | 137 | $requiredParametersInTemplateFile = (Get-Content (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $TemplateFileName) ` 138 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 139 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 140 | | Sort-Object -Property Name ` 141 | | ForEach-Object Name 142 | $allParametersInParametersFile = (Get-Content (Join-Path -Path $($(Get-Item $PSScriptRoot).Parent.FullName) -ChildPath $ParameterFilePath -AdditionalChildPath $ParameterFileName) ` 143 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 144 | | Sort-Object -Property Name ` 145 | | ForEach-Object Name 146 | $result = $requiredParametersInTemplateFile | Where-Object { $allParametersInParametersFile -notcontains $_ } 147 | if ($result.Count -gt 0) { 148 | Write-Host " [-] Required parameters: $result" 149 | } 150 | @($requiredParametersInTemplateFile | Where-Object { $allParametersInParametersFile -notcontains $_ }).Count | Should -Be 0 151 | } 152 | } 153 | } 154 | #endregion -------------------------------------------------------------------------------- /Modules/ARM/UserCreation/Parameters/users.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "userconfig": [ 3 | { 4 | "createUser": true, 5 | "createGroup": true, 6 | "assignUsers": true, 7 | "syncAD": true, 8 | "userName": "WVDTestUser001" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /Modules/ARM/UserCreation/scripts/createUsers.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .DESCRIPTION 4 | This script is ran by the main ARM template as a custom script extension on the domain controller vm to create an AD user group and an AD user (a test user for the WVD environment). 5 | This user then gets synced to Azure Active Directory using the ADSync module. 6 | 7 | .PARAMETER domainName 8 | Name of the domain 9 | 10 | .PARAMETER targetGroup 11 | Name of the test user group to be created 12 | 13 | .PARAMETER artifactsLocation 14 | URL of the GitHub repository 15 | 16 | .PARAMETER domainUsername 17 | username of the domain join account 18 | 19 | .PARAMETER domainPassword 20 | password of the domain join account. Not stored in any logs. 21 | 22 | .PARAMETER devOpsName 23 | Name of the DevOps organization to generate the test user password 24 | 25 | #> 26 | 27 | [CmdletBinding(SupportsShouldProcess = $true)] 28 | $ConfigurationFileName = "users.parameters.json" 29 | 30 | # Parameters below are passed by the main ARM template 31 | $domainName = $args[0] 32 | $targetGroup = $args[1] 33 | $artifactsLocation = $args[2] 34 | $domainUsername = $args[3] 35 | $domainPassword = $args[4] 36 | $devOpsName = $args[5] 37 | ##################################### 38 | 39 | ########## 40 | # Helper # 41 | ########## 42 | #region Functions 43 | function LogInfo($message) { 44 | Log "Info" $message 45 | } 46 | 47 | function LogError($message) { 48 | Log "Error" $message 49 | } 50 | 51 | function LogSkip($message) { 52 | Log "Skip" $message 53 | } 54 | 55 | function LogWarning($message) { 56 | Log "Warning" $message 57 | } 58 | 59 | function Log { 60 | 61 | <# 62 | .SYNOPSIS 63 | Creates a log file and stores logs based on categories with tab seperation 64 | 65 | .PARAMETER category 66 | Category to put into the trace 67 | 68 | .PARAMETER message 69 | Message to be loged 70 | 71 | .EXAMPLE 72 | Log 'Info' 'Message' 73 | 74 | #> 75 | 76 | Param ( 77 | $category = 'Info', 78 | [Parameter(Mandatory = $true)] 79 | $message 80 | ) 81 | 82 | $date = get-date 83 | $content = "[$date]`t$category`t`t$message`n" 84 | Write-Verbose "$content" -verbose 85 | 86 | if (! $script:Log) { 87 | $File = Join-Path $env:TEMP "log.log" 88 | Write-Error "Log file not found, create new $File" 89 | $script:Log = $File 90 | } 91 | else { 92 | $File = $script:Log 93 | } 94 | Add-Content $File $content -ErrorAction Stop 95 | } 96 | 97 | function Set-Logger { 98 | <# 99 | .SYNOPSIS 100 | Sets default log file and stores in a script accessible variable $script:Log 101 | Log File name "executionCustomScriptExtension_$date.log" 102 | 103 | .PARAMETER Path 104 | Path to the log file 105 | 106 | .EXAMPLE 107 | Set-Logger 108 | Create a logger in 109 | #> 110 | 111 | Param ( 112 | [Parameter(Mandatory = $true)] 113 | $Path 114 | ) 115 | 116 | # Create central log file with given date 117 | 118 | $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" 119 | 120 | $scriptName = (Get-Item $PSCommandPath ).Basename 121 | $scriptName = $scriptName -replace "-", "" 122 | 123 | Set-Variable logFile -Scope Script 124 | $script:logFile = "executionCustomScriptExtension_" + $scriptName + "_" + $date + ".log" 125 | 126 | if ((Test-Path $path ) -eq $false) { 127 | $null = New-Item -Path $path -type directory 128 | } 129 | 130 | $script:Log = Join-Path $path $logfile 131 | 132 | Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" 133 | } 134 | #endregion 135 | 136 | 137 | ## MAIN 138 | #Set-Logger "C:\WindowsAzure\CustomScriptExtension\Log" # inside "executionCustomScriptExtension_$date.log" 139 | Set-Logger "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog\UserConfig" # inside "executionCustomScriptExtension_$scriptName_$date.log" 140 | 141 | LogInfo("## 0 - LOAD DATA ##") 142 | 143 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 #Fix for TLS 144 | 145 | $url = $($artifactsLocation + "/Modules/ARM/UserCreation/Parameters/users.parameters.json") 146 | Invoke-WebRequest -Uri $url -OutFile "C:\users.parameters.json" 147 | $ConfigurationJson = Get-Content -Path "C:\users.parameters.json" -Raw -ErrorAction 'Stop' 148 | 149 | try { $UserConfig = $ConfigurationJson | ConvertFrom-Json -ErrorAction 'Stop' } 150 | catch { 151 | Write-Error "Configuration JSON content could not be converted to a PowerShell object" -ErrorAction 'Stop' 152 | } 153 | 154 | Import-Module activedirectory 155 | 156 | $adminUsername = $domainName + "\" + $domainUsername 157 | if ((new-object directoryservices.directoryentry "",$adminUsername,$domainPassword).psbase.name -ne $null) 158 | { 159 | LogInfo("Valid domain join credentials") 160 | } 161 | else 162 | { 163 | Write-Error "Invalid domain join credentials entered" -ErrorAction 'Stop' 164 | } 165 | 166 | foreach ($config in $UserConfig.userconfig) { 167 | 168 | if ($config.createGroup) { 169 | LogInfo("## 1 - Create user group ##") 170 | 171 | $userGroupName = $targetGroup 172 | 173 | LogInfo("Create user group...") 174 | 175 | $existingGroup = Get-ADGroup -Filter "Name -eq '$($userGroupName)'" 176 | if($existingGroup -eq $null) { 177 | New-ADGroup ` 178 | -SamAccountName $userGroupName ` 179 | -Name "$userGroupName" ` 180 | -DisplayName "$userGroupName" ` 181 | -GroupScope "Global" ` 182 | -GroupCategory "Security" -Verbose 183 | } 184 | else { 185 | LogInfo("User group $userGroupName already exists, using that existing group.") 186 | } 187 | 188 | LogInfo("Create user group completed.") 189 | } 190 | 191 | if ($config.createUser) { 192 | LogInfo("## 2 - Create user ##") 193 | 194 | $userName = $config.userName 195 | $password = $devOpsName.substring(13) + '!' 196 | 197 | $existingUser = Get-ADUser -Filter "Name -eq '$($userName)'" 198 | if($existingUser -ne $null) { 199 | LogInfo("Existing user with the username $userName found. Removing that user...") 200 | Set-ADUser -Identity $userName -UserPrincipalName $($userName + "temp@" + $domainName) 201 | Remove-ADUser -Identity $userName -Confirm:$False 202 | Import-Module ADSync -Force 203 | Start-ADSyncSyncCycle -PolicyType Delta -Verbose 204 | Start-Sleep -Seconds 90 205 | LogInfo("Existing user removed.") 206 | } 207 | 208 | LogInfo("Creating user...") 209 | 210 | New-ADUser ` 211 | -SamAccountName $userName ` 212 | -UserPrincipalName $($userName + "@" + $domainName) ` 213 | -Name "$userName" ` 214 | -GivenName $userName ` 215 | -Surname $userName ` 216 | -Enabled $True ` 217 | -ChangePasswordAtLogon $False ` 218 | -DisplayName "$userName" ` 219 | -AccountPassword (convertto-securestring $password -AsPlainText -Force) -Verbose 220 | 221 | LogInfo("Create user completed.") 222 | } 223 | 224 | if ($config.assignUsers) { 225 | LogInfo("## 3 - Assign users to group ##") 226 | 227 | Add-ADGroupMember -Identity $targetGroup -Members $config.userName 228 | LogInfo("User assignment to group completed.") 229 | } 230 | 231 | if ($config.syncAD) { 232 | LogInfo("## 4 - Sync new users & group with AD Sync ##") 233 | 234 | Import-Module ADSync -Force 235 | Start-ADSyncSyncCycle -PolicyType Delta -Verbose 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Modules/ARM/VirtualMachines/Parameters/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vmNames": { 6 | "value": ["csazeuww100011", "csazeuww100012"] 7 | }, 8 | "imageReference": { 9 | "value": { 10 | "publisher": "MicrosoftWindowsServer", 11 | "offer": "WindowsServer", 12 | "sku": "2016-Datacenter", 13 | "version": "latest" 14 | } 15 | }, 16 | "osDisk": { 17 | "value": { 18 | "createOption": "fromImage", 19 | "diskSizeGB": "128", 20 | "managedDisk": { 21 | "storageAccountType": "Premium_LRS" 22 | } 23 | } 24 | }, 25 | "adminUsername": { 26 | "value": "testadminuser01" 27 | }, 28 | "adminPassword": { 29 | "reference": { 30 | "keyVault": { 31 | "id": "/subscriptions/62826c76-d304-46d8-a0f6-718dbdcc536c/resourceGroups/WVD-Mgmt-Rg/providers/Microsoft.KeyVault/vaults/wvd-kvlt" 32 | }, 33 | "secretName": "domainJoinUser-Password" 34 | } 35 | }, 36 | "domainJoinPassword": { 37 | "reference": { 38 | "keyVault": { 39 | "id": "/subscriptions/62826c76-d304-46d8-a0f6-718dbdcc536c/resourceGroups/WVD-Mgmt-Rg/providers/Microsoft.KeyVault/vaults/wvd-kvlt" 40 | }, 41 | "secretName": "domainJoinUser-Password" 42 | } 43 | }, 44 | "subnetId": { 45 | "value": "/subscriptions/3fb93bd4-2dd2-4b12-afba-3dd335c8154d/resourceGroups/omv-cc-valid-np-01-euw-rg-shared-np-01/providers/Microsoft.Network/virtualNetworks/omv-cc-valid-np-01-euw-vnet-test-sna-01/subnets/sub-10-0-1-0--28" 46 | }, 47 | "dNSServers": { 48 | "value": [ 49 | "168.63.129.16" 50 | ] 51 | }, 52 | "enableNetworkWatcherWindows": { 53 | "value": true 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /Modules/ARM/VirtualMachines/Parameters/wvd.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vmInitialNumber": { 6 | "value": 2 7 | }, 8 | "location": { 9 | "value": "westeurope" 10 | }, 11 | "imageReference": { 12 | "value": { 13 | "publisher": "MicrosoftWindowsDesktop", 14 | "offer": "Windows-10", 15 | "sku": "19h2-evd", 16 | "version": "latest" 17 | } 18 | }, 19 | "osDisk": { 20 | "value": { 21 | "createOption": "fromImage", 22 | "diskSizeGB": "128", 23 | "managedDisk": { 24 | "storageAccountType": "Premium_LRS" 25 | } 26 | } 27 | }, 28 | "adminPassword": { 29 | "reference": { 30 | "keyVault": { 31 | "id": "/subscriptions/62826c76-d304-46d8-a0f6-718dbdcc536c/resourceGroups/WVD-Mgmt-Rg/providers/Microsoft.KeyVault/vaults/wvd-kvlt" 32 | }, 33 | "secretName": "domainJoinUser-Password" 34 | } 35 | }, 36 | "adminUsername": { 37 | "value": "localadmin" 38 | }, 39 | "availabilitySetName": { 40 | "value": "wvdv2-availabilitySet-westeurope" 41 | }, 42 | "subnetId": { 43 | "value": "/subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/rgName/providers/Microsoft.Network/virtualNetworks/vnetName/subnets/subnetName" 44 | }, 45 | "domainName": { 46 | "value": "domainname.onmicrosoft.com" 47 | }, 48 | "domainJoinUser": { 49 | "value": "domainjoin@domainname.onmicrosoft.com" 50 | }, 51 | "domainJoinPassword": { 52 | "reference": { 53 | "keyVault": { 54 | "id": "/subscriptions/62826c76-d304-46d8-a0f6-718dbdcc536c/resourceGroups/WVD-Mgmt-Rg/providers/Microsoft.KeyVault/vaults/wvd-kvlt" 55 | }, 56 | "secretName": "domainJoinUser-Password" 57 | } 58 | }, 59 | "dscConfiguration": { 60 | "value": { 61 | "settings": { 62 | "wmfVersion": "latest", 63 | "configuration": { 64 | "url": "https://datrgallerycontainer.blob.core.windows.net/gallaryartifacts/Configuration.zip", 65 | "script": "Configuration.ps1", 66 | "function": "AddSessionHost" 67 | }, 68 | "configurationArguments": { 69 | "hostPoolName": "wvdv2" 70 | } 71 | }, 72 | "protectedSettings": { 73 | "configurationArguments": { 74 | "registrationInfoToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFCOUNENjgwRUZFN0M3ODhEQjExMDQ0Qjk0M0Y5MjMwRUZEOUVDRDYiLCJ0eXAiOiJKV1QifQ.eyJSZWdpc3RyYXRpb25JZCI6ImVhNzA4OTkwLTE5Y2UtNGE5MC04ZjE3LTdkMGVmZWRlZTM4NiIsIkJyb2tlclVyaSI6Imh0dHBzOi8vcmRicm9rZXItZy11cy1yMC53dmQubWljcm9zb2Z0LmNvbS8iLCJEaWFnbm9zdGljc1VyaSI6Imh0dHBzOi8vcmRkaWFnbm9zdGljcy1nLXVzLXIwLnd2ZC5taWNyb3NvZnQuY29tLyIsIkVuZHBvaW50UG9vbElkIjoiOGI4NmU0NGEtODQ3ZS00OTFjLWIzM2EtOWY4Yzc2OGM5NzQ1IiwiR2xvYmFsQnJva2VyVXJpIjoiaHR0cHM6Ly9yZGJyb2tlci53dmQubWljcm9zb2Z0LmNvbS8iLCJHZW9ncmFwaHkiOiJVUyIsIm5iZiI6MTU4OTMwODUxMiwiZXhwIjoxNTkwOTYyNDAwLCJpc3MiOiJSREluZnJhVG9rZW5NYW5hZ2VyIiwiYXVkIjoiUkRtaSJ9.M_2gv0QrXtyEhEChzpDpHAJWRvCTa0YUTz6R71ouH7xJscb-5zFmyIvj-Bw2w7uUz6rRRyEezBnoYVdP7kckfElyrvFcAbsKG5MWgWB2V605obQ7JdMCxvOrdeAQjBzwkKY_ewgdfuSTMI6a0Olq-fwY4UB_hL2vMUjguO9VsN6isSaybK0jTkOdGe4Sto8E9BpZbTDDFQL8m0OVbseG5JrykfwbK9EjU0gudoVU3jEcv-1x4YNU4QG47vM7qnOgGyFk7IdEZkkfKk8sHQedmy3T5cUe4pUQI64Y9_GPsg7NCNRQPEhYo7s2iyjmh0cwke8152HjFXcf4U4UreL_tg" 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Modules/ARM/VirtualMachines/Pipeline/pipeline.jobs.post.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | - job: 3 | displayName: Post Deployment 4 | pool: 5 | ${{ if eq(variables['vmImage'], '') }}: 6 | name: $(poolName) 7 | ${{ if eq(variables['poolName'], '') }}: 8 | vmImage: $(vmImage) 9 | steps: 10 | - task: AzurePowerShell@4 11 | enabled: true 12 | displayName: Initialize data disks of all VMs (Windows) in ${{ parameters.resourceGroupName }} 13 | inputs: 14 | azureSubscription: $(serviceConnection) 15 | ScriptType: InlineScript 16 | azurePowerShellVersion: LatestVersion 17 | Inline: | 18 | $vms = Get-AzVM -ResourceGroupName ${{ parameters.resourceGroupName }} 19 | $scriptLocation = "${{ parameters.modulePath }}/Scripts/InitializeDisksWindows.ps1" 20 | foreach ($vm in $vms) { 21 | Invoke-AzVMRunCommand -ResourceGroupName ${{ parameters.resourceGroupName }} -Name $vm.Name -CommandId 'RunPowerShellScript' -ScriptPath "$(Build.Repository.LocalPath)/$scriptLocation" 22 | } 23 | - task: AzurePowerShell@4 24 | enabled: true 25 | displayName: Encrypt all VMs in ${{ parameters.resourceGroupName }} 26 | inputs: 27 | azureSubscription: $(serviceConnection) 28 | ScriptType: InlineScript 29 | azurePowerShellVersion: LatestVersion 30 | Inline: | 31 | $vms = Get-AzVM -ResourceGroupName ${{ parameters.resourceGroupName }} 32 | $keyVault = Get-AzKeyVault -VaultName $(diskEncryptionKeyVault) -ResourceGroupName ${{ parameters.resourceGroupName }} 33 | foreach ($vm in $vms) { 34 | Set-AzVMDiskEncryptionExtension -ResourceGroupName ${{ parameters.resourceGroupName }} -VMName $vm.Name -DiskEncryptionKeyVaultUrl $keyVault.VaultUri -DiskEncryptionKeyVaultId $keyVault.ResourceId -SkipVmBackup -VolumeType All -Force 35 | } -------------------------------------------------------------------------------- /Modules/ARM/VirtualMachines/Pipeline/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(moduleName) 2 | 3 | variables: 4 | - template: /Modules/ARM/.global/global.variables.yml 5 | - name: moduleName 6 | value: VirtualMachines 7 | - name: removeDeployment 8 | value: false 9 | - name: enablePostDeployment 10 | value: false 11 | 12 | # Module 13 | - name: vmName 14 | value: SAZ0010 15 | - name: diskEncryptionKeyVault 16 | value: sxx-wd-kv-weu-x-001 17 | 18 | # Versioning - Use these only, if you would like to publish modules as Universal Packages (in ADO Artifacts) 19 | - name: moduleVersion 20 | value: 1.0.1 21 | - name: versionOption 22 | value: patch # major, minor, patch, or custom 23 | 24 | trigger: 25 | batch: true 26 | branches: 27 | include: 28 | - master 29 | paths: 30 | include: 31 | - Modules/ARM/.global/* 32 | - Modules/VirtualMachines/WvdHostPools/* 33 | exclude: 34 | - /readme.md 35 | 36 | stages: 37 | - stage: Validation 38 | jobs: 39 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.validate.yml 40 | parameters: 41 | moduleName: '$(moduleName)' 42 | resourceGroupName: '$(resourceGroupName)' 43 | modulePath: '$(modulesPath)/$(moduleName)' 44 | parameterFilePaths: 45 | - $(modulePath)/Parameters/parameters.json 46 | 47 | - stage: Deployment 48 | jobs: 49 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.deploy.yml 50 | parameters: 51 | moduleName: '$(moduleName)' 52 | resourceGroupName: '$(resourceGroupName)' 53 | modulePath: '$(modulesPath)/$(moduleName)' 54 | parameterFilePaths: 55 | - $(modulePath)/Parameters/parameters.json 56 | 57 | - stage: PostDeployment 58 | condition: and(succeeded(), eq( variables['enablePostDeploymentl'], 'true')) 59 | jobs: 60 | - template: pipeline.jobs.post.yml 61 | 62 | - stage: Publishing 63 | jobs: 64 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.publish.yml 65 | parameters: 66 | moduleName: ${{lower( variables.moduleName )}} 67 | versionOption: $(versionOption) 68 | moduleVersion: $(moduleVersion) 69 | 70 | - stage: Removal 71 | dependsOn: Deployment 72 | condition: and(succeededOrFailed(), eq( variables['removeDeployment'], 'true')) 73 | jobs: 74 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.remove.yml -------------------------------------------------------------------------------- /Modules/ARM/VirtualMachines/Scripts/Get-PipelineOutputs.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [Parameter(Mandatory = $true)] 3 | [string] 4 | $outputString 5 | ) 6 | 7 | Write-Host "ARM output JSON is:" 8 | Write-Host $outputString 9 | 10 | $outputObj = $outputString | ConvertFrom-Json 11 | 12 | $outputObj.PSObject.Properties | ForEach-Object { 13 | $type = ($_.value.type).ToLower() 14 | $keyname = "$($_.name)" 15 | $value = $_.value.value 16 | 17 | if ($type -eq "securestring") { 18 | Write-Host "##vso[task.setvariable variable=$keyname;issecret=true]$value" 19 | Write-Host "Added Azure DevOps secret variable '$keyname' ('$type')" 20 | } 21 | elseif ($type -eq "string") { 22 | Write-Host "##vso[task.setvariable variable=$keyname]$value" 23 | Write-Host "Added Azure DevOps variable '$keyname' ('$type') with value '$value'" 24 | } 25 | elseif ($type -eq "array") { 26 | for ($i = 0; $i -lt $value.Length; $i++) { 27 | $variable = "$keyname" + "$i" 28 | $arrayValue = $value[$i] 29 | Write-Host "##vso[task.setvariable variable=$variable]$arrayValue" 30 | Write-Host "Added Azure DevOps variable '$variable' ('$type') with value '$arrayValue'" 31 | } 32 | } 33 | else { 34 | Throw "Type '$type' is not supported for '$keyname'" 35 | } 36 | } -------------------------------------------------------------------------------- /Modules/ARM/VirtualMachines/Scripts/InitializeDisksWindows.ps1: -------------------------------------------------------------------------------- 1 | $disks = Get-Disk | Where partitionstyle -eq 'raw' | sort number 2 | $letters = 70..89 | ForEach-Object { [char]$_ } 3 | $count = 0 4 | $labels = "Data1","Data2","Data3" 5 | 6 | foreach ($disk in $disks) { 7 | $driveLetter = $letters[$count].ToString() 8 | $disk | 9 | Initialize-Disk -PartitionStyle MBR -PassThru | 10 | New-Partition -UseMaximumSize -DriveLetter $driveLetter | 11 | Format-Volume -FileSystem NTFS -NewFileSystemLabel $labels[$count] -Confirm:$false -Force 12 | $count++ 13 | } -------------------------------------------------------------------------------- /Modules/ARM/VirtualMachines/Tests/module.tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .NOTES 3 | ============================================================================================== 4 | Copyright(c) Microsoft Corporation. All rights reserved. 5 | 6 | File: module.tests.ps1 7 | 8 | Purpose: Pester - Test ARM Templates 9 | 10 | Version: 2.0.0.0 - 2nd June 2020 - Microsoft Consulting Services 11 | ============================================================================================== 12 | 13 | .SYNOPSIS 14 | This script contains functionality used to test ARM template synatax. 15 | 16 | .DESCRIPTION 17 | This script contains functionality used to test ARM template synatax. 18 | 19 | Deployment steps of the script are outlined below. 20 | 1) Test Template File Syntax 21 | 2) Test Parameter File Syntax 22 | 3) Test Template and Parameter File Compatibility 23 | #> 24 | 25 | #Requires -Version 7 26 | 27 | #region - Parameters 28 | $parametersLocation = 'Parameters' 29 | $script:here = Split-Path -Path $PSCommandPath -Parent 30 | $script:here = $(Get-Item $here).Parent.FullName 31 | $template = (Get-Item $here).parent.Name 32 | $script:TemplateFileTestCases = @() 33 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "*deploy.json") -Recurse | Select-Object -ExpandProperty Name) ) { 34 | $script:TemplateFileTestCases += @{ TemplateFile = $File } 35 | } 36 | $script:ParameterFileTestCases = @() 37 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("*parameters.json")) -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) ) { 38 | $script:ParameterFileTestCases += @{ ParameterFile = Join-Path -Path "$parametersLocation" -ChildPath $File } 39 | } 40 | $script:Modules = @(); 41 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "deploy.json") ) ) { 42 | $Module = [PSCustomObject]@{ 43 | 'Template' = $null 44 | 'Parameters' = $null 45 | } 46 | $Module.Template = $File.FullName 47 | $Parameters = @() 48 | ForEach ( $ParameterFile in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("*parameters.json")) -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) ) { 49 | $Parameters += (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("$ParameterFile") ) 50 | } 51 | $Module.Parameters = $Parameters 52 | $script:Modules += @{ Module = $Module } 53 | } 54 | #endregion 55 | 56 | #region - Run Pester Test Script 57 | Describe "Template: $template" -Tags Unit { 58 | 59 | Context "Template File Syntax" { 60 | 61 | It "JSON template file (deploy.json) exists" { 62 | (Join-Path -Path "$here" -ChildPath "deploy.json") | Should -Exist 63 | } 64 | 65 | It "Template file (deploy.json) converts from JSON and has all expected properties" -TestCases $TemplateFileTestCases { 66 | Param ($TemplateFile) 67 | $expectedProperties = '$schema', 68 | 'contentVersion', 69 | 'parameters', 70 | 'variables', 71 | 'resources', 72 | 'functions', 73 | 'outputs'| Sort-Object 74 | $templateProperties = (Get-Content (Join-Path -Path "$here" -ChildPath "$TemplateFile") ` 75 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 76 | | Get-Member -MemberType NoteProperty ` 77 | | Sort-Object -Property Name ` 78 | | ForEach-Object Name 79 | $templateProperties | Should -Be $expectedProperties 80 | } 81 | } 82 | 83 | Context "Parameter File Syntax" { 84 | 85 | It "Parameter file ($ParameterFile) does contain all expected properties" -TestCases $ParameterFileTestCases { 86 | Param ($ParameterFile) 87 | $expectedProperties = '$schema', 88 | 'contentVersion', 89 | 'parameters' | Sort-Object 90 | $templateFileProperties = (Get-Content (Join-Path -Path "$here" -ChildPath "$ParameterFile") ` 91 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 92 | | Get-Member -MemberType NoteProperty ` 93 | | Sort-Object -Property Name ` 94 | | ForEach-Object Name 95 | $templateFileProperties | Should -Be $expectedProperties 96 | } 97 | } 98 | 99 | Context "Template and Parameter Compatibility" { 100 | 101 | It "Count of required parameters in template file ($((Get-Item $Module.Template).Name)) is equal or less than count of all parameters in parameters file ($((Get-Item $Module.Parameters).Name))" -TestCases $Modules { 102 | Param ($Module) 103 | 104 | $requiredParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 105 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 106 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 107 | | Sort-Object -Property Name ` 108 | | ForEach-Object Name 109 | ForEach ( $Parameter in $Module.Parameters ) { 110 | $allParametersInParametersFile = (Get-Content $Parameter ` 111 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 112 | | Sort-Object -Property Name ` 113 | | ForEach-Object Name 114 | if ($requiredParametersInTemplateFile.Count -gt $allParametersInParametersFile.Count) { 115 | Write-Host "Mismatch found, parameters from parameter file are more than the expected in the template" 116 | Write-Host "Required parameters are: $(ConvertTo-Json $requiredParametersInTemplateFile)" 117 | Write-Host "Parameters from parameter file are: $(ConvertTo-Json $allParametersInParametersFile)" 118 | } 119 | $requiredParametersInTemplateFile.Count | Should -Not -BeGreaterThan $allParametersInParametersFile.Count; 120 | } 121 | } 122 | 123 | It "All parameters in parameters file ($((Get-Item $Module.Parameters).Name)) exist in template file ($((Get-Item $Module.Template).Name))" -TestCases $Modules { 124 | Param( $Module ) 125 | 126 | $allParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 127 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 128 | | Sort-Object -Property Name ` 129 | | ForEach-Object Name 130 | ForEach ( $Parameter in $Module.Parameters ) { 131 | $allParametersInParametersFile = (Get-Content $Parameter ` 132 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 133 | | Sort-Object -Property Name ` 134 | | ForEach-Object Name 135 | $result = @($allParametersInParametersFile| Where-Object {$allParametersInTemplateFile -notcontains $_}); 136 | if($result) {Write-Host "Invalid parameters: $(ConvertTo-Json $result)"} 137 | @($allParametersInParametersFile| Where-Object {$allParametersInTemplateFile -notcontains $_}).Count | Should -Be 0; 138 | } 139 | } 140 | 141 | It "All required parameters in template file existing in parameters file" -TestCases $Modules { 142 | Param ($Module) 143 | 144 | $requiredParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 145 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 146 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 147 | | Sort-Object -Property Name ` 148 | | ForEach-Object Name 149 | ForEach ( $Parameter in $Module.Parameters ) { 150 | 151 | $allParametersInParametersFile = (Get-Content $Parameter ` 152 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 153 | | Sort-Object -Property Name ` 154 | | ForEach-Object Name 155 | 156 | $invalid = $requiredParametersInTemplateFile | Where-Object {$allParametersInParametersFile -notcontains $_} 157 | if ($invalid.Count -gt 0) { 158 | Write-Host "Invalid parameters: $(ConvertTo-Json $invalid)" 159 | } 160 | @($requiredParametersInTemplateFile | Where-Object {$allParametersInParametersFile -notcontains $_}).Count | Should -Be 0; 161 | } 162 | } 163 | } 164 | 165 | } 166 | #endregion -------------------------------------------------------------------------------- /Modules/ARM/WvdApplicationGroups/Parameters/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appGroupName": { 6 | "value": "remoteApp1" 7 | }, 8 | "location": { 9 | "value": "eastus" 10 | }, 11 | "appGroupType": { 12 | "value": "RemoteApp" 13 | }, 14 | "hostpoolName": { 15 | "value": "wvdv2" 16 | }, 17 | "appGroupFriendlyName": { 18 | "value": "Remote Applications 1" 19 | }, 20 | "appGroupDescription": { 21 | "value": "This is my first Remote Applications bundle" 22 | }, 23 | // "roleAssignments": { 24 | // "value": [ 25 | // { 26 | // "roleDefinitionIdOrName": "Desktop Virtualization User", 27 | // "principalIds": [ 28 | // "12345678-1234-1234-1234-123456789012", // object 1 29 | // "78945612-1234-1234-1234-123456789012" // object 2 30 | // ] 31 | // } 32 | // ] 33 | // }, 34 | "diagnosticLogsRetentionInDays": { 35 | "value": 365 36 | }, 37 | "diagnosticStorageAccountId": { 38 | "value": "" 39 | }, 40 | "workspaceId": { 41 | "value": "" 42 | }, 43 | "eventHubAuthorizationRuleId": { 44 | "value": "" 45 | }, 46 | "eventHubName": { 47 | "value": "" 48 | }, 49 | "lockForDeletion": { 50 | "value": false 51 | }, 52 | "tags": { 53 | "value": { 54 | "Environment": "Validation", 55 | "Contact": "test.user@testcompany.com", 56 | "PurchaseOrder": "1234", 57 | "CostCenter": "6789", 58 | "ServiceName": "WVD", 59 | "Role": "DeploymentValidation" 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /Modules/ARM/WvdApplicationGroups/Pipeline/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(moduleName) 2 | 3 | variables: 4 | - template: /Modules/ARM/.global/global.variables.yml 5 | - name: moduleName 6 | value: WvdApplicationGroups 7 | - name: removeDeployment 8 | value: false 9 | - name: moduleVersion 10 | value: 1.0.0 11 | - name: versionOption 12 | value: patch # major, minor, patch, or custom 13 | 14 | trigger: 15 | batch: true 16 | branches: 17 | include: 18 | - master 19 | paths: 20 | include: 21 | - Modules/ARM/.global/* 22 | - Modules/ARM/WvdApplicationGroups/* 23 | exclude: 24 | - readme.md 25 | 26 | stages: 27 | - stage: Validation 28 | jobs: 29 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.validate.yml 30 | parameters: 31 | moduleName: '$(moduleName)' 32 | resourceGroupName: '$(resourceGroupName)' 33 | modulePath: '$(modulesPath)/$(moduleName)' 34 | parameterFilePaths: 35 | - $(modulePath)/Parameters/parameters.json 36 | 37 | - stage: Deployment 38 | jobs: 39 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.deploy.yml 40 | parameters: 41 | moduleName: '$(moduleName)' 42 | resourceGroupName: '$(resourceGroupName)' 43 | modulePath: '$(modulesPath)/$(moduleName)' 44 | parameterFilePaths: 45 | - $(modulePath)/Parameters/parameters.json 46 | 47 | - stage: Publishing 48 | jobs: 49 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.publish.yml 50 | parameters: 51 | moduleName: ${{lower( variables.moduleName )}} 52 | versionOption: $(versionOption) 53 | moduleVersion: $(moduleVersion) 54 | 55 | - stage: Removal 56 | dependsOn: Deployment 57 | condition: and(succeededOrFailed(), eq( variables['removeDeployment'], 'true')) 58 | jobs: 59 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.remove.yml -------------------------------------------------------------------------------- /Modules/ARM/WvdApplicationGroups/Scripts/git_placeholder.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/Modules/ARM/WvdApplicationGroups/Scripts/git_placeholder.md -------------------------------------------------------------------------------- /Modules/ARM/WvdApplicationGroups/readme.md: -------------------------------------------------------------------------------- 1 | # WVD Application Groups 2 | 3 | This module deploys WVD Application Groups, with resource lock and diagnostics configuration. 4 | 5 | ## Resources 6 | 7 | - Microsoft.DesktopVirtualization/applicationgroups 8 | - Microsoft.DesktopVirtualization/applicationgroups/providers/diagnosticsettings 9 | - Microsoft.DesktopVirtualization/applicationgroups/providers/locks 10 | 11 | ## Parameters 12 | 13 | | Parameter Name | Type | Default Value | Possible values | Description | 14 | | :- | :- | :- | :- | :- | 15 | | `appGroupName` | string | | | Required. Name of the Application Group to create this application in. 16 | | `location` | string | `[resourceGroup().location]` | | Optional. Location for all resources. 17 | | `appGroupType` | string | "" | "RemoteApp", "Desktop" | appGroupType 18 | | `hostpoolName` | string | "" | | Required. Name of the Host Pool to be linked to this Application Group. 19 | | `appGroupFriendlyName` | string | "" | | Optional. The friendly name of the Application Group to be created. 20 | | `appGroupDescription` | string | "" | | Optional. The description of the Application Group to be created. 21 | | `roleAssignments` | array | [] | Complex structure, see below. | Optional. Array of role assignment objects that contain the 'roleDefinitionIdOrName' and 'principalIds' to define RBAC role assignments on this resource. In the roleDefinitionIdOrName attribute, you can provide either the display name of the role definition, or it's fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11' 22 | | `diagnosticLogsRetentionInDays` | int | `365` | | Optional. Specifies the number of days that logs will be kept for; a value of 0 will retain data indefinitely. 23 | | `diagnosticStorageAccountId` | string | "" | | Optional. Resource identifier of the Diagnostic Storage Account. 24 | | `workspaceId` | string | "" | | Optional. Resource identifier of Log Analytics. 25 | | `eventHubAuthorizationRuleId` | string | "" | | Optional. Resource ID of the event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to. 26 | | `eventHubName` | string | "" | | Optional. Name of the event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. 27 | | `lockForDeletion` | bool | `true` | | Optional. Switch to lock the resource from deletion. 28 | | `tags` | object | {} | Complex structure, see below. | Optional. Tags of the resource. 29 | | `cuaId` | string | "" | | Optional. Customer Usage Attribution id (GUID). This GUID must be previously registered 30 | 31 | ### Parameter Usage: `roleAssignments` 32 | 33 | ```json 34 | "roleAssignments": { 35 | "value": [ 36 | { 37 | "roleDefinitionIdOrName": "Desktop Virtualization User", 38 | "principalIds": [ 39 | "12345678-1234-1234-1234-123456789012", // object 1 40 | "78945612-1234-1234-1234-123456789012" // object 2 41 | ] 42 | }, 43 | { 44 | "roleDefinitionIdOrName": "Reader", 45 | "principalIds": [ 46 | "12345678-1234-1234-1234-123456789012", // object 1 47 | "78945612-1234-1234-1234-123456789012" // object 2 48 | ] 49 | }, 50 | { 51 | "roleDefinitionIdOrName": "/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11", 52 | "principalIds": [ 53 | "12345678-1234-1234-1234-123456789012" // object 1 54 | ] 55 | } 56 | ] 57 | } 58 | ``` 59 | 60 | ### Parameter Usage: `tags` 61 | 62 | Tag names and tag values can be provided as needed. A tag can be left without a value. 63 | 64 | ```json 65 | "tags": { 66 | "value": { 67 | "Environment": "Non-Prod", 68 | "Contact": "test.user@testcompany.com", 69 | "PurchaseOrder": "1234", 70 | "CostCenter": "7890", 71 | "ServiceName": "DeploymentValidation", 72 | "Role": "DeploymentValidation" 73 | } 74 | } 75 | ``` 76 | 77 | ## Outputs 78 | 79 | | Output Name | Description | 80 | | :- | :- | 81 | | `appGroupResourceId` | The Resource ID of the Application Group deployed. | 82 | | `appGroupResourceGroup` | The name of the Resource Group the WVD Application Group was created in. | 83 | | `appGroupName` | The Name of the Application Group. | 84 | 85 | ## Considerations 86 | 87 | *N/A* 88 | 89 | ## Additional resources 90 | 91 | - [What is Windows Virtual Desktop?](https://docs.microsoft.com/en-us/azure/virtual-desktop/overview) 92 | - [Windows Virtual Desktop environment](https://docs.microsoft.com/en-us/azure/virtual-desktop/environment-setup) 93 | - [Use tags to organize your Azure resources](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-using-tags) -------------------------------------------------------------------------------- /Modules/ARM/WvdApplications/Parameters/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "applications": { 6 | "value": [ 7 | { 8 | "name": "notepad", 9 | "description": "Notepad by ARM template", 10 | "friendlyName": "Notepad", 11 | "filePath": "C:\\Windows\\System32\\notepad.exe", 12 | "commandLineSetting": "DoNotAllow", 13 | "commandLineArguments": "", 14 | "showInPortal": true, 15 | "iconPath": "C:\\Windows\\System32\\notepad.exe", 16 | "iconIndex": 0 17 | }, 18 | { 19 | "name": "wordpad", 20 | "description": "WordPad by ARM template 2", 21 | "friendlyName": "WordPad", 22 | "filePath": "C:\\Program Files\\Windows NT\\Accessories\\wordpad.exe", 23 | "commandLineSetting": "DoNotAllow", 24 | "commandLineArguments": "", 25 | "showInPortal": true, 26 | "iconPath": "C:\\Program Files\\Windows NT\\Accessories\\wordpad.exe", 27 | "iconIndex": 0 28 | } 29 | ] 30 | }, 31 | "location": { 32 | "value": "eastus" 33 | }, 34 | "appGroupName": { 35 | "value": "remoteApp1" 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Modules/ARM/WvdApplications/Pipeline/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(moduleName) 2 | 3 | variables: 4 | - template: /Modules/ARM/.global/global.variables.yml 5 | - name: moduleName 6 | value: WvdApplications 7 | # - name: removeDeployment 8 | # value: false 9 | - name: moduleVersion 10 | value: 1.0.0 11 | - name: versionOption 12 | value: patch # major, minor, patch, or custom 13 | 14 | trigger: 15 | batch: true 16 | branches: 17 | include: 18 | - master 19 | paths: 20 | include: 21 | - Modules/ARM/.global/* 22 | - Modules/ARM/WvdApplications/* 23 | exclude: 24 | - /readme.md 25 | 26 | stages: 27 | - stage: Validation 28 | jobs: 29 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.validate.yml 30 | parameters: 31 | moduleName: '$(moduleName)' 32 | resourceGroupName: '$(resourceGroupName)' 33 | modulePath: '$(modulesPath)/$(moduleName)' 34 | parameterFilePaths: 35 | - $(modulePath)/Parameters/parameters.json 36 | 37 | - stage: Deployment 38 | jobs: 39 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.deploy.yml 40 | parameters: 41 | moduleName: '$(moduleName)' 42 | resourceGroupName: '$(resourceGroupName)' 43 | modulePath: '$(modulesPath)/$(moduleName)' 44 | parameterFilePaths: 45 | - $(modulePath)/Parameters/parameters.json 46 | 47 | - stage: Publishing 48 | jobs: 49 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.publish.yml 50 | parameters: 51 | moduleName: ${{lower( variables.moduleName )}} 52 | versionOption: $(versionOption) 53 | moduleVersion: $(moduleVersion) 54 | 55 | # - stage: Removal 56 | # dependsOn: Deployment 57 | # condition: and(succeededOrFailed(), eq( variables['removeDeployment'], 'true')) 58 | # jobs: 59 | # - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.remove.yml -------------------------------------------------------------------------------- /Modules/ARM/WvdApplications/Scripts/git_placeholder.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/Modules/ARM/WvdApplications/Scripts/git_placeholder.md -------------------------------------------------------------------------------- /Modules/ARM/WvdApplications/Tests/module.tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .NOTES 3 | ============================================================================================== 4 | Copyright(c) Microsoft Corporation. All rights reserved. 5 | 6 | File: module.tests.ps1 7 | 8 | Purpose: Pester - Test ARM Templates 9 | 10 | Version: 2.0.0.0 - 2nd June 2020 - Microsoft Consulting Services 11 | ============================================================================================== 12 | 13 | .SYNOPSIS 14 | This script contains functionality used to test ARM template synatax. 15 | 16 | .DESCRIPTION 17 | This script contains functionality used to test ARM template synatax. 18 | 19 | Deployment steps of the script are outlined below. 20 | 1) Test Template File Syntax 21 | 2) Test Parameter File Syntax 22 | 3) Test Template and Parameter File Compatibility 23 | #> 24 | 25 | #Requires -Version 7 26 | 27 | #region - Parameters 28 | $parametersLocation = 'Parameters' 29 | $script:here = Split-Path -Path $PSCommandPath -Parent 30 | $script:here = $(Get-Item $here).Parent.FullName 31 | $template = (Get-Item $here).parent.Name 32 | $script:TemplateFileTestCases = @() 33 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "*deploy.json") -Recurse | Select-Object -ExpandProperty Name) ) { 34 | $script:TemplateFileTestCases += @{ TemplateFile = $File } 35 | } 36 | $script:ParameterFileTestCases = @() 37 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("*parameters.json")) -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) ) { 38 | $script:ParameterFileTestCases += @{ ParameterFile = Join-Path -Path "$parametersLocation" -ChildPath $File } 39 | } 40 | $script:Modules = @(); 41 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "deploy.json") ) ) { 42 | $Module = [PSCustomObject]@{ 43 | 'Template' = $null 44 | 'Parameters' = $null 45 | } 46 | $Module.Template = $File.FullName 47 | $Parameters = @() 48 | ForEach ( $ParameterFile in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("*parameters.json")) -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) ) { 49 | $Parameters += (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("$ParameterFile") ) 50 | } 51 | $Module.Parameters = $Parameters 52 | $script:Modules += @{ Module = $Module } 53 | } 54 | #endregion 55 | 56 | #region - Run Pester Test Script 57 | Describe "Template: $template" -Tags Unit { 58 | 59 | Context "Template File Syntax" { 60 | 61 | It "JSON template file (deploy.json) exists" { 62 | (Join-Path -Path "$here" -ChildPath "deploy.json") | Should -Exist 63 | } 64 | 65 | It "Template file (deploy.json) converts from JSON and has all expected properties" -TestCases $TemplateFileTestCases { 66 | Param ($TemplateFile) 67 | $expectedProperties = '$schema', 68 | 'contentVersion', 69 | 'parameters', 70 | 'variables', 71 | 'resources', 72 | 'functions', 73 | 'outputs'| Sort-Object 74 | $templateProperties = (Get-Content (Join-Path -Path "$here" -ChildPath "$TemplateFile") ` 75 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 76 | | Get-Member -MemberType NoteProperty ` 77 | | Sort-Object -Property Name ` 78 | | ForEach-Object Name 79 | $templateProperties | Should -Be $expectedProperties 80 | } 81 | } 82 | 83 | Context "Parameter File Syntax" { 84 | 85 | It "Parameter file ($ParameterFile) does contain all expected properties" -TestCases $ParameterFileTestCases { 86 | Param ($ParameterFile) 87 | $expectedProperties = '$schema', 88 | 'contentVersion', 89 | 'parameters' | Sort-Object 90 | $templateFileProperties = (Get-Content (Join-Path -Path "$here" -ChildPath "$ParameterFile") ` 91 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 92 | | Get-Member -MemberType NoteProperty ` 93 | | Sort-Object -Property Name ` 94 | | ForEach-Object Name 95 | $templateFileProperties | Should -Be $expectedProperties 96 | } 97 | } 98 | 99 | Context "Template and Parameter Compatibility" { 100 | 101 | It "Count of required parameters in template file ($((Get-Item $Module.Template).Name)) is equal or less than count of all parameters in parameters file ($((Get-Item $Module.Parameters).Name))" -TestCases $Modules { 102 | Param ($Module) 103 | 104 | $requiredParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 105 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 106 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 107 | | Sort-Object -Property Name ` 108 | | ForEach-Object Name 109 | ForEach ( $Parameter in $Module.Parameters ) { 110 | $allParametersInParametersFile = (Get-Content $Parameter ` 111 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 112 | | Sort-Object -Property Name ` 113 | | ForEach-Object Name 114 | if ($requiredParametersInTemplateFile.Count -gt $allParametersInParametersFile.Count) { 115 | Write-Host "Mismatch found, parameters from parameter file are more than the expected in the template" 116 | Write-Host "Required parameters are: $(ConvertTo-Json $requiredParametersInTemplateFile)" 117 | Write-Host "Parameters from parameter file are: $(ConvertTo-Json $allParametersInParametersFile)" 118 | } 119 | $requiredParametersInTemplateFile.Count | Should -Not -BeGreaterThan $allParametersInParametersFile.Count; 120 | } 121 | } 122 | 123 | It "All parameters in parameters file ($((Get-Item $Module.Parameters).Name)) exist in template file ($((Get-Item $Module.Template).Name))" -TestCases $Modules { 124 | Param( $Module ) 125 | 126 | $allParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 127 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 128 | | Sort-Object -Property Name ` 129 | | ForEach-Object Name 130 | ForEach ( $Parameter in $Module.Parameters ) { 131 | $allParametersInParametersFile = (Get-Content $Parameter ` 132 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 133 | | Sort-Object -Property Name ` 134 | | ForEach-Object Name 135 | $result = @($allParametersInParametersFile| Where-Object {$allParametersInTemplateFile -notcontains $_}); 136 | if($result) {Write-Host "Invalid parameters: $(ConvertTo-Json $result)"} 137 | @($allParametersInParametersFile| Where-Object {$allParametersInTemplateFile -notcontains $_}).Count | Should -Be 0; 138 | } 139 | } 140 | 141 | It "All required parameters in template file existing in parameters file" -TestCases $Modules { 142 | Param ($Module) 143 | 144 | $requiredParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 145 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 146 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 147 | | Sort-Object -Property Name ` 148 | | ForEach-Object Name 149 | ForEach ( $Parameter in $Module.Parameters ) { 150 | 151 | $allParametersInParametersFile = (Get-Content $Parameter ` 152 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 153 | | Sort-Object -Property Name ` 154 | | ForEach-Object Name 155 | 156 | $invalid = $requiredParametersInTemplateFile | Where-Object {$allParametersInParametersFile -notcontains $_} 157 | if ($invalid.Count -gt 0) { 158 | Write-Host "Invalid parameters: $(ConvertTo-Json $invalid)" 159 | } 160 | @($requiredParametersInTemplateFile | Where-Object {$allParametersInParametersFile -notcontains $_}).Count | Should -Be 0; 161 | } 162 | } 163 | } 164 | 165 | } 166 | #endregion -------------------------------------------------------------------------------- /Modules/ARM/WvdApplications/deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "applications": { 6 | "type": "array", 7 | "minLength": 1, 8 | "metadata": { 9 | "description": "Required. List of applications to be created in the Application Group." 10 | } 11 | }, 12 | "location": { 13 | "type": "string", 14 | "defaultValue": "[resourceGroup().location]", 15 | "metadata": { 16 | "description": "Optional. Location for all resources." 17 | } 18 | }, 19 | "appGroupName": { 20 | "type": "string", 21 | "minLength": 1, 22 | "metadata": { 23 | "description": "Required. Name of the Application Group to create the application(s) in." 24 | } 25 | }, 26 | "cuaId": { 27 | "type": "string", 28 | "defaultValue": "", 29 | "metadata": { 30 | "description": "Optional. Customer Usage Attribution id (GUID). This GUID must be previously registered" 31 | } 32 | } 33 | }, 34 | "variables": { 35 | }, 36 | "resources": [ 37 | { 38 | "condition": "[not(empty(parameters('cuaId')))]", 39 | "type": "Microsoft.Resources/deployments", 40 | "apiVersion": "2018-02-01", 41 | "name": "[concat('pid-', parameters('cuaId'))]", 42 | "properties": { 43 | "mode": "Incremental", 44 | "template": { 45 | "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", 46 | "contentVersion": "1.0.0.0", 47 | "resources": [ 48 | ] 49 | } 50 | } 51 | }, 52 | { 53 | "type": "Microsoft.DesktopVirtualization/applicationGroups/applications", 54 | "apiVersion": "2019-12-10-preview", 55 | "copy": { 56 | "name": "appCopy", 57 | "count": "[length(parameters('applications'))]" 58 | }, 59 | "name": "[concat(parameters('appGroupName'), '/', parameters('applications')[copyIndex()].name)]", 60 | "location": "[parameters('location')]", 61 | "properties": { 62 | "description": "[parameters('applications')[copyIndex()].description]", 63 | "friendlyName": "[parameters('applications')[copyIndex()].friendlyName]", 64 | "filePath": "[parameters('applications')[copyIndex()].filePath]", 65 | "commandLineSetting": "[parameters('applications')[copyIndex()].commandLineSetting]", 66 | "commandLineArguments": "[parameters('applications')[copyIndex()].commandLineArguments]", 67 | "showInPortal": "[parameters('applications')[copyIndex()].showInPortal]", 68 | "iconPath": "[parameters('applications')[copyIndex()].iconPath]", 69 | "iconIndex": "[parameters('applications')[copyIndex()].iconIndex]" 70 | } 71 | } 72 | ], 73 | "functions": [ 74 | ], 75 | "outputs": { 76 | "applicationResourceIds": { 77 | "type": "array", 78 | "metadata": { 79 | "description": "The list of the application resourceIds deployed." 80 | }, 81 | "copy": { 82 | "count": "[length(parameters('applications'))]", 83 | "input": "[resourceId('Microsoft.DesktopVirtualization/applicationGroups/applications', parameters('appGroupName'), parameters('applications')[copyIndex()].name)]" 84 | } 85 | }, 86 | "applicationResourceGroup": { 87 | "type": "string", 88 | "value": "[resourceGroup().name]", 89 | "metadata": { 90 | "description": "The name of the Resource Group the WVD Applications were created in." 91 | } 92 | }, 93 | "appGroupName": { 94 | "type": "string", 95 | "value": "[parameters('appGroupName')]", 96 | "metadata": { 97 | "description": "The Name of the Application Group to register the Application(s) in." 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Modules/ARM/WvdApplications/readme.md: -------------------------------------------------------------------------------- 1 | # WVD Applications 2 | 3 | This module deploys WVD Applications. 4 | 5 | ## Resources 6 | 7 | - Microsoft.DesktopVirtualization/applicationGroups/applications 8 | 9 | ## Parameters 10 | 11 | | Parameter Name | Type | Default Value | Possible values | Description | 12 | | :- | :- | :- | :- | :- | 13 | | `applications` | array | {} | Complex structure, see below. | Required. List of applications to be created in the Application Group. 14 | | `location` | string | `[resourceGroup().location]` | | Optional. Location for all resources. 15 | | `appGroupName` | string | "" | | Required. Name of the Application Group to create the application(s) in. 16 | | `cuaId` | string | "" | | Optional. Customer Usage Attribution id (GUID). This GUID must be previously registered 17 | 18 | ### Parameter Usage: `applications` 19 | 20 | ```json 21 | "applications": { 22 | "value": [ 23 | { 24 | "name": "notepad", 25 | "description": "Notepad by ARM template", 26 | "friendlyName": "Notepad", 27 | "filePath": "C:\\Windows\\System32\\notepad.exe", 28 | "commandLineSetting": "DoNotAllow", 29 | "commandLineArguments": "", 30 | "showInPortal": true, 31 | "iconPath": "C:\\Windows\\System32\\notepad.exe", 32 | "iconIndex": 0 33 | }, 34 | { 35 | "name": "wordpad", 36 | "description": "WordPad by ARM template 2", 37 | "friendlyName": "WordPad", 38 | "filePath": "C:\\Program Files\\Windows NT\\Accessories\\wordpad.exe", 39 | "commandLineSetting": "DoNotAllow", 40 | "commandLineArguments": "", 41 | "showInPortal": true, 42 | "iconPath": "C:\\Program Files\\Windows NT\\Accessories\\wordpad.exe", 43 | "iconIndex": 0 44 | } 45 | ] 46 | } 47 | 48 | ## Outputs 49 | 50 | | Output Name | Description | 51 | | :- | :- | 52 | | `applicationResourceIds` | The list of the application resourceIds deployed. | 53 | | `applicationResourceGroup` | The name of the Resource Group the WVD Applications were created in. | 54 | | `appGroupName` | The Name of the Application Group to register the Application(s) in. | 55 | 56 | 57 | ## Considerations 58 | 59 | *N/A* 60 | 61 | ## Additional resources 62 | 63 | - [What is Windows Virtual Desktop?](https://docs.microsoft.com/en-us/azure/virtual-desktop/overview) 64 | - [Windows Virtual Desktop environment](https://docs.microsoft.com/en-us/azure/virtual-desktop/environment-setup) 65 | -------------------------------------------------------------------------------- /Modules/ARM/WvdHostPools/Parameters/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "hostpoolName": { 6 | "value": "wvdv2" 7 | }, 8 | "location": { 9 | "value": "eastus" 10 | }, 11 | "hostpoolFriendlyName": { 12 | "value": "WVDv2" 13 | }, 14 | "hostpoolDescription": { 15 | "value": "My first WVD Host Pool" 16 | }, 17 | "hostpoolType": { 18 | "value": "Pooled" 19 | }, 20 | "personalDesktopAssignmentType": { 21 | "value": "Automatic" 22 | }, 23 | "maxSessionLimit": { 24 | "value": 99999 25 | }, 26 | "loadBalancerType": { 27 | "value": "BreadthFirst" 28 | }, 29 | "customRdpProperty": { 30 | "value": "audiocapturemode:i:1;audiomode:i:0;drivestoredirect:s:;redirectclipboard:i:1;redirectcomports:i:1;redirectprinters:i:1;redirectsmartcards:i:1;screen mode id:i:2;" 31 | }, 32 | "vmTemplate": { 33 | "value": { 34 | "domain": "domainname.onmicrosoft.com", 35 | "galleryImageOffer": "office-365", 36 | "galleryImagePublisher": "microsoftwindowsdesktop", 37 | "galleryImageSKU": "20h1-evd-o365pp", 38 | "imageType": "Gallery", 39 | "imageUri": null, 40 | "customImageId": null, 41 | "namePrefix": "wvdv2", 42 | "osDiskType": "StandardSSD_LRS", 43 | "useManagedDisks": true, 44 | "vmSize": { 45 | "id": "Standard_D2s_v3", 46 | "cores": 2, 47 | "ram": 8 48 | } 49 | } 50 | }, 51 | "validationEnviroment": { 52 | "value": false 53 | }, 54 | "diagnosticLogsRetentionInDays": { 55 | "value": 365 56 | }, 57 | "diagnosticStorageAccountId": { 58 | "value": "" 59 | }, 60 | "workspaceId": { 61 | "value": "" 62 | }, 63 | "eventHubAuthorizationRuleId": { 64 | "value": "" 65 | }, 66 | "eventHubName": { 67 | "value": "" 68 | }, 69 | "lockForDeletion": { 70 | "value": false 71 | }, 72 | "tags": { 73 | "value": { 74 | "Environment": "Validation", 75 | "Contact": "test.user@testcompany.com", 76 | "PurchaseOrder": "1234", 77 | "CostCenter": "6789", 78 | "ServiceName": "WVD", 79 | "Role": "DeploymentValidation" 80 | } 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /Modules/ARM/WvdHostPools/Pipeline/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(moduleName) 2 | 3 | variables: 4 | - template: /Modules/ARM/.global/global.variables.yml 5 | - name: moduleName 6 | value: WvdHostPools 7 | - name: removeDeployment 8 | value: false 9 | - name: moduleVersion 10 | value: 1.0.1 11 | - name: versionOption 12 | value: patch # major, minor, patch, or custom 13 | 14 | trigger: 15 | batch: true 16 | branches: 17 | include: 18 | - master 19 | paths: 20 | include: 21 | - Modules/ARM/.global/* 22 | - Modules/ARM/WvdHostPools/* 23 | exclude: 24 | - /readme.md 25 | 26 | stages: 27 | - stage: Validation 28 | jobs: 29 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.validate.yml 30 | parameters: 31 | moduleName: '$(moduleName)' 32 | resourceGroupName: '$(resourceGroupName)' 33 | modulePath: '$(modulesPath)/$(moduleName)' 34 | parameterFilePaths: 35 | - $(modulePath)/Parameters/parameters.json 36 | 37 | - stage: Deployment 38 | jobs: 39 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.deploy.yml 40 | parameters: 41 | moduleName: '$(moduleName)' 42 | resourceGroupName: '$(resourceGroupName)' 43 | modulePath: '$(modulesPath)/$(moduleName)' 44 | parameterFilePaths: 45 | - $(modulePath)/Parameters/parameters.json 46 | 47 | - stage: Publishing 48 | jobs: 49 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.publish.yml 50 | parameters: 51 | moduleName: ${{lower( variables.moduleName )}} 52 | versionOption: $(versionOption) 53 | moduleVersion: $(moduleVersion) 54 | 55 | - stage: Removal 56 | dependsOn: Deployment 57 | condition: and(succeededOrFailed(), eq( variables['removeDeployment'], 'true')) 58 | jobs: 59 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.remove.yml -------------------------------------------------------------------------------- /Modules/ARM/WvdHostPools/Scripts/git_placeholder.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/Modules/ARM/WvdHostPools/Scripts/git_placeholder.md -------------------------------------------------------------------------------- /Modules/ARM/WvdHostPools/Tests/module.tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .NOTES 3 | ============================================================================================== 4 | Copyright(c) Microsoft Corporation. All rights reserved. 5 | 6 | File: module.tests.ps1 7 | 8 | Purpose: Pester - Test ARM Templates 9 | 10 | Version: 2.0.0.0 - 2nd June 2020 - Microsoft Consulting Services 11 | ============================================================================================== 12 | 13 | .SYNOPSIS 14 | This script contains functionality used to test ARM template synatax. 15 | 16 | .DESCRIPTION 17 | This script contains functionality used to test ARM template synatax. 18 | 19 | Deployment steps of the script are outlined below. 20 | 1) Test Template File Syntax 21 | 2) Test Parameter File Syntax 22 | 3) Test Template and Parameter File Compatibility 23 | #> 24 | 25 | #Requires -Version 7 26 | 27 | #region - Parameters 28 | $parametersLocation = 'Parameters' 29 | $script:here = Split-Path -Path $PSCommandPath -Parent 30 | $script:here = $(Get-Item $here).Parent.FullName 31 | $template = (Get-Item $here).parent.Name 32 | $script:TemplateFileTestCases = @() 33 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "*deploy.json") -Recurse | Select-Object -ExpandProperty Name) ) { 34 | $script:TemplateFileTestCases += @{ TemplateFile = $File } 35 | } 36 | $script:ParameterFileTestCases = @() 37 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("*parameters.json")) -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) ) { 38 | $script:ParameterFileTestCases += @{ ParameterFile = Join-Path -Path "$parametersLocation" -ChildPath $File } 39 | } 40 | $script:Modules = @(); 41 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "deploy.json") ) ) { 42 | $Module = [PSCustomObject]@{ 43 | 'Template' = $null 44 | 'Parameters' = $null 45 | } 46 | $Module.Template = $File.FullName 47 | $Parameters = @() 48 | ForEach ( $ParameterFile in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("*parameters.json")) -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) ) { 49 | $Parameters += (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("$ParameterFile") ) 50 | } 51 | $Module.Parameters = $Parameters 52 | $script:Modules += @{ Module = $Module } 53 | } 54 | #endregion 55 | 56 | #region - Run Pester Test Script 57 | Describe "Template: $template" -Tags Unit { 58 | 59 | Context "Template File Syntax" { 60 | 61 | It "JSON template file (deploy.json) exists" { 62 | (Join-Path -Path "$here" -ChildPath "deploy.json") | Should -Exist 63 | } 64 | 65 | It "Template file (deploy.json) converts from JSON and has all expected properties" -TestCases $TemplateFileTestCases { 66 | Param ($TemplateFile) 67 | $expectedProperties = '$schema', 68 | 'contentVersion', 69 | 'parameters', 70 | 'variables', 71 | 'resources', 72 | 'functions', 73 | 'outputs'| Sort-Object 74 | $templateProperties = (Get-Content (Join-Path -Path "$here" -ChildPath "$TemplateFile") ` 75 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 76 | | Get-Member -MemberType NoteProperty ` 77 | | Sort-Object -Property Name ` 78 | | ForEach-Object Name 79 | $templateProperties | Should -Be $expectedProperties 80 | } 81 | } 82 | 83 | Context "Parameter File Syntax" { 84 | 85 | It "Parameter file ($ParameterFile) does contain all expected properties" -TestCases $ParameterFileTestCases { 86 | Param ($ParameterFile) 87 | $expectedProperties = '$schema', 88 | 'contentVersion', 89 | 'parameters' | Sort-Object 90 | $templateFileProperties = (Get-Content (Join-Path -Path "$here" -ChildPath "$ParameterFile") ` 91 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 92 | | Get-Member -MemberType NoteProperty ` 93 | | Sort-Object -Property Name ` 94 | | ForEach-Object Name 95 | $templateFileProperties | Should -Be $expectedProperties 96 | } 97 | } 98 | 99 | Context "Template and Parameter Compatibility" { 100 | 101 | It "Count of required parameters in template file ($((Get-Item $Module.Template).Name)) is equal or less than count of all parameters in parameters file ($((Get-Item $Module.Parameters).Name))" -TestCases $Modules { 102 | Param ($Module) 103 | 104 | $requiredParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 105 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 106 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 107 | | Sort-Object -Property Name ` 108 | | ForEach-Object Name 109 | ForEach ( $Parameter in $Module.Parameters ) { 110 | $allParametersInParametersFile = (Get-Content $Parameter ` 111 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 112 | | Sort-Object -Property Name ` 113 | | ForEach-Object Name 114 | if ($requiredParametersInTemplateFile.Count -gt $allParametersInParametersFile.Count) { 115 | Write-Host "Mismatch found, parameters from parameter file are more than the expected in the template" 116 | Write-Host "Required parameters are: $(ConvertTo-Json $requiredParametersInTemplateFile)" 117 | Write-Host "Parameters from parameter file are: $(ConvertTo-Json $allParametersInParametersFile)" 118 | } 119 | $requiredParametersInTemplateFile.Count | Should -Not -BeGreaterThan $allParametersInParametersFile.Count; 120 | } 121 | } 122 | 123 | It "All parameters in parameters file ($((Get-Item $Module.Parameters).Name)) exist in template file ($((Get-Item $Module.Template).Name))" -TestCases $Modules { 124 | Param( $Module ) 125 | 126 | $allParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 127 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 128 | | Sort-Object -Property Name ` 129 | | ForEach-Object Name 130 | ForEach ( $Parameter in $Module.Parameters ) { 131 | $allParametersInParametersFile = (Get-Content $Parameter ` 132 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 133 | | Sort-Object -Property Name ` 134 | | ForEach-Object Name 135 | $result = @($allParametersInParametersFile| Where-Object {$allParametersInTemplateFile -notcontains $_}); 136 | if($result) {Write-Host "Invalid parameters: $(ConvertTo-Json $result)"} 137 | @($allParametersInParametersFile| Where-Object {$allParametersInTemplateFile -notcontains $_}).Count | Should -Be 0; 138 | } 139 | } 140 | 141 | It "All required parameters in template file existing in parameters file" -TestCases $Modules { 142 | Param ($Module) 143 | 144 | $requiredParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 145 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 146 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 147 | | Sort-Object -Property Name ` 148 | | ForEach-Object Name 149 | ForEach ( $Parameter in $Module.Parameters ) { 150 | 151 | $allParametersInParametersFile = (Get-Content $Parameter ` 152 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 153 | | Sort-Object -Property Name ` 154 | | ForEach-Object Name 155 | 156 | $invalid = $requiredParametersInTemplateFile | Where-Object {$allParametersInParametersFile -notcontains $_} 157 | if ($invalid.Count -gt 0) { 158 | Write-Host "Invalid parameters: $(ConvertTo-Json $invalid)" 159 | } 160 | @($requiredParametersInTemplateFile | Where-Object {$allParametersInParametersFile -notcontains $_}).Count | Should -Be 0; 161 | } 162 | } 163 | } 164 | 165 | } 166 | #endregion -------------------------------------------------------------------------------- /Modules/ARM/WvdHostPools/readme.md: -------------------------------------------------------------------------------- 1 | # WVD HostPools 2 | 3 | This module deploys WVD Host Pools, with resource lock and diagnostics configuration. 4 | 5 | ## Resources 6 | 7 | - Microsoft.DesktopVirtualization/hostpools 8 | - Microsoft.DesktopVirtualization/hostpools/providers/diagnosticsettings 9 | - Microsoft.DesktopVirtualization/hostpools/providers/locks 10 | 11 | ## Parameters 12 | 13 | | Parameter Name | Type | Default Value | Possible values | Description | 14 | | :- | :- | :- | :- | :- | 15 | | `hostPoolName` | string | | | Required. Name of the Host Pool 16 | | `location` | string | `[resourceGroup().location]` | | Optional. Location for all resources. 17 | | `hostpoolFriendlyName` | string | "" | | Optional. The friendly name of the Host Pool to be created. 18 | | `hostpoolDescription` | string | "" | | Optional. The description of the Host Pool to be created. 19 | | `hostpoolType` | string | `Pooled` | "Personal", "Pooled" | Optional. Set this parameter to Personal if you would like to enable Persistent Desktop experience. Defaults to Pooled. 20 | | `personalDesktopAssignmentType` | string | "" | "Automatic", "Direct", "" | Optional. Set the type of assignment for a Personal Host Pool type 21 | | `loadBalancerType` | string | `true` | "BreadthFirst", "DepthFirst", "Persistent" | Optional. Type of load balancer algorithm. 22 | | `maxSessionLimit` | int | `99999` | | Optional. Maximum number of sessions. | 23 | | `customRdpProperty` | string | `audiocapturemode:i:1; audiomode:i:0; drivestoredirect:s:; redirectclipboard:i:1; redirectcomports:i:1; redirectprinters:i:1; redirectsmartcards:i:1; screen mode id:i:2;` | [Supported Remote desktop RDP file settings](https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/rdp-files?context=/azure/virtual-desktop/context/context) | Optional. Host Pool RDP properties 24 | | `validationEnviroment` | bool | `false` | | Optional. Whether to use validation enviroment. When set to true, the Host Pool will be deployed in a validation 'ring' (environment) that receives all the new features (might be less stable). Ddefaults to false that stands for the stable, production-ready environment. 25 | | `vmTemplate` | object | {} | Complex structure, see below. | Optional. The necessary information for adding more VMs to this Host Pool 26 | | `tokenValidityLength` | string | `PT8H` | Duration in ISO 8601 format. E.g. PT8H, P1Y, P5D | Optional. Host Pool token validity length. Usage: 'PT8H' - valid for 8 hours; 'P5D' - valid for 5 days; 'P1Y' - valid for 1 year. When not provided, the token will be valid for 8 hours. 27 | | `baseTime` | string | `utcNow('u')` | | Generated. Do not provide a value! This date value is used to generate a registration token. 28 | | `diagnosticLogsRetentionInDays` | int | `365` | | Optional. Specifies the number of days that logs will be kept for; a value of 0 will retain data indefinitely. 29 | | `diagnosticStorageAccountId` | string | "" | | Optional. Resource identifier of the Diagnostic Storage Account. 30 | | `workspaceId` | string | "" | | Optional. Resource identifier of Log Analytics. 31 | | `eventHubAuthorizationRuleId` | string | "" | | Optional. Resource ID of the event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to. 32 | | `eventHubName` | string | "" | | Optional. Name of the event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. 33 | | `lockForDeletion` | bool | `true` | | Optional. Switch to lock the resource from deletion. 34 | | `tags` | object | {} | Complex structure, see below. | Optional. Tags of the resource. 35 | | `cuaId` | string | "" | | Optional. Customer Usage Attribution id (GUID). This GUID must be previously registered 36 | 37 | ### Parameter Usage: `vmTemplate` 38 | 39 | The below parameter object is converted to an in-line string when handed over to the resource deployment, since that only takes strings. 40 | 41 | ```json 42 | "vmTemplate": { 43 | "value": { 44 | "domain": ".com", 45 | "galleryImageOffer": "office-365", 46 | "galleryImagePublisher": "microsoftwindowsdesktop", 47 | "galleryImageSKU": "19h2-evd-o365pp", 48 | "imageType": "Gallery", 49 | "imageUri": null, 50 | "customImageId": null, 51 | "namePrefix": "wvdv2", 52 | "osDiskType": "StandardSSD_LRS", 53 | "useManagedDisks": true, 54 | "vmSize": { 55 | "id": "Standard_D2s_v3", 56 | "cores": 2, 57 | "ram": 8 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | ### Parameter Usage: `customRdpProperty` 64 | 65 | ```json 66 | "customRdpProperty": { 67 | "value": "audiocapturemode:i:1;audiomode:i:0;drivestoredirect:s:;redirectclipboard:i:1;redirectcomports:i:1;redirectprinters:i:1;redirectsmartcards:i:1;screen mode id:i:2;" 68 | } 69 | ``` 70 | 71 | ### Parameter Usage: `tags` 72 | 73 | Tag names and tag values can be provided as needed. A tag can be left without a value. 74 | 75 | ```json 76 | "tags": { 77 | "value": { 78 | "Environment": "Non-Prod", 79 | "Contact": "test.user@testcompany.com", 80 | "PurchaseOrder": "1234", 81 | "CostCenter": "7890", 82 | "ServiceName": "DeploymentValidation", 83 | "Role": "DeploymentValidation" 84 | } 85 | } 86 | ``` 87 | 88 | ## Outputs 89 | 90 | | Output Name | Description | 91 | | :- | :- | 92 | | `hostPoolResourceId` | The Resource Id of the Host Pool. | 93 | | `hostPoolResourceGroup` | The name of the Resource Group the Host Pool was created in. | 94 | | `hostPoolName` | The Name of the Host Pool. | 95 | | `tokenExpirationTime` | The expiration time of the Host Pool registration token. | 96 | | `hostpoolToken` | The token that has to be used to register a VM to the Host Pool. | 97 | 98 | ## Considerations 99 | 100 | *N/A* 101 | 102 | ## Additional resources 103 | 104 | - [What is Windows Virtual Desktop?](https://docs.microsoft.com/en-us/azure/virtual-desktop/overview) 105 | - [Windows Virtual Desktop environment](https://docs.microsoft.com/en-us/azure/virtual-desktop/environment-setup) 106 | - [Use tags to organize your Azure resources](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-using-tags) -------------------------------------------------------------------------------- /Modules/ARM/WvdWorkspaces/Parameters/parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "workSpaceName": { 6 | "value": "wvdWorkspace1" 7 | }, 8 | "location": { 9 | "value": "eastus" 10 | }, 11 | "appGroupResourceIds": { 12 | "value": [ 13 | "/subscriptions/62826c76-d304-46d8-a0f6-718dbdcc536c/resourceGroups/Validation-RG/providers/Microsoft.DesktopVirtualization/applicationgroups/remoteApp1" 14 | ] 15 | }, 16 | "workspaceFriendlyName": { 17 | "value": "My first WVD Workspace" 18 | }, 19 | "workspaceDescription": { 20 | "value": "This is my first WVD Workspace" 21 | }, 22 | "diagnosticLogsRetentionInDays": { 23 | "value": 365 24 | }, 25 | "diagnosticStorageAccountId": { 26 | "value": "" 27 | }, 28 | "workspaceId": { 29 | "value": "" 30 | }, 31 | "eventHubAuthorizationRuleId": { 32 | "value": "" 33 | }, 34 | "eventHubName": { 35 | "value": "" 36 | }, 37 | "lockForDeletion": { 38 | "value": false 39 | }, 40 | "tags": { 41 | "value": { 42 | "Environment": "Validation", 43 | "Contact": "test.user@testcompany.com", 44 | "PurchaseOrder": "1234", 45 | "CostCenter": "6789", 46 | "ServiceName": "WVD", 47 | "Role": "DeploymentValidation" 48 | } 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Modules/ARM/WvdWorkspaces/Pipeline/pipeline.yml: -------------------------------------------------------------------------------- 1 | name: $(moduleName) 2 | 3 | variables: 4 | - template: /Modules/ARM/.global/global.variables.yml 5 | - name: moduleName 6 | value: WvdWorkspaces 7 | - name: removeDeployment 8 | value: false 9 | - name: moduleVersion 10 | value: 1.0.1 11 | - name: versionOption 12 | value: patch # major, minor, patch, or custom 13 | 14 | trigger: 15 | batch: true 16 | branches: 17 | include: 18 | - master 19 | paths: 20 | include: 21 | - Modules/ARM/.global/* 22 | - Modules/ARM/WvdWorkspaces/* 23 | exclude: 24 | - /readme.md 25 | 26 | stages: 27 | - stage: Validation 28 | jobs: 29 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.validate.yml 30 | parameters: 31 | moduleName: '$(moduleName)' 32 | resourceGroupName: '$(resourceGroupName)' 33 | modulePath: '$(modulesPath)/$(moduleName)' 34 | parameterFilePaths: 35 | - $(modulePath)/Parameters/parameters.json 36 | 37 | - stage: Deployment 38 | jobs: 39 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.deploy.yml 40 | parameters: 41 | moduleName: '$(moduleName)' 42 | resourceGroupName: '$(resourceGroupName)' 43 | modulePath: '$(modulesPath)/$(moduleName)' 44 | parameterFilePaths: 45 | - $(modulePath)/Parameters/parameters.json 46 | 47 | - stage: Publishing 48 | jobs: 49 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.publish.yml 50 | parameters: 51 | moduleName: ${{lower( variables.moduleName )}} 52 | versionOption: $(versionOption) 53 | moduleVersion: $(moduleVersion) 54 | 55 | - stage: Removal 56 | dependsOn: Deployment 57 | condition: and(succeededOrFailed(), eq( variables['removeDeployment'], 'true')) 58 | jobs: 59 | - template: /Modules/ARM/.global/PipelineTemplates/pipeline.jobs.remove.yml -------------------------------------------------------------------------------- /Modules/ARM/WvdWorkspaces/Scripts/git_placeholder.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/Modules/ARM/WvdWorkspaces/Scripts/git_placeholder.md -------------------------------------------------------------------------------- /Modules/ARM/WvdWorkspaces/Tests/module.tests.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .NOTES 3 | ============================================================================================== 4 | Copyright(c) Microsoft Corporation. All rights reserved. 5 | 6 | File: module.tests.ps1 7 | 8 | Purpose: Pester - Test ARM Templates 9 | 10 | Version: 2.0.0.0 - 2nd June 2020 - Microsoft Consulting Services 11 | ============================================================================================== 12 | 13 | .SYNOPSIS 14 | This script contains functionality used to test ARM template synatax. 15 | 16 | .DESCRIPTION 17 | This script contains functionality used to test ARM template synatax. 18 | 19 | Deployment steps of the script are outlined below. 20 | 1) Test Template File Syntax 21 | 2) Test Parameter File Syntax 22 | 3) Test Template and Parameter File Compatibility 23 | #> 24 | 25 | #Requires -Version 7 26 | 27 | #region - Parameters 28 | $parametersLocation = 'Parameters' 29 | $script:here = Split-Path -Path $PSCommandPath -Parent 30 | $script:here = $(Get-Item $here).Parent.FullName 31 | $template = (Get-Item $here).parent.Name 32 | $script:TemplateFileTestCases = @() 33 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "*deploy.json") -Recurse | Select-Object -ExpandProperty Name) ) { 34 | $script:TemplateFileTestCases += @{ TemplateFile = $File } 35 | } 36 | $script:ParameterFileTestCases = @() 37 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("*parameters.json")) -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) ) { 38 | $script:ParameterFileTestCases += @{ ParameterFile = Join-Path -Path "$parametersLocation" -ChildPath $File } 39 | } 40 | $script:Modules = @(); 41 | ForEach ( $File in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "deploy.json") ) ) { 42 | $Module = [PSCustomObject]@{ 43 | 'Template' = $null 44 | 'Parameters' = $null 45 | } 46 | $Module.Template = $File.FullName 47 | $Parameters = @() 48 | ForEach ( $ParameterFile in (Get-ChildItem (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("*parameters.json")) -Recurse -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) ) { 49 | $Parameters += (Join-Path -Path "$here" -ChildPath "$parametersLocation" -AdditionalChildPath @("$ParameterFile") ) 50 | } 51 | $Module.Parameters = $Parameters 52 | $script:Modules += @{ Module = $Module } 53 | } 54 | #endregion 55 | 56 | #region - Run Pester Test Script 57 | Describe "Template: $template" -Tags Unit { 58 | 59 | Context "Template File Syntax" { 60 | 61 | It "JSON template file (deploy.json) exists" { 62 | (Join-Path -Path "$here" -ChildPath "deploy.json") | Should -Exist 63 | } 64 | 65 | It "Template file (deploy.json) converts from JSON and has all expected properties" -TestCases $TemplateFileTestCases { 66 | Param ($TemplateFile) 67 | $expectedProperties = '$schema', 68 | 'contentVersion', 69 | 'parameters', 70 | 'variables', 71 | 'resources', 72 | 'functions', 73 | 'outputs'| Sort-Object 74 | $templateProperties = (Get-Content (Join-Path -Path "$here" -ChildPath "$TemplateFile") ` 75 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 76 | | Get-Member -MemberType NoteProperty ` 77 | | Sort-Object -Property Name ` 78 | | ForEach-Object Name 79 | $templateProperties | Should -Be $expectedProperties 80 | } 81 | } 82 | 83 | Context "Parameter File Syntax" { 84 | 85 | It "Parameter file ($ParameterFile) does contain all expected properties" -TestCases $ParameterFileTestCases { 86 | Param ($ParameterFile) 87 | $expectedProperties = '$schema', 88 | 'contentVersion', 89 | 'parameters' | Sort-Object 90 | $templateFileProperties = (Get-Content (Join-Path -Path "$here" -ChildPath "$ParameterFile") ` 91 | | ConvertFrom-Json -ErrorAction SilentlyContinue) ` 92 | | Get-Member -MemberType NoteProperty ` 93 | | Sort-Object -Property Name ` 94 | | ForEach-Object Name 95 | $templateFileProperties | Should -Be $expectedProperties 96 | } 97 | } 98 | 99 | Context "Template and Parameter Compatibility" { 100 | 101 | It "Count of required parameters in template file ($((Get-Item $Module.Template).Name)) is equal or less than count of all parameters in parameters file ($((Get-Item $Module.Parameters).Name))" -TestCases $Modules { 102 | Param ($Module) 103 | 104 | $requiredParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 105 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 106 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 107 | | Sort-Object -Property Name ` 108 | | ForEach-Object Name 109 | ForEach ( $Parameter in $Module.Parameters ) { 110 | $allParametersInParametersFile = (Get-Content $Parameter ` 111 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 112 | | Sort-Object -Property Name ` 113 | | ForEach-Object Name 114 | if ($requiredParametersInTemplateFile.Count -gt $allParametersInParametersFile.Count) { 115 | Write-Host "Mismatch found, parameters from parameter file are more than the expected in the template" 116 | Write-Host "Required parameters are: $(ConvertTo-Json $requiredParametersInTemplateFile)" 117 | Write-Host "Parameters from parameter file are: $(ConvertTo-Json $allParametersInParametersFile)" 118 | } 119 | $requiredParametersInTemplateFile.Count | Should -Not -BeGreaterThan $allParametersInParametersFile.Count; 120 | } 121 | } 122 | 123 | It "All parameters in parameters file ($((Get-Item $Module.Parameters).Name)) exist in template file ($((Get-Item $Module.Template).Name))" -TestCases $Modules { 124 | Param( $Module ) 125 | 126 | $allParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 127 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 128 | | Sort-Object -Property Name ` 129 | | ForEach-Object Name 130 | ForEach ( $Parameter in $Module.Parameters ) { 131 | $allParametersInParametersFile = (Get-Content $Parameter ` 132 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 133 | | Sort-Object -Property Name ` 134 | | ForEach-Object Name 135 | $result = @($allParametersInParametersFile| Where-Object {$allParametersInTemplateFile -notcontains $_}); 136 | if($result) {Write-Host "Invalid parameters: $(ConvertTo-Json $result)"} 137 | @($allParametersInParametersFile| Where-Object {$allParametersInTemplateFile -notcontains $_}).Count | Should -Be 0; 138 | } 139 | } 140 | 141 | It "All required parameters in template file existing in parameters file" -TestCases $Modules { 142 | Param ($Module) 143 | 144 | $requiredParametersInTemplateFile = (Get-Content "$($Module.Template)" ` 145 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 146 | | Where-Object -FilterScript { -not ($_.Value.PSObject.Properties.Name -eq "defaultValue") } ` 147 | | Sort-Object -Property Name ` 148 | | ForEach-Object Name 149 | ForEach ( $Parameter in $Module.Parameters ) { 150 | 151 | $allParametersInParametersFile = (Get-Content $Parameter ` 152 | | ConvertFrom-Json -ErrorAction SilentlyContinue).Parameters.PSObject.Properties ` 153 | | Sort-Object -Property Name ` 154 | | ForEach-Object Name 155 | 156 | $invalid = $requiredParametersInTemplateFile | Where-Object {$allParametersInParametersFile -notcontains $_} 157 | if ($invalid.Count -gt 0) { 158 | Write-Host "Invalid parameters: $(ConvertTo-Json $invalid)" 159 | } 160 | @($requiredParametersInTemplateFile | Where-Object {$allParametersInParametersFile -notcontains $_}).Count | Should -Be 0; 161 | } 162 | } 163 | } 164 | 165 | } 166 | #endregion -------------------------------------------------------------------------------- /Modules/ARM/WvdWorkspaces/readme.md: -------------------------------------------------------------------------------- 1 | # WVD Workspaces 2 | 3 | This module deploys WVD Workspaces, with resource lock and diagnostic configuration. 4 | 5 | ## Resources 6 | 7 | - Microsoft.DesktopVirtualization/workspaces 8 | - Microsoft.DesktopVirtualization/workspaces/providers/diagnosticsettings 9 | - Microsoft.DesktopVirtualization/workspaces/providers/locks 10 | 11 | ## Parameters 12 | 13 | | Parameter Name | Type | Default Value | Possible values | Description | 14 | | :- | :- | :- | :- | :- | 15 | | `workSpaceName` | string | | | Required. The name of the Workspace to be attach to new Application Group. 16 | | `location` | string | `[resourceGroup().location]` | | Optional. Location for all resources. 17 | | `appGroupResourceIds` | array | [] | | Required. Resource IDs fo the existing Application groups this workspace will group together. 18 | | `workspaceFriendlyName` | string | "" | | Optional. The friendly name of the Workspace to be created. 19 | | `workspaceDescription` | string | "" | | Optional. The description of the Workspace to be created. 20 | | `diagnosticLogsRetentionInDays` | int | `365` | | Optional. Specifies the number of days that logs will be kept for; a value of 0 will retain data indefinitely. 21 | | `diagnosticStorageAccountId` | string | "" | | Optional. Resource identifier of the Diagnostic Storage Account. 22 | | `workspaceId` | string | "" | | Optional. Resource identifier of Log Analytics. 23 | | `eventHubAuthorizationRuleId` | string | "" | | Optional. Resource ID of the event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to. 24 | | `eventHubName` | string | "" | | Optional. Name of the event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. 25 | | `lockForDeletion` | bool | `true` | | Optional. Switch to lock the resource from deletion. 26 | | `tags` | object | {} | Complex structure, see below. | Optional. Tags of the resource. 27 | | `cuaId` | string | "" | | Optional. Customer Usage Attribution id (GUID). This GUID must be previously registered 28 | 29 | ### Parameter Usage: `tags` 30 | 31 | Tag names and tag values can be provided as needed. A tag can be left without a value. 32 | 33 | ```json 34 | "tags": { 35 | "value": { 36 | "Environment": "Non-Prod", 37 | "Contact": "test.user@testcompany.com", 38 | "PurchaseOrder": "1234", 39 | "CostCenter": "7890", 40 | "ServiceName": "DeploymentValidation", 41 | "Role": "DeploymentValidation" 42 | } 43 | } 44 | ``` 45 | 46 | ## Outputs 47 | 48 | | Output Name | Description | 49 | | :- | :- | 50 | | `workspaceResourceId` | The Resource Id of the WVD Workspace. | 51 | | `workspaceResourceGroup` | The name of the Resource Group the WVD Workspace was created in. | 52 | | `workspaceName` | The Name of the Workspace. | 53 | 54 | ## Considerations 55 | 56 | *N/A* 57 | 58 | ## Additional resources 59 | 60 | - [What is Windows Virtual Desktop?](https://docs.microsoft.com/en-us/azure/virtual-desktop/overview) 61 | - [Windows Virtual Desktop environment](https://docs.microsoft.com/en-us/azure/virtual-desktop/environment-setup) 62 | - [Use tags to organize your Azure resources](https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-using-tags) -------------------------------------------------------------------------------- /NewSubAADDSSetup/README.md: -------------------------------------------------------------------------------- 1 | The deploy.json file in this folder can be used when starting from a new or empty Azure subscription. This ARM template will setup an Azure AD DS managed domain before deploying WVD through a DevOps automation pipeline. For more information, check out https://www.wvdquickstart.com/howtoEmpty 2 | 3 | 4 | 5 |
6 | -------------------------------------------------------------------------------- /QS-WVD/Parameters/DoNotRemove.md: -------------------------------------------------------------------------------- 1 | This file is a required placeholder that should not be removed. 2 | -------------------------------------------------------------------------------- /QS-WVD/Scripts/Invoke-StorageAccountPostDeployment.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Run the Post-Deployment for the storage account deployment 4 | 5 | .DESCRIPTION 6 | Run the Post-Deployment for the storage account deployment 7 | - Upload required data to the storage account 8 | 9 | .PARAMETER orchestrationFunctionsPath 10 | Mandatory. Path to the required functions 11 | 12 | .PARAMETER storageAccountName 13 | Mandatory. Name of the storage account to host the deployment files 14 | 15 | .PARAMETER Confirm 16 | Will promt user to confirm the action to create invasible commands 17 | 18 | .PARAMETER WhatIf 19 | Dry run of the script 20 | 21 | .EXAMPLE 22 | Invoke-StorageAccountPostDeployment -orchestrationFunctionsPath $currentDir -storageAccountName "wvdStorageAccount" 23 | 24 | Upload any required data to the storage account 25 | #> 26 | function Invoke-StorageAccountPostDeployment { 27 | 28 | [CmdletBinding(SupportsShouldProcess = $true)] 29 | param( 30 | [Parameter(Mandatory = $true)] 31 | [string] $orchestrationFunctionsPath, 32 | 33 | [Parameter(Mandatory = $true)] 34 | [string] $wvdUploadsPath, 35 | 36 | [Parameter(Mandatory = $true)] 37 | [string] $storageAccountName 38 | ) 39 | 40 | begin { 41 | Write-Verbose ("[{0} entered]" -f $MyInvocation.MyCommand) 42 | . "$orchestrationFunctionsPath\Storage\Import-WVDSoftware.ps1" 43 | . "$orchestrationFunctionsPath\Storage\Compress-WVDCSEContent.ps1" 44 | . "$orchestrationFunctionsPath\Storage\Export-WVDCSEContentToBlob.ps1" 45 | } 46 | 47 | process { 48 | 49 | Write-Verbose "###########################################" 50 | Write-Verbose "## 1 - Download software from public url ##" 51 | Write-Verbose "###########################################" 52 | 53 | Write-Verbose("#####################") 54 | Write-Verbose("## 1.1 - LOAD DATA ##") 55 | Write-Verbose("#####################") 56 | $ConfigurationFilePath = (Join-Path "$wvdUploadsPath/WVDScripts" "downloads.parameters.json") 57 | $ConfigurationJson = Get-Content -Path $ConfigurationFilePath -Raw -ErrorAction 'Stop' 58 | 59 | try { $Downloads = $ConfigurationJson | ConvertFrom-Json -ErrorAction 'Stop' } 60 | catch { 61 | Write-Error "Configuration JSON content could not be converted to a PowerShell object" -ErrorAction 'Stop' 62 | } 63 | 64 | Write-Verbose("####################") 65 | Write-Verbose("## 1.2 - EVALUATE ##") 66 | Write-Verbose("####################") 67 | foreach ($download in $Downloads.WVDSoftware) { 68 | $InputObject = @{ 69 | Url = $download.Url 70 | FileName = (Join-Path "$wvdUploadsPath/WVDScripts" $download.DestinationFilePath) 71 | } 72 | Write-Verbose $InputObject 73 | 74 | if ($PSCmdlet.ShouldProcess("Required executable files to be installed on WVD VMs", "Import")) { 75 | Import-WVDSoftware @InputObject -Verbose 76 | Write-Verbose "WVD Software download invocation finished" 77 | } 78 | } 79 | Write-Verbose "######################################################################################################" 80 | Write-Verbose "## 2 - Create zip files for all WVDScripts subfolders and save them to the WVDCSEZipToUpload folder ##" 81 | Write-Verbose "######################################################################################################" 82 | 83 | $InputObject = @{ 84 | SourceFolderPath = "$wvdUploadsPath/WVDScripts" 85 | DestinationFolderPath = "$wvdUploadsPath/WVDCSEZipToUpload" 86 | } 87 | if ($PSCmdlet.ShouldProcess("$wvdUploadsPath/WVDScripts subfolders as .zip and store them into $wvdUploadsPath/WVDCSEZipToUpload", "Compress")) { 88 | Compress-WVDCSEContent @InputObject -Verbose 89 | Write-Verbose "WVD CSE for VMs compression finished" 90 | } 91 | 92 | Write-Verbose "###################################" 93 | Write-Verbose "## 3 - Upload to storage account ##" 94 | Write-Verbose "###################################" 95 | 96 | $InputObject = @{ 97 | ResourceGroupName = (Get-AzResource -Name $storageAccountName -ResourceType 'Microsoft.Storage/storageAccounts').ResourceGroupName 98 | StorageAccountName = $storageAccountName 99 | } 100 | if ($PSCmdlet.ShouldProcess("Required storage content for storage account '$storageAccountName'", "Export")) { 101 | Export-WVDCSEContentToBlob @InputObject -Verbose 102 | Write-Verbose "Storage account content upload invocation finished" 103 | } 104 | 105 | } 106 | end { 107 | Write-Verbose ("[{0} existed]" -f $MyInvocation.MyCommand) 108 | } 109 | } -------------------------------------------------------------------------------- /QS-WVD/Scripts/New-PipelineParameterSetup.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Generate the deployment parameter files required by the WVD deployment using the provided values 4 | 5 | .DESCRIPTION 6 | The files are generated from the templates in the "templateFolderPath". 7 | Every value defined as [KeyWord] is replaced with a token of the .psd1 file store in the "parameterSourcePath" 8 | The resulting files are stored in the given path with the same name as the templates, but without the ".template" in their name 9 | 10 | .PARAMETER templateFolderPath 11 | The path to the templates folder 12 | Can contain any time of file with '[keyword]' tokens 13 | 14 | .PARAMETER parameterSourcePath 15 | The path to the file containing the values for the given keywords (must be a .psd1 file) 16 | 17 | .PARAMETER targetFolderPath 18 | The folder to store the resulting parameter files in. 19 | 20 | .PARAMETER templateSeachPattern 21 | The pattern to select the desired template files with. Default is "*" 22 | 23 | .PARAMETER Confirm 24 | Will promt user to confirm the action to create invasible commands 25 | 26 | .PARAMETER WhatIf 27 | Dry run of the script 28 | 29 | .EXAMPLE 30 | New-WVDPreReqPipelineParameterSetup -targetFolderPath 'C:\dev\ip\WVD-Automation\WVDPreReq\bin' 31 | 32 | Generates the requried tokenized parameter files in path 'C:\dev\ip\WVD-Automation\WVDPreReq\bin' 33 | 34 | .EXAMPLE 35 | New-WVDPreReqPipelineParameterSetup -targetFolderPath 'C:\dev\ip\WVD-Automation\WVDPreReq\bin' -templateSeachPattern "wvd*" 36 | 37 | Generates the requried tokenized parameter files matching the file pattern 'wvd*' in path 'C:\dev\ip\WVD-Automation\WVDPreReq\bin' 38 | #> 39 | function New-PipelineParameterSetup { 40 | 41 | [CmdletBinding(SupportsShouldProcess = $true)] 42 | param( 43 | [Parameter(Mandatory = $false)] 44 | [string] $templateFolderPath = (Join-Path (Split-Path $PSScriptRoot -Parent) "static\templates\pipelineInput"), 45 | 46 | [Parameter(Mandatory = $false)] 47 | [string] $parameterSourcePath = (Join-Path (Split-Path $PSScriptRoot -Parent) "static\appliedParameters.psd1"), 48 | 49 | [Parameter(Mandatory = $false)] 50 | [string] $targetFolderPath = (Join-Path (Split-Path $PSScriptRoot -Parent) "Parameters"), 51 | 52 | [Parameter(Mandatory = $false)] 53 | [string] $templateSeachPattern = "*" 54 | ) 55 | 56 | Write-Verbose "Load parameters file from '$parameterSourcePath'" 57 | $parametersObject = Import-PowerShellDataFile -Path $parameterSourcePath 58 | 59 | Write-Verbose "Load templates from '$templateFolderPath'" 60 | $templatePaths = Get-ChildItem "$templateFolderPath\*" -Include $templateSeachPattern | ForEach-Object { $_.FullName } 61 | foreach ($templatePath in $templatePaths) { 62 | 63 | Write-Verbose "Load template from '$templatePath'" 64 | $content = Get-Content -Path $templatePath 65 | 66 | Write-Verbose "Replace tokens" 67 | foreach ($key in $parametersObject.Keys) { 68 | if ($parametersObject[$key] -is [string]) { 69 | $content = $content.Replace("[$key]", $parametersObject[$key]) 70 | } elseif ($parametersObject[$key] -is [bool]) { 71 | # Required for e.g. bool 72 | $content = $content.Replace(('"[{0}]"' -f $key), $parametersObject[$key].ToString().ToLower()) 73 | } 74 | else { 75 | # Required for e.g. integer 76 | $content = $content.Replace(('"[{0}]"' -f $key), $parametersObject[$key]) 77 | } 78 | } 79 | 80 | $fileName = (Split-Path -Path $templatePath -Leaf).Replace('.template', '') 81 | if ($PSCmdlet.ShouldProcess("Parameter file '$fileName' to '$targetFolderPath'", "Store")) { 82 | $targetPath = Join-Path $targetFolderPath $fileName 83 | if (-not (Test-Path $targetPath)) { 84 | Write-Verbose "Generate file '$targetPath'" 85 | New-Item -ItemType File -Path $targetPath 86 | } 87 | Set-Content -Value $content -Path "$targetFolderPath\$fileName" -Force 88 | } 89 | } 90 | } -------------------------------------------------------------------------------- /QS-WVD/static/appliedParameters.template.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | # General Information # 3 | # =================== # 4 | # Environment 5 | subscriptionId = "[subscriptionId]" # Azure Subscription Id 6 | tenantId = "[tenantId]" # Azure Active Directory Tenant Id 7 | objectId = "[objectId]" # Object Id of the serviceprincipal, found in Azure Active Directory / App registrations 8 | 9 | # ResourceGroups 10 | location = "[location]" # Location in which WVD resources will be deployed 11 | resourceGroupName = "[resourceGroupName]" # Name of the resource group in which WVD resources will be deployed 12 | ####################### 13 | 14 | # Key Vault related # 15 | # ================= # 16 | keyVaultName = "[keyVaultName]" # Name of the keyvault where the admin password is stored as secret 17 | AdminPasswordSecret = "adminPassword" # Default, name of the secret in the keyvault 18 | ##################### 19 | 20 | # Storage related # 21 | # =============== # 22 | wvdAssetsStorage = "[assetsName]" # Name of assets storage account 23 | profilesStorageAccountName = "[profilesName]" # Name of the profiles storage account 24 | storageAccountSku = "Standard_LRS" # default, storage account SKU 25 | profilesShareName = "wvdprofiles" # Name of the file share in the profiles storage account where profiles will be stored 26 | ################### 27 | 28 | # Host pool related # 29 | # ================== # 30 | hostpoolName = "QS-WVD-HP" # Name of the WVD host pool 31 | hostpoolType = "Pooled" # Type of host pool, can be "Personal" or "Pooled" (default) 32 | maxSessionLimit = 16 # default 33 | loadBalancerType = "BreadthFirst" # Load-balancing algorithm 34 | vmNamePrefix = "QS-WVD-VM" # Prefix for the WVD VMs that will be deployed 35 | vmSize = "Standard_D2s_v3" # The VM SKU 36 | vmNumberOfInstances = 2 # Number of VMs to be deployed 37 | vmInitialNumber = 1 # default 38 | diskSizeGB = 128 # Size of the VMs' disk 39 | vmDiskType = "Premium_LRS" # SKU of the above disk 40 | domainJoinUser = "[DomainJoinAccountUPN]" # The domain join account UPN 41 | domainName = "[existingDomainName]" # domain for the VMs to join, taken from domainJoinUser 42 | adminUsername = "[existingDomainUsername]" # domain controller admin username, taken from domainJoinUser 43 | computerName = "[computerName]" # The name of the VM with the domain controller on it. Required only when using AD Identity Approach. 44 | vnetName = "[existingVnetName]" # Name of the virtual network with the domain controller 45 | vnetResourceGroupName = "[virtualNetworkResourceGroupName]" # Name of the resource group with the domain controller VM and VNET in it 46 | subnetName = "[existingSubnetName]" # Name of the subnet for the VMs to join 47 | enablePersistentDesktop = $false # WVD setting 48 | ###################### 49 | 50 | # App group related # 51 | # ================== # 52 | appGroupName = "QS-WVD-RAG" # Remote app group name 53 | DesktopAppGroupName = "QS-WVD-DAG" # Desktop app group name 54 | targetGroup = "[targetGroup]" # Name of the user group to be assigned to the WVD environment. Only change to an existing group as group is created only in the initial ARM deployment. 55 | principalIds = "[principalIds]" # principal ID of the above test user group 56 | workSpaceName = "QS-WVD-WS" # Name of the WVD workspace 57 | workspaceFriendlyName = "WVD Workspace" # User-facing friendly name of the above workspace 58 | ###################### 59 | 60 | # Imaging related # 61 | # ================ # 62 | imagingResourceGroupName = "QS-WVD-IMG-RG" # [Not used, can be used for custom imaging] 63 | imageTemplateName = "QS-WVD-ImageTemplate" # [Not used, can be used for custom imaging] 64 | imagingMSItt = "[imagingMSItt]" # [Not used, can be used for custom imaging] 65 | sigGalleryName = "[sigGalleryName]" # [Not used, can be used for custom imaging] 66 | sigImageDefinitionId = "" # [Not used, can be used for custom imaging] 67 | imageDefinitionName = "W10-20H1-O365" # [Not used, can be used for custom imaging] 68 | osType = "Windows" # default 69 | publisher = "microsoftwindowsdesktop" # default 70 | offer = "office-365" # This image includes Office 365 71 | sku = "20h1-evd-o365pp" # Points to Windows 10 Enterprise Multi-Session, build 2004 72 | imageVersion = "latest" # default 73 | ###################### 74 | 75 | # Authentication related 76 | # ==================== # 77 | identityApproach = "[identityApproach]" # (AD or Azure AD DS) identity approach to use 78 | } 79 | -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/azfiles.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "azfilesconfig": [ 3 | { 4 | "enableAzureFiles": true, 5 | "identityApproach": "[identityApproach]", 6 | "SubscriptionId": "[subscriptionId]", 7 | "ResourceGroupName": "[resourceGroupName]", 8 | "StorageAccountName": "[profilesStorageAccountName]", 9 | "domainName": "[domainName]", 10 | "domainJoinUsername": "[adminUsername]" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/fslogix.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "fslogix": [ 3 | { 4 | "installFSLogix": true, 5 | "configureFSLogix": true, 6 | "profileContainerKeys": [ 7 | { 8 | "Name": "Enabled", 9 | "Type": "DWORD", 10 | "Value": "1" 11 | }, 12 | { 13 | "Name": "VHDLocations", 14 | "Type": "MultiString", 15 | "Value": "\\\\[profilesStorageAccountName].file.core.windows.net\\[profilesShareName]" 16 | } 17 | ], 18 | "NTFSPermission": true, 19 | "fileShareName": "[profilesShareName]", 20 | "fileShareStorageAccountName": "[profilesStorageAccountName]", 21 | "domain": "[domainName]", 22 | "targetGroup": "[targetGroup]" 23 | } 24 | ] 25 | } -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/keyvault.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "keyVaultName": { 6 | "value": "[keyVaultName]" 7 | }, 8 | "accessPolicies": { 9 | "value": [ 10 | { 11 | "tenantId": "[tenantId]", 12 | "objectId": "[objectId]", 13 | "permissions": { 14 | "keys": [ 15 | "All" 16 | ], 17 | "secrets": [ 18 | "All" 19 | ], 20 | "certificates": [ 21 | "All" 22 | ] 23 | } 24 | } 25 | ] 26 | }, 27 | "secretsObject": { 28 | "value": { 29 | "secrets": [] 30 | } 31 | }, 32 | "enableVaultForDeployment": { 33 | "value": true 34 | }, 35 | "enableVaultForDiskEncryption": { 36 | "value": true 37 | }, 38 | "enableVaultForTemplateDeployment": { 39 | "value": true 40 | }, 41 | "vaultSku": { 42 | "value": "Standard" 43 | }, 44 | "diagnosticLogsRetentionInDays": { 45 | "value": 365 46 | }, 47 | "lockForDeletion": { 48 | "value": false 49 | }, 50 | "tags": { 51 | "value": {} 52 | }, 53 | "enableSoftDelete": { 54 | "value": false 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/storageaccount.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "storageAccountName": { 6 | "value": "[wvdAssetsStorage]" 7 | }, 8 | "storageAccountKind": { 9 | "value": "StorageV2" 10 | }, 11 | "storageAccountSku": { 12 | "value": "[storageAccountSku]" 13 | }, 14 | "storageAccountAccessTier": { 15 | "value": "Hot" 16 | }, 17 | "lockForDeletion": { 18 | "value": false 19 | }, 20 | "blobContainers": { 21 | "value": [ 22 | { 23 | "name": "wvdscripts", 24 | "publicAccess": "Container", 25 | "roleAssignments": [] 26 | } 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/wvdapplication.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "applications": { 6 | "value": [ 7 | { 8 | "name": "NotepadPP", 9 | "description": "Notepad++ by ARM template", 10 | "friendlyName": "NotepadPP", 11 | "filePath": "C:\\Program Files (x86)\\Notepad++\\Notepad++.exe", 12 | "commandLineSetting": "DoNotAllow", 13 | "commandLineArguments": "", 14 | "showInPortal": true, 15 | "iconPath": "C:\\Program Files (x86)\\Notepad++\\Notepad++.exe", 16 | "iconIndex": 0 17 | }, 18 | { 19 | "name": "Microsoft Teams", 20 | "description": "Microsoft Teams by ARM template", 21 | "friendlyName": "Microsoft Teams", 22 | "filePath": "C:\\Program Files (x86)\\Teams Installer\\Teams.exe", 23 | "commandLineSetting": "DoNotAllow", 24 | "commandLineArguments": "", 25 | "showInPortal": true, 26 | "iconPath": "C:\\Program Files (x86)\\Teams Installer\\Teams.exe", 27 | "iconIndex": 0 28 | } 29 | ] 30 | }, 31 | "location": { 32 | "value": "[location]" 33 | }, 34 | "appGroupName": { 35 | "value": "[appGroupName]" 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/wvdapplicationgroup01.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appGroupName": { 6 | "value": "[appGroupName]" 7 | }, 8 | "location": { 9 | "value": "[location]" 10 | }, 11 | "appGroupType": { 12 | "value": "RemoteApp" 13 | }, 14 | "hostpoolName": { 15 | "value": "[hostpoolName]" 16 | }, 17 | "appGroupFriendlyName": { 18 | "value": "" 19 | }, 20 | "appGroupDescription": { 21 | "value": "" 22 | }, 23 | "roleAssignments": { 24 | "value": [ 25 | { 26 | "roleDefinitionIdOrName": "Desktop Virtualization User", 27 | "principalIds": [ 28 | "[principalIds]" 29 | ] 30 | } 31 | ] 32 | }, 33 | "diagnosticLogsRetentionInDays": { 34 | "value": 365 35 | }, 36 | "diagnosticStorageAccountId": { 37 | "value": "" 38 | }, 39 | "workspaceId": { 40 | "value": "" 41 | }, 42 | "eventHubAuthorizationRuleId": { 43 | "value": "" 44 | }, 45 | "eventHubName": { 46 | "value": "" 47 | }, 48 | "lockForDeletion": { 49 | "value": false 50 | }, 51 | "tags": { 52 | "value": {} 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/wvddesktoppapplicationgroup.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "appGroupName": { 6 | "value": "[DesktopAppGroupName]" 7 | }, 8 | "location": { 9 | "value": "[location]" 10 | }, 11 | "appGroupType": { 12 | "value": "Desktop" 13 | }, 14 | "hostpoolName": { 15 | "value": "[hostpoolName]" 16 | }, 17 | "appGroupFriendlyName": { 18 | "value": "" 19 | }, 20 | "appGroupDescription": { 21 | "value": "" 22 | }, 23 | "roleAssignments": { 24 | "value": [ 25 | { 26 | "roleDefinitionIdOrName": "Desktop Virtualization User", 27 | "principalIds": [ 28 | "[principalIds]" 29 | ] 30 | } 31 | ] 32 | }, 33 | "diagnosticLogsRetentionInDays": { 34 | "value": 365 35 | }, 36 | "diagnosticStorageAccountId": { 37 | "value": "" 38 | }, 39 | "workspaceId": { 40 | "value": "" 41 | }, 42 | "eventHubAuthorizationRuleId": { 43 | "value": "" 44 | }, 45 | "eventHubName": { 46 | "value": "" 47 | }, 48 | "lockForDeletion": { 49 | "value": false 50 | }, 51 | "tags": { 52 | "value": {} 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/wvdhostpool.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "hostpoolName": { 6 | "value": "[hostpoolName]" 7 | }, 8 | "location": { 9 | "value": "[location]" 10 | }, 11 | "hostpoolFriendlyName": { 12 | "value": "" 13 | }, 14 | "hostpoolDescription": { 15 | "value": "" 16 | }, 17 | "hostpoolType": { 18 | "value": "[hostpoolType]" 19 | }, 20 | "personalDesktopAssignmentType": { 21 | "value": "" 22 | }, 23 | "maxSessionLimit": { 24 | "value": 16 25 | }, 26 | "loadBalancerType": { 27 | "value": "[loadBalancerType]" 28 | }, 29 | "customRdpProperty": { 30 | "value": "audiocapturemode:i:1;audiomode:i:0;drivestoredirect:s:;redirectclipboard:i:1;redirectcomports:i:1;redirectprinters:i:1;redirectsmartcards:i:1;screen mode id:i:2;" 31 | }, 32 | "vmTemplate": { 33 | "value": {} 34 | }, 35 | "validationEnviroment": { 36 | "value": false 37 | }, 38 | "diagnosticLogsRetentionInDays": { 39 | "value": 365 40 | }, 41 | "diagnosticStorageAccountId": { 42 | "value": "" 43 | }, 44 | "workspaceId": { 45 | "value": "" 46 | }, 47 | "eventHubAuthorizationRuleId": { 48 | "value": "" 49 | }, 50 | "eventHubName": { 51 | "value": "" 52 | }, 53 | "lockForDeletion": { 54 | "value": false 55 | }, 56 | "tags": { 57 | "value": { } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/wvdprofiles-storageaccount-01.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "storageAccountName": { 6 | "value": "[profilesStorageAccountName]" 7 | }, 8 | "storageAccountKind": { 9 | "value": "StorageV2" 10 | }, 11 | "storageAccountSku": { 12 | "value": "[storageAccountSku]" 13 | }, 14 | "storageAccountAccessTier": { 15 | "value": "Hot" 16 | }, 17 | "fileShares": { 18 | "value": [ 19 | { 20 | "name": "[profilesShareName]", 21 | "shareQuota": "5120", 22 | "roleAssignments": [ 23 | { 24 | "roleDefinitionIdOrName": "Storage File Data SMB Share Contributor", 25 | "principalIds": [ 26 | "[principalIds]" 27 | ] 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/wvdsessionhost.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vmNamePrefix": { 6 | "value": "[vmNamePrefix]" 7 | }, 8 | "vmNumberOfInstances": { 9 | "value": 2 10 | }, 11 | "vmInitialNumber": { 12 | "value": 1 13 | }, 14 | "vmSize": { 15 | "value": "[vmSize]" 16 | }, 17 | "imageReference": { 18 | "value": { 19 | "id": "", 20 | "publisher": "[publisher]", 21 | "offer": "[offer]", 22 | "sku": "[sku]", 23 | "version": "[imageVersion]" 24 | } 25 | }, 26 | "osDisk": { 27 | "value": { 28 | "createOption": "fromImage", 29 | "diskSizeGB": "[diskSizeGB]", 30 | "managedDisk": { 31 | "storageAccountType": "[vmDiskType]" 32 | } 33 | } 34 | }, 35 | "adminUsername": { 36 | "value": "[adminUsername]" 37 | }, 38 | "adminPassword": { 39 | "reference": { 40 | "keyVault": { 41 | "id": "/subscriptions/[subscriptionId]/resourceGroups/[resourceGroupName]/providers/Microsoft.KeyVault/vaults/[keyVaultName]" 42 | }, 43 | "secretName": "[AdminPasswordSecret]" 44 | } 45 | }, 46 | "availabilitySetName": { 47 | "value": "" 48 | }, 49 | "subnetId": { 50 | "value": "subscriptions/[subscriptionId]/resourceGroups/[vnetResourceGroupName]/providers/Microsoft.Network/virtualNetworks/[vnetName]/subnets/[subnetName]" 51 | }, 52 | "domainName": { 53 | "value": "[domainName]" 54 | }, 55 | "domainJoinUser": { 56 | "value": "[domainJoinUser]" 57 | }, 58 | "domainJoinPassword": { 59 | "reference": { 60 | "keyVault": { 61 | "id": "/subscriptions/[subscriptionId]/resourceGroups/[resourceGroupName]/providers/Microsoft.KeyVault/vaults/[keyVaultName]" 62 | }, 63 | "secretName": "[AdminPasswordSecret]" 64 | } 65 | }, 66 | "domainJoinOU": { 67 | "value": "" 68 | }, 69 | "dscConfiguration": { 70 | "value": { 71 | "settings": { 72 | "wmfVersion": "latest", 73 | "configuration": { 74 | "url": "https://github.com/stgeorgi/wvdquickstart/raw/master/Uploads/Configuration.zip", 75 | "script": "Configuration.ps1", 76 | "function": "AddSessionHost" 77 | }, 78 | "configurationArguments": { 79 | "hostPoolName": "[hostpoolName]" 80 | } 81 | }, 82 | "protectedSettings": { 83 | "configurationArguments": { 84 | "registrationInfoToken": "" 85 | } 86 | } 87 | } 88 | }, 89 | "enablePublicIP": { 90 | "value": false 91 | }, 92 | "diagnosticLogsRetentionInDays": { 93 | "value": 365 94 | }, 95 | "diagnosticStorageAccountId": { 96 | "value": "" 97 | }, 98 | "workspaceId": { 99 | "value": "" 100 | }, 101 | "eventHubAuthorizationRuleId": { 102 | "value": "" 103 | }, 104 | "eventHubName": { 105 | "value": "" 106 | }, 107 | "lockForDeletion": { 108 | "value": false 109 | }, 110 | "tags": { 111 | "value": {} 112 | }, 113 | "windowsScriptExtensionFileData": { 114 | "value": [ 115 | { 116 | "uri": "https://[wvdAssetsStorage].blob.core.windows.net/wvdscripts/scriptExtensionMasterInstaller.ps1", 117 | "storageAccountId": "/subscriptions/[subscriptionId]/resourceGroups/[resourceGroupName]/providers/Microsoft.Storage/storageAccounts/[wvdAssetsStorage]" 118 | }, 119 | { 120 | "uri": "https://[wvdAssetsStorage].blob.core.windows.net/wvdscripts/001-AzFiles.zip", 121 | "storageAccountId": "/subscriptions/[subscriptionId]/resourceGroups/[resourceGroupName]/providers/Microsoft.Storage/storageAccounts/[wvdAssetsStorage]" 122 | }, 123 | { 124 | "uri": "https://[wvdAssetsStorage].blob.core.windows.net/wvdscripts/002-FSLogix.zip", 125 | "storageAccountId": "/subscriptions/[subscriptionId]/resourceGroups/[resourceGroupName]/providers/Microsoft.Storage/storageAccounts/[wvdAssetsStorage]" 126 | }, 127 | { 128 | "uri": "https://[wvdAssetsStorage].blob.core.windows.net/wvdscripts/003-NotepadPP.zip", 129 | "storageAccountId": "/subscriptions/[subscriptionId]/resourceGroups/[resourceGroupName]/providers/Microsoft.Storage/storageAccounts/[wvdAssetsStorage]" 130 | }, 131 | { 132 | "uri": "https://[wvdAssetsStorage].blob.core.windows.net/wvdscripts/004-Teams.zip", 133 | "storageAccountId": "/subscriptions/[subscriptionId]/resourceGroups/[resourceGroupName]/providers/Microsoft.Storage/storageAccounts/[wvdAssetsStorage]" 134 | } 135 | ] 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /QS-WVD/static/templates/pipelineInput/wvdworkspace.parameters.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "workSpaceName": { 6 | "value": "[workSpaceName]" 7 | }, 8 | "location": { 9 | "value": "[location]" 10 | }, 11 | "appGroupResourceIds": { 12 | "value": [ 13 | "/subscriptions/[subscriptionId]/resourceGroups/[resourceGroupName]/providers/Microsoft.DesktopVirtualization/applicationgroups/[DesktopAppGroupName]", 14 | "/subscriptions/[subscriptionId]/resourceGroups/[resourceGroupName]/providers/Microsoft.DesktopVirtualization/applicationgroups/[appGroupName]" 15 | ] 16 | }, 17 | "workspaceFriendlyName": { 18 | "value": "[workspaceFriendlyName]" 19 | }, 20 | "workspaceDescription": { 21 | "value": "" 22 | }, 23 | "diagnosticLogsRetentionInDays": { 24 | "value": 365 25 | }, 26 | "diagnosticStorageAccountId": { 27 | "value": "" 28 | }, 29 | "workspaceId": { 30 | "value": "" 31 | }, 32 | "eventHubAuthorizationRuleId": { 33 | "value": "" 34 | }, 35 | "eventHubName": { 36 | "value": "" 37 | }, 38 | "lockForDeletion": { 39 | "value": false 40 | }, 41 | "tags": { 42 | "value": { } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /QS-WVD/variables.template.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | 3 | ############# 4 | ## GENERAL ## 5 | ############# 6 | #region general 7 | 8 | - name: orchestrationFunctionsPath # Name of folder where some functions are located 9 | value: SharedDeploymentFunctions 10 | 11 | - name: vmImage # Image of agent used in DevOps pipeline 12 | value: "ubuntu-latest" 13 | 14 | - name: serviceConnection # Name of the service connection between the Azure subscription and DevOps 15 | value: "WVDServiceConnection" 16 | 17 | - name: location 18 | value: "[location]" 19 | 20 | #endregion 21 | 22 | ####################### 23 | ## PIPELINE SPECIFIC ## 24 | ####################### 25 | #region specific 26 | 27 | # Jobs 28 | - name: enableJobDeployAssetsStorageAccount # To enable/disable job 29 | value: true 30 | 31 | - name: parameterFolderPath 32 | value: 'QS-WVD' 33 | 34 | ### Key Vault ### 35 | - group: "WVDSecrets" # Name of DevOps variable group name 36 | 37 | - name: domainJoinUserName 38 | value: "[adminUsername]" 39 | 40 | ### Storage Account ### 41 | - name: wvdAssetsStorage 42 | value: "[wvdAssetsStorage]" 43 | 44 | - name: wvdUploadsPath 45 | value: 'Uploads' 46 | 47 | #endregion 48 | 49 | ################################ 50 | ## HOSTPOOL PIPELINE SPECIFIC ## 51 | ################################ 52 | #region specific 53 | 54 | ## Resource group where WVD resources will be deployed 55 | - name: resourceGroupName 56 | value: "[resourceGroupName]" 57 | 58 | ## Jobs 59 | - name: enableApplicationJob 60 | value: true # To enable/disable remote apps job 61 | 62 | ## Hostpool 63 | #customImageReferenceId, as value, put: '/subscriptions//resourceGroups//providers/Microsoft.Compute/galleries//images//versions/' 64 | - name: customImageReferenceId 65 | value: '' 66 | 67 | - name: publisher 68 | value: "MicrosoftWindowsDesktop" 69 | 70 | - name: offer 71 | value: "office-365" 72 | 73 | - name: sku 74 | value: "20h1-evd-o365pp" 75 | 76 | - name: version 77 | value: "latest" 78 | 79 | - name: HostPoolName 80 | value: "QS-WVD-HP" 81 | 82 | - name: profilesStorageAccountName 83 | value: "[profilesStorageAccountName]" 84 | 85 | #endregion 86 | 87 | ################################ 88 | ## PROFILES PIPELINE SPECIFIC ## 89 | ################################ 90 | #region specific 91 | 92 | ## IdentityApproach (AD or Azure AD DS) 93 | - name: identityApproach 94 | value: "[identityApproach]" 95 | 96 | #endregion 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WVD QuickStart 2 | 3 | Welcome to the WVD QuickStart GitHub repository! The WVD QuickStart is a solution intended to simplify and automate WVD deployments, empowering IT professionals to get started with WVD in a matter of clicks. New to WVD? Check out https://aka.ms/wvddocs for more information. 4 | 5 | By clicking the "Deploy to Azure" button, you will be taken to the Azure Portal for a custom deployment. There, you can fill out the required user input and click "deploy". This will set up some resources needed for the QuickStart, including an Azure DevOps project. 6 | 7 | 8 | 9 |
10 | 11 | 12 | Once the deployment completes, please navigate to https://dev.azure.com, where you will find the WVD QuickStart project. Navigate to the "pipelines" section - Here you'll find a running pipeline that deploys a WVD environment (VMs, host pool, desktop app group, FSLogix configuration) for you. Upon completion of this pipeline, which will take about 15 minutes, your WVD environment is ready for use! 13 | 14 | The QuickStart creates a test user for you to try out the environment. Navigate to https://rdweb.wvd.microsoft.com/arm/webclient/index.html and login with the following test user credentials: 15 | 16 | Username: WVDTestUser001@{your-domain}.com
17 | Password: Taken from DevOps organization in the following way: If organization is called "WVDQuickStartOrg120011Z", your password will be "Org120011Z!" (case sensitive, and don't forget the exclamation point at the end) 18 | (Disclaimer: You should change this password at your earliest convenience.) 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /SharedDeploymentFunctions/Add-CustomParameters.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Overwrite a given paremeter.json with a version populated by the values in the provided valuemap 4 | 5 | .DESCRIPTION 6 | Overwrite a given paremeter.json with a version populated by the values in the provided valuemap 7 | 8 | .PARAMETER parameterFilePath 9 | Mandatory. Full path to the parameter file 10 | 11 | .PARAMETER valueMap 12 | Mandatory. Array with [Path = targetPath; Value = targetValue] or [Path = targetPath; Value = targetValue; ReplaceToken = 'myToken'] hashtables. 13 | The Path has to start below the parameters:{} level of the parameter file 14 | If a replace token is specified, only this value will be replaced with the targetValue 15 | If a reploce token is NOT specified, the whole value will be replaced with the targed value 16 | 17 | .PARAMETER jsonDepth 18 | Optional. The depth of the json to deal with. Important for the convertion back into the json format. Defaults to 15 19 | 20 | .EXAMPLE 21 | Add-CustomParameters -parameterFilePath 'C:/parameter.json' -valueMap @( @{ Path = pathA; Value = 'valueA'}, @{ Path = pathB; Value = 'valueB' }) 22 | 23 | Overwrite all parameter in the parameter.json file that match the paths 'pathA' & 'pathB' with the values 'valueA' & 'valueB' respectively 24 | 25 | .EXAMPLE 26 | Add-CustomParameters -parameterFilePath 'C:/parameter.json' -valueMap @( @{ Path = pathA; Value = 'valueA'; replaceToken = '[sasKey]'}) 27 | 28 | Overwrite all parameter in the parameter.json file that match the path 'pathA' and contain the token 'sasKey' with the values 'valueA' for this token 29 | #> 30 | function Add-CustomParameters { 31 | 32 | [CmdletBinding(SupportsShouldProcess = $true)] 33 | param ( 34 | [Parameter(Mandatory = $true)] 35 | [string] $parameterFilePath, 36 | 37 | [Parameter(Mandatory = $true)] 38 | [Array] $valueMap, 39 | 40 | [Parameter(Mandatory = $false)] 41 | [int] $jsonDepth = 15 42 | ) 43 | 44 | begin { 45 | Write-Debug ("[{0} entered]" -f $MyInvocation.MyCommand) 46 | } 47 | 48 | process { 49 | $paramFileContent = ConvertFrom-Json (Get-Content -Raw -Path $parameterFilePath) 50 | 51 | foreach ($valueItem in $valueMap) { 52 | $path = $valueItem.Path 53 | 54 | try { 55 | if ($valueItem.ReplaceToken) { 56 | $currentValue = Invoke-Expression "`$paramFileContent.parameters.$path" 57 | $targetValue = $currentValue.Replace($valueItem.ReplaceToken, $valueItem.Value) 58 | Invoke-Expression "`$paramFileContent.parameters.$path = '$targetValue'" 59 | } 60 | elseif ($valueItem.AddToArray) { 61 | $currentValue = Invoke-Expression "`$paramFileContent.parameters.$path" 62 | $targetValue = $currentValue += $valueItem.Value 63 | Invoke-Expression "`$paramFileContent.parameters.$path = '$targetValue'" 64 | } 65 | else { 66 | $targetValue = $valueItem.Value 67 | Invoke-Expression "`$paramFileContent.parameters.$path = '$targetValue'" 68 | } 69 | } 70 | catch { 71 | Write-Error ("Exception caught. Please doublecheck if the property path [{0}] is valid" -f $valueItem.Path) 72 | throw $_ 73 | } 74 | } 75 | 76 | if ($PSCmdlet.ShouldProcess(("Paramter file [{0}]" -f (Split-Path $parameterFilePath -Leaf)), "Overwrite")) { 77 | ConvertTo-Json $paramFileContent -Depth 15 | Out-File -FilePath $parameterFilePath 78 | Write-Verbose "Custom parameters added to file" 79 | } 80 | } 81 | 82 | end { 83 | Write-Debug ("[{0} existed]" -f $MyInvocation.MyCommand) 84 | } 85 | } -------------------------------------------------------------------------------- /SharedDeploymentFunctions/Invoke-GeneralDeployment.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-GeneralDeployment { 2 | 3 | [CmdletBinding()] 4 | param( 5 | [string] $resourcegroupName, 6 | [string] $location, 7 | [string] $moduleName, 8 | [string] $moduleVersion, 9 | [string] $parameterFilePath, 10 | [Parameter(Mandatory = $false)][hashtable] $optionalParameters, 11 | [string] $managementGroupId 12 | ) 13 | 14 | begin { 15 | Write-Debug ("[{0} entered]" -f $MyInvocation.MyCommand) 16 | } 17 | 18 | process { 19 | 20 | $templateUri = 'https://raw.githubusercontent.com/stgeorgi/wvdquickstart/master/Modules/ARM/{0}/deploy.json' -f $moduleName 21 | 22 | Write-Verbose "Parameters are" -Verbose 23 | $param = ConvertFrom-Json (Get-Content -Raw -Path $parameterFilePath) 24 | $paramSet = @{ } 25 | $param.parameters | Get-Member -MemberType NoteProperty | ForEach-Object { 26 | $key = $_.Name 27 | $value = $param.parameters.($_.Name).Value 28 | if ($value -is [string]) { 29 | $formattedValue = $value.subString(0, [System.Math]::Min(15, $value.Length)) 30 | if ($value.Length -gt 40) { 31 | $formattedValue += '...' 32 | } 33 | } 34 | else { 35 | $formattedValue = $value 36 | } 37 | $paramSet[$key] = $formattedValue 38 | } 39 | Write-Verbose ($paramSet | Format-Table | Out-String) -Verbose 40 | 41 | Write-Verbose "Additional Parameters are" 42 | Write-Verbose ($optionalParameters | Format-Table | Out-String) -Verbose 43 | 44 | Write-Verbose "Deploy to resource group '$resourcegroupName'" 45 | $deploymentId = 'WVD-QuickStart-Deployment' 46 | 47 | $DeploymentInputs = @{ 48 | Name = ("{0}-{1}" -f $moduleName, $deploymentId) 49 | TemplateUri = $templateUri 50 | TemplateParameterFile = $parameterFilePath 51 | Verbose = $true 52 | ErrorAction = "Stop" 53 | } 54 | 55 | Foreach ($key in $optionalParameters.Keys) { 56 | $DeploymentInputs += @{ 57 | $key = $optionalParameters.Item($key) 58 | } 59 | } 60 | 61 | $deploymentSchema = (Invoke-RestMethod -Uri $templateUri -Method 'GET').'$schema' # Works with PS7 62 | Write-Verbose "Evaluating schema [$deploymentSchema]" -Verbose 63 | switch -regex ($deploymentSchema) { 64 | '\/deploymentTemplate.json#$' { 65 | Write-Verbose 'Handling resource group level deployment' -Verbose 66 | if (-not (Get-AzResourceGroup -Name $resourcegroupName -ErrorAction SilentlyContinue)) { 67 | Write-Verbose 'Deploying resource group [$resourcegroupName]' -Verbose 68 | New-AzResourceGroup -Name $resourcegroupName -Location $location 69 | } 70 | 71 | if (-not (Get-AzResourceGroup -Name $resourcegroupName -ErrorAction SilentlyContinue)) { 72 | $Location = $location -replace " ", "" 73 | New-AzResourceGroup -Name $resourcegroupName -Location $Location 74 | } 75 | 76 | $Deployment = New-AzResourceGroupDeployment @DeploymentInputs -ResourceGroupName $resourcegroupName 77 | break 78 | } 79 | '\/subscriptionDeploymentTemplate.json#$' { 80 | Write-Verbose 'Handling subscription level deployment' -Verbose 81 | $DeploymentInputs += @{ 82 | Location = $location 83 | } 84 | $Deployment = New-AzSubscriptionDeployment @DeploymentInputs 85 | break 86 | } 87 | '\/managementGroupDeploymentTemplate.json#$' { 88 | Write-Verbose 'Handling management group level deployment' -Verbose 89 | $DeploymentInputs += @{ 90 | ManagementGroupId = $managementGroupId 91 | Location = $location 92 | } 93 | $Deployment = New-AzManagementGroupDeployment @DeploymentInputs 94 | break 95 | } 96 | '\/tenantDeploymentTemplate.json#$' { 97 | Write-Verbose 'Handling tenant level deployment' -Verbose 98 | $DeploymentInputs += @{ 99 | Location = $location 100 | } 101 | $Deployment = New-AzTenantDeployment @DeploymentInputs 102 | break 103 | } 104 | default { 105 | throw "[$deploymentSchema] is a non-supported ARM template schema" 106 | } 107 | } 108 | 109 | if ($Deployment.Outputs) { 110 | foreach ($Outputkey in $Deployment.Outputs.Keys) { 111 | Write-Verbose "Set [$Outputkey] deployment output as pipeline environment variable" -Verbose 112 | Write-Host ("##vso[task.setvariable variable={0};isOutput=true]{1}" -f $Outputkey, $Deployment.Outputs[$Outputkey].Value) 113 | } 114 | } 115 | 116 | Write-Verbose "Deployment successful" -Verbose 117 | } 118 | end { 119 | Write-Debug ("[{0} existed]" -f $MyInvocation.MyCommand) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /SharedDeploymentFunctions/Storage/Compress-WVDCSEContent.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Compress Scripts and Executable files needed to customize WVD VMs to a zip archive. 4 | 5 | .DESCRIPTION 6 | This cmdlet performs compression for all content of each subfolder of a specified source folder into a specified destination folder. 7 | 8 | .PARAMETER SourceFolderPath 9 | Specifies the location containing subfolders to be compressed. 10 | 11 | .PARAMETER DestinationFolderPath 12 | Specifies the location for the .zip files. 13 | 14 | .PARAMETER CompressionLevel 15 | Specifies how much compression to apply when creating the archive file. Fastest as default. 16 | 17 | .PARAMETER Confirm 18 | Will promt user to confirm the action to create invasible commands 19 | 20 | .PARAMETER WhatIf 21 | Dry run of the script 22 | 23 | .EXAMPLE 24 | Compress-WVDCSEContent -SourceFolderPath "\\path\to\sourcefolder" -DestinationFolderPath "\\path\to\destinationfolder" 25 | 26 | Creates the "\\path\to\destinationfolder" if not existing 27 | Moves there the scriptExtensionMasterInstaller.ps1 master script for CSE 28 | For each subfolder in "\\path\to\sourcefolder" creates an archive with the fastest compression level named "subfolder.zip" in the "\\path\to\destinationfolder". 29 | #> 30 | 31 | function Compress-WVDCSEContent { 32 | 33 | [CmdletBinding(SupportsShouldProcess = $True)] 34 | param( 35 | [Parameter( 36 | Mandatory = $true, 37 | HelpMessage = "Specifies the location containing subfolders to be compressed." 38 | )] 39 | [string] $SourceFolderPath, 40 | 41 | [Parameter( 42 | Mandatory = $true, 43 | HelpMessage = "Specifies the location for the .zip files." 44 | )] 45 | [string] $DestinationFolderPath, 46 | 47 | [Parameter( 48 | Mandatory = $false, 49 | HelpMessage = "Specifies how much compression to apply when creating the archive file. Fastest as default." 50 | )] 51 | [string] $CompressionLevel = "Fastest" 52 | ) 53 | 54 | 55 | Write-Verbose "## Checking destination folder existance $DestinationFolderPath" 56 | If (!(Test-path $DestinationFolderPath)) { 57 | Write-Verbose "Not existing, creating..." 58 | New-Item -ItemType "directory" -Path $DestinationFolderPath 59 | } 60 | 61 | Write-Verbose "## Move master script from $SourceFolderPath to $DestinationFolderPath" 62 | $CSEMasterScriptSource = Join-Path $SourceFolderPath "scriptExtensionMasterInstaller.ps1" 63 | $CSEMasterScriptDestination = Join-Path $DestinationFolderPath "scriptExtensionMasterInstaller.ps1" 64 | Move-Item -Path $CSEMasterScriptSource -Destination $CSEMasterScriptDestination 65 | 66 | Write-Verbose "## Create archives " 67 | $subfolders = Get-ChildItem $SourceFolderPath | ?{$_.PSISContainer} 68 | foreach ($sf in $subfolders){ 69 | try { 70 | $destinationFilePath = Join-Path -Path $DestinationFolderPath -ChildPath ($sf.Name + ".zip") 71 | $sourceFilePath = Join-Path -Path $sf.FullName -ChildPath "*" 72 | 73 | Write-Verbose "Working on subfolder $sf" 74 | Write-Verbose "Archive will be created from path $sourceFilePath" 75 | Write-Verbose "Archive will be stored as $destinationFilePath" 76 | 77 | $CompressInputObject = @{ 78 | Path = $sourceFilePath 79 | DestinationPath = $destinationFilePath 80 | CompressionLevel = $CompressionLevel 81 | Force = $true 82 | } 83 | 84 | Write-Verbose "Starting compression...." 85 | if ($PSCmdlet.ShouldProcess("Required files from $sourceFilePath to $destinationFilePath", "Compress")) { 86 | Compress-Archive @CompressInputObject 87 | } 88 | Write-Verbose "Compression completed." 89 | } 90 | catch { 91 | Write-Error "Compression FAILED: $_" 92 | } 93 | 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /SharedDeploymentFunctions/Storage/Export-WVDCSEContentToBlob.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Upload Scripts and Executable files needed to customize WVD VMs to the created Storage Accounts blob containers. 4 | 5 | .DESCRIPTION 6 | This cmdlet uploads files specifiied in the contentToUpload-sourcePath parameter to the blob specified in the contentToUpload-targetBlob parameter to the specified Azure Storage Account. 7 | 8 | .PARAMETER ResourceGroupName 9 | Name of the resource group that contains the Storage account to update. 10 | 11 | .PARAMETER StorageAccountName 12 | Name of the Storage account to update. 13 | 14 | .PARAMETER contentToUpload 15 | Optional. Array with a contentmap to upload. 16 | E.g. $( @{ sourcePath = 'WVDScripts'; targetBlob = 'wvdscripts' }) 17 | 18 | .PARAMETER Confirm 19 | Will promt user to confirm the action to create invasible commands 20 | 21 | .PARAMETER WhatIf 22 | Dry run of the script 23 | 24 | .EXAMPLE 25 | Export-WVDCSEContentToBlob -ResourceGroupName "RG01" -StorageAccountName "storageaccount01" 26 | 27 | Uploads files contained in the WVDScripts Repo folder and the files contained in the WVDScaling Repo folder 28 | respectively to the "wvdscripts" blob container and to the "wvdScaling" blob container in the Storage Account "storageaccount01" 29 | of the Resource Group "RG01" 30 | 31 | .EXAMPLE 32 | Export-WVDCSEContentToBlob -ResourceGroupName "RG01" -StorageAccountName "storageaccount01" -contentToUpload $( @{ sourcePath = 'WVDScripts'; targetBlob = 'wvdscripts' }) 33 | 34 | Uploads files contained in the WVDScripts Repo folder to the "wvdscripts" blob container in the Storage Account "storageaccount01" 35 | of the Resource Group "RG01" 36 | #> 37 | function Export-WVDCSEContentToBlob { 38 | 39 | [CmdletBinding(SupportsShouldProcess = $True)] 40 | param( 41 | [Parameter( 42 | Mandatory = $true, 43 | HelpMessage = "Specifies the name of the resource group that contains the Storage account to update." 44 | )] 45 | [string] $ResourceGroupName, 46 | 47 | [Parameter( 48 | Mandatory = $true, 49 | HelpMessage = "Specifies the name of the Storage account to update." 50 | )] 51 | [string] $StorageAccountName, 52 | 53 | [Parameter( 54 | Mandatory = $false, 55 | HelpMessage = "Map of source/target tuples for upload" 56 | )] 57 | [Hashtable[]] $contentToUpload = $( 58 | @{ 59 | sourcePath = 'WVDCSEZipToUpload' 60 | targetBlob = 'wvdscripts' 61 | } 62 | ) 63 | ) 64 | 65 | Write-Verbose "Getting storage account context." 66 | $storageAccount = Get-AzStorageAccount -ResourceGroupName $ResourceGroupName -StorageAccountName $StorageAccountName -ErrorAction Stop 67 | $ctx = $storageAccount.Context 68 | 69 | Write-Verbose "Building paths to the local folders to upload." 70 | Write-Verbose "Script Directory: '$PSScriptRoot'" 71 | $sourcesPath = Split-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) -Parent 72 | $contentDirectory = Join-Path -Path $sourcesPath "parameters/s/Uploads" 73 | Write-Verbose "Content directory: '$contentDirectory'" 74 | 75 | foreach ($contentObject in $contentToUpload) { 76 | 77 | $sourcePath = $contentObject.sourcePath 78 | $targetBlob = $contentObject.targetBlob 79 | 80 | try { 81 | $pathToContentToUpload = Join-Path $contentDirectory $sourcePath 82 | Write-Verbose "Processing content in path: '$pathToContentToUpload'" 83 | 84 | Write-Verbose "Testing local path" 85 | If (-Not (Test-Path -Path $pathToContentToUpload)) { 86 | throw "Testing local paths FAILED: Cannot find content path to upload '$pathToContentToUpload'" 87 | } 88 | Write-Verbose "Testing paths: SUCCEEDED" 89 | 90 | Write-Verbose "Getting files to be uploaded..." 91 | $scriptsToUpload = Get-ChildItem -Path $pathToContentToUpload -ErrorAction Stop 92 | Write-Verbose "Files to be uploaded:" 93 | Write-Verbose ($scriptsToUpload.Name | Format-List | Out-String) 94 | 95 | Write-Verbose "Testing blob container" 96 | Get-AzStorageContainer -Name $targetBlob -Context $ctx -ErrorAction Stop 97 | Write-Verbose "Testing blob container SUCCEEDED" 98 | 99 | if ($PSCmdlet.ShouldProcess("Files to the '$targetBlob' container", "Upload")) { 100 | $scriptsToUpload | Set-AzStorageBlobContent -Container $targetBlob -Context $ctx -Force -ErrorAction Stop 101 | } 102 | } 103 | catch { 104 | Write-Error "Upload FAILED: $_" 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /SharedDeploymentFunctions/Storage/Import-WVDSoftware.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Upload Scripts and Executable files needed to customize WVD VMs to the created Storage Accounts blob containers. 4 | 5 | .DESCRIPTION 6 | This cmdlet uploads files specifiied in the contentToUpload-sourcePath parameter to the blob specified in the contentToUpload-targetBlob parameter to the specified Azure Storage Account. 7 | 8 | .PARAMETER Url 9 | Specifies the URI from which to download data. 10 | 11 | .PARAMETER FileName 12 | Specifies the name of the local file that is to receive the data. 13 | 14 | .PARAMETER Confirm 15 | Will promt user to confirm the action to create invasible commands 16 | 17 | .PARAMETER WhatIf 18 | Dry run of the script 19 | 20 | .EXAMPLE 21 | Import-WVDSoftware -Url "https://aka.ms/fslogix_download" -FileName "FSLogixApp.zip" 22 | 23 | Downloads file from the specified Uri and save it to the specified filepath 24 | #> 25 | 26 | function Import-WVDSoftware { 27 | 28 | [CmdletBinding(SupportsShouldProcess = $True)] 29 | param( 30 | [Parameter( 31 | Mandatory = $true, 32 | HelpMessage = "Specifies the URI from which to download data." 33 | )] 34 | [string] $Url, 35 | 36 | [Parameter( 37 | Mandatory = $true, 38 | HelpMessage = "Specifies the name of the local file that is to receive the data." 39 | )] 40 | [string] $FileName 41 | ) 42 | 43 | Write-Verbose "Getting current time." 44 | $start_time = Get-Date 45 | 46 | try { 47 | Write-Verbose "Starting download...." 48 | if ($PSCmdlet.ShouldProcess("Required executable files from $url to $filename", "Import")) { 49 | (New-Object System.Net.WebClient).DownloadFile($Url, $FileName) 50 | } 51 | Write-Verbose "Download completed." 52 | Write-Verbose "Time taken: $((Get-Date).Subtract($start_time).Seconds) second(s)" 53 | } 54 | catch { 55 | Write-Error "Download FAILED: $_" 56 | } 57 | } -------------------------------------------------------------------------------- /Uploads/Configuration.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/Uploads/Configuration.zip -------------------------------------------------------------------------------- /Uploads/WVDScripts/001-AzFiles/Eula.txt: -------------------------------------------------------------------------------- 1 | Sysinternals Software License Terms 2 | These license terms are an agreement between Sysinternals (a wholly owned subsidiary of Microsoft Corporation) and you. Please read them. They apply to the software you are downloading from technet.microsoft.com/sysinternals, which includes the media on which you received it, if any. The terms also apply to any Sysinternals 3 | * updates, 4 | * supplements, 5 | * Internet-based services, 6 | * and support services 7 | for this software, unless other terms accompany those items. If so, those terms apply. 8 | BY USING THE SOFTWARE, YOU ACCEPT THESE TERMS. IF YOU DO NOT ACCEPT THEM, DO NOT USE THE SOFTWARE. 9 | If you comply with these license terms, you have the rights below. 10 | 11 | Installation and User Rights 12 | 13 | You may install and use any number of copies of the software on your devices. 14 | 15 | Scope of License 16 | 17 | The software is licensed, not sold. This agreement only gives you some rights to use the software. Sysinternals reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the software only as expressly permitted in this agreement. In doing so, you must comply with any technical limitations in the software that only allow you to use it in certain ways. You may not 18 | * work around any technical limitations in the software; 19 | * reverse engineer, decompile or disassemble the software, except and only to the extent that applicable law expressly permits, despite this limitation; 20 | * make more copies of the software than specified in this agreement or allowed by applicable law, despite this limitation; 21 | * publish the software for others to copy; 22 | * rent, lease or lend the software; 23 | * transfer the software or this agreement to any third party; or 24 | * use the software for commercial software hosting services. 25 | 26 | Sensitive Information 27 | 28 | Please be aware that, similar to other debug tools that capture “process state” information, files saved by Sysinternals tools may include personally identifiable or other sensitive information (such as usernames, passwords, paths to files accessed, and paths to registry accessed). By using this software, you acknowledge that you are aware of this and take sole responsibility for any personally identifiable or other sensitive information provided to Microsoft or any other party through your use of the software. 29 | 30 | Documentation 31 | 32 | Any person that has valid access to your computer or internal network may copy and use the documentation for your internal, reference purposes. 33 | 34 | Export Restrictions 35 | 36 | The software is subject to United States export laws and regulations. You must comply with all domestic and international export laws and regulations that apply to the software. These laws include restrictions on destinations, end users and end use. For additional information, see www.microsoft.com/exporting . 37 | 38 | Support Services 39 | 40 | Because this software is "as is," we may not provide support services for it. 41 | 42 | Entire Agreement 43 | 44 | This agreement, and the terms for supplements, updates, Internet-based services and support services that you use, are the entire agreement for the software and support services. 45 | 46 | Applicable Law 47 | 48 | United States . If you acquired the software in the United States , Washington state law governs the interpretation of this agreement and applies to claims for breach of it, regardless of conflict of laws principles. The laws of the state where you live govern all other claims, including claims under state consumer protection laws, unfair competition laws, and in tort. 49 | Outside the United States . If you acquired the software in any other country, the laws of that country apply. 50 | 51 | Legal Effect 52 | 53 | This agreement describes certain legal rights. You may have other rights under the laws of your country. You may also have rights with respect to the party from whom you acquired the software. This agreement does not change your rights under the laws of your country if the laws of your country do not permit it to do so. 54 | 55 | Disclaimer of Warranty 56 | 57 | The software is licensed "as-is." You bear the risk of using it. Sysinternals gives no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this agreement cannot change. To the extent permitted under your local laws, sysinternals excludes the implied warranties of merchantability, fitness for a particular purpose and non-infringement. 58 | 59 | Limitation on and Exclusion of Remedies and Damages 60 | 61 | You can recover from sysinternals and its suppliers only direct damages up to U.S. $5.00. You cannot recover any other damages, including consequential, lost profits, special, indirect or incidental damages. 62 | This limitation applies to 63 | * anything related to the software, services, content (including code) on third party Internet sites, or third party programs; and 64 | * claims for breach of contract, breach of warranty, guarantee or condition, strict liability, negligence, or other tort to the extent permitted by applicable law. 65 | 66 | It also applies even if Sysinternals knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your country may not allow the exclusion or limitation of incidental, consequential or other damages. 67 | Please note: As this software is distributed in Quebec , Canada , some of the clauses in this agreement are provided below in French. 68 | Remarque : Ce logiciel étant distribué au Québec, Canada, certaines des clauses dans ce contrat sont fournies ci-dessous en français. 69 | EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert « tel quel ». Toute utilisation de ce logiciel est à votre seule risque et péril. Sysinternals n'accorde aucune autre garantie expresse. Vous pouvez bénéficier de droits additionnels en vertu du droit local sur la protection dues consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le droit locale, les garanties implicites de qualité marchande, d'adéquation à un usage particulier et d'absence de contrefaçon sont exclues. 70 | LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES DOMMAGES. Vous pouvez obtenir de Sysinternals et de ses fournisseurs une indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris les dommages spéciaux, indirects ou accessoires et pertes de bénéfices. 71 | Cette limitation concerne : 72 | tout ce qui est relié au logiciel, aux services ou au contenu (y compris le code) figurant sur des sites Internet tiers ou dans des programmes tiers ; et 73 | les réclamations au titre de violation de contrat ou de garantie, ou au titre de responsabilité stricte, de négligence ou d'une autre faute dans la limite autorisée par la loi en vigueur. 74 | Elle s'applique également, même si Sysinternals connaissait ou devrait connaître l'éventualité d'un tel dommage. Si votre pays n'autorise pas l'exclusion ou la limitation de responsabilité pour les dommages indirects, accessoires ou de quelque nature que ce soit, il se peut que la limitation ou l'exclusion ci-dessus ne s'appliquera pas à votre égard. 75 | EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous pourriez avoir d'autres droits prévus par les lois de votre pays. Le présent contrat ne modifie pas les droits que vous confèrent les lois de votre pays si celles-ci ne le permettent pas. 76 | -------------------------------------------------------------------------------- /Uploads/WVDScripts/001-AzFiles/PsExec.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/Uploads/WVDScripts/001-AzFiles/PsExec.exe -------------------------------------------------------------------------------- /Uploads/WVDScripts/001-AzFiles/PsExec64.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/Uploads/WVDScripts/001-AzFiles/PsExec64.exe -------------------------------------------------------------------------------- /Uploads/WVDScripts/001-AzFiles/Pstools.chm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stgeorgi/wvdquickstart/17190dcafb4ea9161f31b7fc6329a9a154e04064/Uploads/WVDScripts/001-AzFiles/Pstools.chm -------------------------------------------------------------------------------- /Uploads/WVDScripts/001-AzFiles/cse_run.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(SupportsShouldProcess = $true)] 2 | param ( 3 | # [Parameter(Mandatory = $true)] 4 | # [ValidateNotNullOrEmpty()] 5 | # [string] $storageAccountKey, 6 | 7 | [Parameter(Mandatory = $false)] 8 | [Hashtable] $DynParameters, 9 | 10 | [Parameter(Mandatory = $false)] 11 | [string] $AzureAdminUpn, 12 | 13 | [Parameter(Mandatory = $true)] 14 | [string] $AzureAdminPassword, 15 | 16 | [Parameter(Mandatory = $true)] 17 | [string] $domainJoinPassword, 18 | 19 | [Parameter(Mandatory = $false)] 20 | [ValidateNotNullOrEmpty()] 21 | [string] $ConfigurationFileName = "azfiles.parameters.json" 22 | ) 23 | 24 | ##################################### 25 | 26 | ########## 27 | # Helper # 28 | ########## 29 | #region Functions 30 | function LogInfo($message) { 31 | Log "Info" $message 32 | } 33 | 34 | function LogError($message) { 35 | Log "Error" $message 36 | } 37 | 38 | function LogSkip($message) { 39 | Log "Skip" $message 40 | } 41 | 42 | function LogWarning($message) { 43 | Log "Warning" $message 44 | } 45 | 46 | function Log { 47 | 48 | <# 49 | .SYNOPSIS 50 | Creates a log file and stores logs based on categories with tab seperation 51 | 52 | .PARAMETER category 53 | Category to put into the trace 54 | 55 | .PARAMETER message 56 | Message to be loged 57 | 58 | .EXAMPLE 59 | Log 'Info' 'Message' 60 | 61 | #> 62 | 63 | Param ( 64 | $category = 'Info', 65 | [Parameter(Mandatory = $true)] 66 | $message 67 | ) 68 | 69 | $date = get-date 70 | $content = "[$date]`t$category`t`t$message`n" 71 | Write-Verbose "$content" -verbose 72 | 73 | if (! $script:Log) { 74 | $File = Join-Path $env:TEMP "log.log" 75 | Write-Error "Log file not found, create new $File" 76 | $script:Log = $File 77 | } 78 | else { 79 | $File = $script:Log 80 | } 81 | Add-Content $File $content -ErrorAction Stop 82 | } 83 | 84 | function Set-Logger { 85 | <# 86 | .SYNOPSIS 87 | Sets default log file and stores in a script accessible variable $script:Log 88 | Log File name "executionCustomScriptExtension_$date.log" 89 | 90 | .PARAMETER Path 91 | Path to the log file 92 | 93 | .EXAMPLE 94 | Set-Logger 95 | Create a logger in 96 | #> 97 | 98 | Param ( 99 | [Parameter(Mandatory = $true)] 100 | $Path 101 | ) 102 | 103 | # Create central log file with given date 104 | 105 | $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" 106 | 107 | $scriptName = (Get-Item $PSCommandPath ).Basename 108 | $scriptName = $scriptName -replace "-", "" 109 | 110 | Set-Variable logFile -Scope Script 111 | $script:logFile = "executionCustomScriptExtension_" + $scriptName + "_" + $date + ".log" 112 | 113 | if ((Test-Path $path ) -eq $false) { 114 | $null = New-Item -Path $path -type directory 115 | } 116 | 117 | $script:Log = Join-Path $path $logfile 118 | 119 | Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" 120 | } 121 | #endregion 122 | 123 | 124 | ## MAIN 125 | #Set-Logger "C:\WindowsAzure\CustomScriptExtension\Log" # inside "executionCustomScriptExtension_$date.log" 126 | Set-Logger "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog\azfilesconfig" # inside "executionCustomScriptExtension_$scriptName_$date.log" 127 | 128 | LogInfo("###################") 129 | LogInfo("## 0 - LOAD DATA ##") 130 | LogInfo("###################") 131 | #$storageaccountkey = $DynParameters.storageaccountkey 132 | 133 | $ConfigurationFilePath= Join-Path $PSScriptRoot $ConfigurationFileName 134 | 135 | $ConfigurationJson = Get-Content -Path $ConfigurationFilePath -Raw -ErrorAction 'Stop' 136 | 137 | try { $azfilesconfig = $ConfigurationJson | ConvertFrom-Json -ErrorAction 'Stop' } 138 | catch { 139 | Write-Error "Configuration JSON content could not be converted to a PowerShell object" -ErrorAction 'Stop' 140 | } 141 | 142 | LogInfo("##################") 143 | LogInfo("## 1 - EVALUATE ##") 144 | LogInfo("##################") 145 | foreach ($config in $azfilesconfig.azfilesconfig) { 146 | if ($config.enableAzureFiles) { 147 | if ($config.identityApproach -eq "AD") { 148 | LogInfo("############################") 149 | LogInfo("## 2 - Enable Azure Files ##") 150 | LogInfo("############################") 151 | 152 | LogInfo("Set Execution Policy...") 153 | #Change the execution policy to unblock importing AzFilesHybrid.psm1 module 154 | Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser -Force 155 | 156 | #Define parameters 157 | $ResourceGroupName = $config.ResourceGroupName 158 | $ResourceGroupName = $ResourceGroupName.Replace('"', "'") 159 | $StorageAccountName = $config.StorageAccountName 160 | $StorageAccountName = $StorageAccountName.Replace('"', "'") 161 | 162 | # Register the target storage account with your active directory environment under the target OU (for example: specify the OU with Name as "UserAccounts" or DistinguishedName as "OU=UserAccounts,DC=CONTOSO,DC=COM"). 163 | # You can use to this PowerShell cmdlet: Get-ADOrganizationalUnit to find the Name and DistinguishedName of your target OU. If you are using the OU Name, specify it with -OrganizationalUnitName as shown below. If you are using the OU DistinguishedName, you can set it with -OrganizationalUnitDistinguishedName. You can choose to provide one of the two names to specify the target OU. 164 | # You can choose to create the identity that represents the storage account as either a Service Logon Account or Computer Account (default parameter value), depends on the AD permission you have and preference. 165 | # Run Get-Help Join-AzStorageAccountForAuth for more details on this cmdlet. 166 | 167 | $split = $config.domainName.Split(".") 168 | $username = $($split[0] + "\" + $config.domainJoinUsername) 169 | $scriptPath = $($PSScriptRoot + "\setup.ps1") 170 | Set-Location $PSScriptRoot 171 | 172 | LogInfo("Using PSExec, set execution policy for the admin user") 173 | $scriptBlock = { .\psexec /accepteula -h -u $username -p $domainJoinPassword -c -f "powershell.exe" Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser -Force } 174 | Invoke-Command $scriptBlock -Verbose 175 | 176 | LogInfo("Execution policy for the admin user set. Now joining the storage account through another PSExec command... This command takes roughly 5 minutes") 177 | $scriptBlock = { .\psexec /accepteula -h -u $username -p $domainJoinPassword -c -f "powershell.exe" "$scriptPath -S $StorageAccountName -RG $ResourceGroupName -U $AzureAdminUpn -P $AzureAdminPassword" } 178 | LogInfo("Scriptblock to execute: $scriptBlock") 179 | Invoke-Command $scriptBlock -Verbose 180 | 181 | LogInfo("Azure Files Enabled!") 182 | } 183 | elseif ($config.identityApproach -eq "Azure AD DS") { 184 | LogInfo("Azure AD DS is used, for which the storage account has been enabled in the DevOps pipeline. No further action is needed in this Custom Script Extension") 185 | } 186 | } 187 | } -------------------------------------------------------------------------------- /Uploads/WVDScripts/001-AzFiles/psversion.txt: -------------------------------------------------------------------------------- 1 | PsTools Version in this package: 2.45 -------------------------------------------------------------------------------- /Uploads/WVDScripts/001-AzFiles/setup.ps1: -------------------------------------------------------------------------------- 1 | 2 | <# 3 | .SYNOPSIS 4 | Enables Azure Files for a native AD environment, executing the domain join of the storage account using the AzFilesHybrid module. 5 | Parameter names have been abbreviated to shorten the 'PSExec' command, which has a limited number of allowed characters. 6 | 7 | .PARAMETER RG 8 | Resource group of the profiles storage account 9 | 10 | .PARAMETER S 11 | Name of the profiles storage account 12 | 13 | .PARAMETER U 14 | Azure admin UPN 15 | 16 | .PARAMETER P 17 | Azure admin password 18 | 19 | #> 20 | 21 | param( 22 | 23 | 24 | [Parameter(Mandatory = $true)] 25 | [string] $RG, 26 | 27 | [Parameter(Mandatory = $true)] 28 | [string] $S, 29 | 30 | [Parameter(Mandatory = $true)] 31 | [string] $U, 32 | 33 | [Parameter(Mandatory = $true)] 34 | [string] $P 35 | 36 | ) 37 | 38 | # Set execution policy 39 | Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Scope CurrentUser -Force 40 | 41 | Set-Location $PSScriptroot 42 | 43 | # Import required modules 44 | .\CopyToPSPath.ps1 45 | Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force 46 | Install-Module -Name PowershellGet -MinimumVersion 2.2.4.1 -Force 47 | 48 | Install-Module -Name Az -Force -Verbose 49 | 50 | Import-Module -Name AzFilesHybrid -Force -Verbose 51 | Import-Module -Name activedirectory -Force -Verbose 52 | 53 | # Find existing OU or create new one. Get path for OU from domain by splitting the domain name, to format DC=fabrikam,DC=com 54 | $domain = $U.split('@')[1] 55 | $DC = $domain.split('.') 56 | foreach($name in $DC) { 57 | $path = $path + ',DC=' + $name 58 | } 59 | $path = $path.substring(1) 60 | $ou = Get-ADOrganizationalUnit -Filter 'Name -like "Profiles Storage"' 61 | if ($ou -eq $null) { 62 | New-ADOrganizationalUnit -name 'Profiles Storage' -path $path 63 | } 64 | 65 | # Connect to Azure 66 | $Credential = New-Object System.Management.Automation.PsCredential($U, (ConvertTo-SecureString $P -AsPlainText -Force)) 67 | Connect-AzAccount -Credential $Credential 68 | $context = Get-AzContext 69 | Select-AzSubscription -SubscriptionId $context.Subscription.Id 70 | 71 | Join-AzStorageAccountForAuth -ResourceGroupName $RG -StorageAccountName $S -DomainAccountType 'ComputerAccount' -OrganizationalUnitName 'Profiles Storage' -OverwriteExistingADObject 72 | -------------------------------------------------------------------------------- /Uploads/WVDScripts/002-FSLogix/Install-FSLogix.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | 3 | 4 | ########## 5 | # Helper # 6 | ########## 7 | #region Functions 8 | function LogInfo($message) { 9 | Log "Info" $message 10 | } 11 | 12 | function LogError($message) { 13 | Log "Error" $message 14 | } 15 | 16 | function LogSkip($message) { 17 | Log "Skip" $message 18 | } 19 | 20 | function LogWarning($message) { 21 | Log "Warning" $message 22 | } 23 | 24 | function Log { 25 | 26 | <# 27 | .SYNOPSIS 28 | Creates a log file and stores logs based on categories with tab seperation 29 | 30 | .PARAMETER category 31 | Category to put into the trace 32 | 33 | .PARAMETER message 34 | Message to be loged 35 | 36 | .EXAMPLE 37 | Log 'Info' 'Message' 38 | 39 | #> 40 | 41 | Param ( 42 | $category = 'Info', 43 | [Parameter(Mandatory = $true)] 44 | $message 45 | ) 46 | 47 | $date = get-date 48 | $content = "[$date]`t$category`t`t$message`n" 49 | Write-Verbose "$content" -verbose 50 | 51 | if (! $script:Log) { 52 | $File = Join-Path $env:TEMP "log.log" 53 | Write-Error "Log file not found, create new $File" 54 | $script:Log = $File 55 | } 56 | else { 57 | $File = $script:Log 58 | } 59 | Add-Content $File $content -ErrorAction Stop 60 | } 61 | 62 | function Set-Logger { 63 | <# 64 | .SYNOPSIS 65 | Sets default log file and stores in a script accessible variable $script:Log 66 | Log File name "executionCustomScriptExtension_$date.log" 67 | 68 | .PARAMETER Path 69 | Path to the log file 70 | 71 | .EXAMPLE 72 | Set-Logger 73 | Create a logger in 74 | #> 75 | 76 | Param ( 77 | [Parameter(Mandatory = $true)] 78 | $Path 79 | ) 80 | 81 | # Create central log file with given date 82 | 83 | $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" 84 | 85 | $scriptName = (Get-Item $PSCommandPath ).Basename 86 | $scriptName = $scriptName -replace "-", "" 87 | 88 | Set-Variable logFile -Scope Script 89 | $script:logFile = "executionCustomScriptExtension_" + $scriptName + "_" + $date + ".log" 90 | 91 | if ((Test-Path $path ) -eq $false) { 92 | $null = New-Item -Path $path -type directory 93 | } 94 | 95 | $script:Log = Join-Path $path $logfile 96 | 97 | Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" 98 | } 99 | #endregion 100 | 101 | Set-Logger "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog\FSLogix" # inside "executionCustomScriptExtension_$scriptName_$date.log" 102 | 103 | ##################### 104 | # 1 Extract FSLogix # 105 | ##################### 106 | LogInfo("######################") 107 | LogInfo("# 1. Extract FSLogix #") 108 | LogInfo("######################") 109 | 110 | $FSLogixArchivePath = Join-Path $PSScriptRoot "FSLogixApp.zip" 111 | 112 | LogInfo("Expanding Archive $FSLogixArchivePath into $PSScriptRoot") 113 | Expand-Archive -Path $FSLogixArchivePath -DestinationPath $PSScriptRoot 114 | LogInfo("Archive expanded") 115 | 116 | ##################### 117 | # 2 Install FSLogix # 118 | ##################### 119 | LogInfo("######################") 120 | LogInfo("# 2. Install FSLogix #") 121 | LogInfo("######################") 122 | 123 | # To get switches run cmd with '$path /?' 124 | 125 | $Switches = "/passive /norestart" 126 | $ExecutableName = "x64\Release\FSLogixAppsSetup.exe" 127 | $FSLogixExePath = Join-Path $PSScriptRoot $ExecutableName 128 | 129 | LogInfo("Trigger installation of file '$FSLogixExePath' with switches '$switches'") 130 | $Installer = Start-Process -FilePath $FSLogixExePath -ArgumentList $Switches -Wait -PassThru 131 | LogInfo("The exit code is $($Installer.ExitCode)") 132 | -------------------------------------------------------------------------------- /Uploads/WVDScripts/002-FSLogix/Set-FSLogix.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | 3 | <# 4 | Install and configure FSLogix 5 | 6 | CSE based on instructions at: 7 | https://docs.microsoft.com/en-us/azure/virtual-desktop/create-host-pools-user-profile#configure-the-fslogix-profile-container 8 | #> 9 | 10 | [cmdletbinding()] 11 | 12 | param( 13 | [parameter(Mandatory, ValueFromPipeline)] 14 | [object]$registryValues 15 | ) 16 | 17 | ########## 18 | # Helper # 19 | ########## 20 | #region Functions 21 | function LogInfo($message) { 22 | Log "Info" $message 23 | } 24 | 25 | function LogError($message) { 26 | Log "Error" $message 27 | } 28 | 29 | function LogSkip($message) { 30 | Log "Skip" $message 31 | } 32 | function LogWarning($message) { 33 | Log "Warning" $message 34 | } 35 | 36 | function Log { 37 | 38 | <# 39 | .SYNOPSIS 40 | Creates a log file and stores logs based on categories with tab seperation 41 | 42 | .PARAMETER category 43 | Category to put into the trace 44 | 45 | .PARAMETER message 46 | Message to be loged 47 | 48 | .EXAMPLE 49 | Log 'Info' 'Message' 50 | 51 | #> 52 | 53 | Param ( 54 | $category = 'Info', 55 | [Parameter(Mandatory = $true)] 56 | $message 57 | ) 58 | 59 | $date = get-date 60 | $content = "[$date]`t$category`t`t$message`n" 61 | Write-Verbose "$content" -verbose 62 | 63 | if (! $script:Log) { 64 | $File = Join-Path $env:TEMP "log.log" 65 | Write-Error "Log file not found, create new $File" 66 | $script:Log = $File 67 | } 68 | else { 69 | $File = $script:Log 70 | } 71 | Add-Content $File $content -ErrorAction Stop 72 | } 73 | 74 | function Set-Logger { 75 | <# 76 | .SYNOPSIS 77 | Sets default log file and stores in a script accessible variable $script:Log 78 | Log File name "executionCustomScriptExtension_$date.log" 79 | 80 | .PARAMETER Path 81 | Path to the log file 82 | 83 | .EXAMPLE 84 | Set-Logger 85 | Create a logger in 86 | #> 87 | 88 | Param ( 89 | [Parameter(Mandatory = $true)] 90 | $Path 91 | ) 92 | 93 | # Create central log file with given date 94 | 95 | $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" 96 | 97 | $scriptName = (Get-Item $PSCommandPath ).Basename 98 | $scriptName = $scriptName -replace "-", "" 99 | 100 | Set-Variable logFile -Scope Script 101 | $script:logFile = "executionCustomScriptExtension_" + $scriptName + "_" + $date + ".log" 102 | 103 | if ((Test-Path $path ) -eq $false) { 104 | $null = New-Item -Path $path -type directory 105 | } 106 | 107 | $script:Log = Join-Path $path $logfile 108 | 109 | Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" 110 | } 111 | #endregion 112 | 113 | 114 | Set-Logger "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog\FSLogix" # inside "executionCustomScriptExtension_$scriptName_$date.log" 115 | 116 | ##################### 117 | # Configure FSLogix # 118 | ##################### 119 | 120 | Write-Verbose "In function count: $($testArr.Count)" 121 | $testArr 122 | 123 | LogInfo("###############################") 124 | LogInfo("# 1. Retrieve Registry Values #") 125 | LogInfo("###############################") 126 | $fsLogixRegPath = "HKLM:\software\FSLogix" 127 | $expectedReqKey = 'Profiles' 128 | $expectedfsLogixRegKeyPath = Join-Path $fsLogixRegPath $expectedReqKey 129 | 130 | LogInfo("############################") 131 | LogInfo("# 2. Check Profiles RegKey #") 132 | LogInfo("############################") 133 | 134 | if (-not (Test-Path $expectedfsLogixRegKeyPath)) { 135 | LogInfo("RegexPath '$expectedfsLogixRegKeyPath' not existing. Creating") 136 | New-Item -Path $expectedfsLogixRegKeyPath -Force | Out-Null 137 | } 138 | else { 139 | LogInfo("RegexPath '$fsLogixRegPath' already existing. Creation skipped") 140 | } 141 | 142 | LogInfo("######################") 143 | LogInfo("# 3. Creating Values #") 144 | LogInfo("######################") 145 | 146 | $registryValues | ForEach-Object { 147 | LogInfo('Creating entry "{0}" of type "{1}" with value "{2}" in path "{3}"' -f $_.Name, $_.Type, $_.Value, $expectedfsLogixRegKeyPath) 148 | $inputObject = @{ 149 | Path = $expectedfsLogixRegKeyPath 150 | Name = $_.Name 151 | Value = $_.Value 152 | PropertyType = $_.Type 153 | } 154 | New-ItemProperty @inputObject -Force | Out-Null 155 | } -------------------------------------------------------------------------------- /Uploads/WVDScripts/002-FSLogix/Set-NTFSPermissions.ps1: -------------------------------------------------------------------------------- 1 | #Requires -RunAsAdministrator 2 | 3 | 4 | <# 5 | Assign Storage Account File Share Permissions 6 | 7 | CSE based on instructions at: 8 | https://docs.microsoft.com/en-us/azure/storage/files/storage-files-active-directory-enable 9 | https://docs.microsoft.com/en-us/azure/storage/files/storage-how-to-use-files-windows 10 | 11 | Using New-PSDrive to mount the drive. A good alternative is 'net use'. 12 | 13 | # For a not domain joined machine: 14 | net use $driveLetter \\$storageAccountName.file.core.windows.net\$fileShareName $storageAccountKey /user:Azure\$storageAccountName 15 | 16 | # For a domain joned machine (avoids keys): 17 | net use $driveLetter: \\$storageAccountName.file.core.windows.net\$fileShareName 18 | #> 19 | 20 | [cmdletbinding()] 21 | param( 22 | [Parameter( 23 | Mandatory = $true, 24 | HelpMessage = 'Name of the storage account to interact with' 25 | )] 26 | [string] $storageAccountName, 27 | 28 | [Parameter( 29 | Mandatory = $true, 30 | HelpMessage = 'Name of the file share within the storage account' 31 | )] 32 | [string] $fileShareUri, 33 | 34 | [Parameter( 35 | Mandatory = $true, 36 | HelpMessage = 'Key to access storage account' 37 | )] 38 | [System.Security.SecureString] $storageAccountKey, 39 | 40 | [Parameter( 41 | Mandatory = $true, 42 | HelpMessage = 'Domain containing the grop to assign permission for the file share. With or without ".onmicrosoft.com"' 43 | )] 44 | [string] $domain, 45 | 46 | [Parameter( 47 | Mandatory = $true, 48 | HelpMessage = 'Name of the group to assign file share access to' 49 | )] 50 | [string] $targetGroup, 51 | 52 | [Parameter( 53 | Mandatory = $false, 54 | HelpMessage = 'Drive letter to mount the drive to' 55 | )] 56 | [string] $driveLetter = 'Y' 57 | ) 58 | 59 | 60 | ########## 61 | # Helper # 62 | ########## 63 | #region Functions 64 | function LogInfo($message) { 65 | Log "Info" $message 66 | } 67 | 68 | function LogError($message) { 69 | Log "Error" $message 70 | } 71 | 72 | function LogSkip($message) { 73 | Log "Skip" $message 74 | } 75 | 76 | function LogWarning($message) { 77 | Log "Warning" $message 78 | } 79 | 80 | function Log { 81 | 82 | <# 83 | .SYNOPSIS 84 | Creates a log file and stores logs based on categories with tab seperation 85 | 86 | .PARAMETER category 87 | Category to put into the trace 88 | 89 | .PARAMETER message 90 | Message to be loged 91 | 92 | .EXAMPLE 93 | Log 'Info' 'Message' 94 | 95 | #> 96 | 97 | Param ( 98 | $category = 'Info', 99 | [Parameter(Mandatory = $true)] 100 | $message 101 | ) 102 | 103 | $date = get-date 104 | $content = "[$date]`t$category`t`t$message`n" 105 | Write-Verbose "$content" -verbose 106 | 107 | if (! $script:Log) { 108 | $File = Join-Path $env:TEMP "log.log" 109 | Write-Error "Log file not found, create new $File" 110 | $script:Log = $File 111 | } 112 | else { 113 | $File = $script:Log 114 | } 115 | Add-Content $File $content -ErrorAction Stop 116 | } 117 | 118 | function Set-Logger { 119 | <# 120 | .SYNOPSIS 121 | Sets default log file and stores in a script accessible variable $script:Log 122 | Log File name "executionCustomScriptExtension_$date.log" 123 | 124 | .PARAMETER Path 125 | Path to the log file 126 | 127 | .EXAMPLE 128 | Set-Logger 129 | Create a logger in 130 | #> 131 | 132 | Param ( 133 | [Parameter(Mandatory = $true)] 134 | [string] $Path 135 | ) 136 | 137 | # Create central log file with given date 138 | 139 | $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" 140 | 141 | $scriptName = (Get-Item $PSCommandPath ).Basename 142 | $scriptName = $scriptName -replace "-", "" 143 | 144 | Set-Variable logFile -Scope Script 145 | $script:logFile = "executionCustomScriptExtension_" + $scriptName + "_" + $date + ".log" 146 | 147 | if ((Test-Path $path ) -eq $false) { 148 | $null = New-Item -Path $path -type directory 149 | } 150 | 151 | $script:Log = Join-Path $path $logfile 152 | 153 | Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" 154 | } 155 | #endregion 156 | 157 | Set-Logger "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog\FSLogix" # inside "executionCustomScriptExtension_$scriptName_$date.log" 158 | 159 | # Mount the drive 160 | LogInfo('###################') 161 | LogInfo('# MOUNT THE DRIVE #') 162 | LogInfo('###################') 163 | 164 | # The value given to the root parameter of the New-PSDrive cmdlet is the host address for the storage account, 165 | # .file.core.windows.net for Azure Public Regions. $fileShare.StorageUri.PrimaryUri.Host is 166 | # used because non-Public Azure regions, such as sovereign clouds or Azure Stack deployments, will have different 167 | # hosts for Azure file shares (and other storage resources). 168 | $credential = New-Object System.Management.Automation.PSCredential -ArgumentList "AZURE\$($storageAccountName)", $storageAccountKey 169 | # Transform https://.file.core.windows.net/wvdprofile to '\\.file.core.windows.net\wvdprofile' 170 | 171 | $driveInputObject = @{ 172 | Name = $driveLetter 173 | PSProvider = 'FileSystem' 174 | Root = $fileShareUri 175 | Credential = $credential 176 | } 177 | LogInfo("Try to get drive '$driveLetter'") 178 | if (-not (Get-PSDrive -Name $driveLetter -ErrorAction SilentlyContinue)) { 179 | LogInfo('Mount Drive "{0}" from root "{1}"' -f $driveInputObject.Name, $driveInputObject.Root) 180 | try { 181 | New-PSDrive @driveInputObject -Persist -Verbose 182 | } 183 | catch { 184 | Write-Error $_.Exception.Message 185 | throw $_ 186 | } 187 | 188 | $drive = Get-PSDrive -Name $driveLetter 189 | LogInfo("Drive mounted: {0}" -f ($drive | Format-List | Out-String)) 190 | } 191 | else { 192 | LogInfo('Drive "{0}" from root "{1}" already mounted' -f $driveInputObject.Name, $driveInputObject.Root) 193 | } 194 | 195 | LogInfo('########################') 196 | LogInfo('# SET NTFS PERMISSIONS #') 197 | LogInfo('########################') 198 | 199 | LogInfo('Cleanup domain name') 200 | $domain = $domain.Replace('.onmicrosoft.com', '') 201 | 202 | # Assign permissions 203 | $command = "icacls {0}: /grant ('{1}\{2}:(M)'); icacls {0}: /grant ('Creator Owner:(OI)(CI)(IO)(M)'); icacls {0}: /remove ('Authenticated Users'); icacls {0}: /remove ('Builtin\Users')" -f $driveLetter, $domain, $targetGroup 204 | LogInfo("Run ACL command: '$command'") 205 | Invoke-Expression -Command $command 206 | LogInfo("ACLs set") 207 | LogInfo("Read ACLs") 208 | $readCommand = "icacls {0}:" -f $driveLetter 209 | LogInfo("Run command: '$readCommand'") 210 | $info = Invoke-Expression -Command $readCommand 211 | LogInfo($info | Format-List | Out-String) -------------------------------------------------------------------------------- /Uploads/WVDScripts/002-FSLogix/cse_run.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(SupportsShouldProcess = $true)] 2 | param ( 3 | # [Parameter(Mandatory = $true)] 4 | # [ValidateNotNullOrEmpty()] 5 | # [string] $storageAccountKey, 6 | 7 | [Parameter(Mandatory = $false)] 8 | [Hashtable] $DynParameters, 9 | 10 | [Parameter(Mandatory = $false)] 11 | [string] $AzureAdminUpn, 12 | 13 | [Parameter(Mandatory = $false)] 14 | [string] $AzureAdminPassword, 15 | 16 | [Parameter(Mandatory = $false)] 17 | [string] $domainJoinPassword, 18 | 19 | [Parameter(Mandatory = $false)] 20 | [ValidateNotNullOrEmpty()] 21 | #[string] $ConfigurationFilePath = (Join-Path $PSScriptRoot "fslogix.parameters.json") 22 | [string] $ConfigurationFileName = "fslogix.parameters.json" 23 | ) 24 | 25 | ##################################### 26 | 27 | ########## 28 | # Helper # 29 | ########## 30 | #region Functions 31 | function LogInfo($message) { 32 | Log "Info" $message 33 | } 34 | 35 | function LogError($message) { 36 | Log "Error" $message 37 | } 38 | 39 | function LogSkip($message) { 40 | Log "Skip" $message 41 | } 42 | 43 | function LogWarning($message) { 44 | Log "Warning" $message 45 | } 46 | 47 | function Log { 48 | 49 | <# 50 | .SYNOPSIS 51 | Creates a log file and stores logs based on categories with tab seperation 52 | 53 | .PARAMETER category 54 | Category to put into the trace 55 | 56 | .PARAMETER message 57 | Message to be loged 58 | 59 | .EXAMPLE 60 | Log 'Info' 'Message' 61 | 62 | #> 63 | 64 | Param ( 65 | $category = 'Info', 66 | [Parameter(Mandatory = $true)] 67 | $message 68 | ) 69 | 70 | $date = get-date 71 | $content = "[$date]`t$category`t`t$message`n" 72 | Write-Verbose "$content" -verbose 73 | 74 | if (! $script:Log) { 75 | $File = Join-Path $env:TEMP "log.log" 76 | Write-Error "Log file not found, create new $File" 77 | $script:Log = $File 78 | } 79 | else { 80 | $File = $script:Log 81 | } 82 | Add-Content $File $content -ErrorAction Stop 83 | } 84 | 85 | function Set-Logger { 86 | <# 87 | .SYNOPSIS 88 | Sets default log file and stores in a script accessible variable $script:Log 89 | Log File name "executionCustomScriptExtension_$date.log" 90 | 91 | .PARAMETER Path 92 | Path to the log file 93 | 94 | .EXAMPLE 95 | Set-Logger 96 | Create a logger in 97 | #> 98 | 99 | Param ( 100 | [Parameter(Mandatory = $true)] 101 | $Path 102 | ) 103 | 104 | # Create central log file with given date 105 | 106 | $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" 107 | 108 | $scriptName = (Get-Item $PSCommandPath ).Basename 109 | $scriptName = $scriptName -replace "-", "" 110 | 111 | Set-Variable logFile -Scope Script 112 | $script:logFile = "executionCustomScriptExtension_" + $scriptName + "_" + $date + ".log" 113 | 114 | if ((Test-Path $path ) -eq $false) { 115 | $null = New-Item -Path $path -type directory 116 | } 117 | 118 | $script:Log = Join-Path $path $logfile 119 | 120 | Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" 121 | } 122 | #endregion 123 | 124 | 125 | ## MAIN 126 | #Set-Logger "C:\WindowsAzure\CustomScriptExtension\Log" # inside "executionCustomScriptExtension_$date.log" 127 | Set-Logger "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog\FSLogix" # inside "executionCustomScriptExtension_$scriptName_$date.log" 128 | 129 | LogInfo("###################") 130 | LogInfo("## 0 - LOAD DATA ##") 131 | LogInfo("###################") 132 | #$storageaccountkey = $DynParameters.storageaccountkey 133 | 134 | $ConfigurationFilePath= Join-Path $PSScriptRoot $ConfigurationFileName 135 | 136 | $ConfigurationJson = Get-Content -Path $ConfigurationFilePath -Raw -ErrorAction 'Stop' 137 | 138 | try { $FSLogixConfig = $ConfigurationJson | ConvertFrom-Json -ErrorAction 'Stop' } 139 | catch { 140 | Write-Error "Configuration JSON content could not be converted to a PowerShell object" -ErrorAction 'Stop' 141 | } 142 | 143 | LogInfo("##################") 144 | LogInfo("## 1 - EVALUATE ##") 145 | LogInfo("##################") 146 | foreach ($config in $FSLogixConfig.fslogix) { 147 | 148 | if ($config.installFSLogix) { 149 | LogInfo("########################") 150 | LogInfo("## 2 - INSTALL FLOGIX ##") 151 | LogInfo("########################") 152 | LogInfo("Trigger FSLogix") 153 | 154 | # & "$PSScriptRoot\Install-FSLogix.ps1" 155 | if ($PSCmdlet.ShouldProcess("FSLogix", "Install")) { 156 | & "$PSScriptRoot\Install-FSLogix.ps1" 157 | LogInfo("FSLogix installed") 158 | } 159 | } 160 | 161 | if ($config.configureFSLogix) { 162 | LogInfo("###########################") 163 | LogInfo("## 3 - CONFIGURE FSLOGIX ##") 164 | LogInfo("###########################") 165 | foreach ($key in $config.profileContainerKeys) { 166 | LogInfo($key.Name) 167 | LogInfo($key.Type) 168 | LogInfo($key.Value) 169 | } 170 | 171 | $($config.profileContainerKeys).GetType() | Format-Table 172 | Write-Verbose "Before function count: $($testArr.Count)" 173 | 174 | # & "$PSScriptRoot\Configure-FSLogix.ps1" $config.profileContainerKeys 175 | if ($PSCmdlet.ShouldProcess("FSLogix", "Set")) { 176 | & "$PSScriptRoot\Set-FSLogix.ps1" $config.profileContainerKeys 177 | LogInfo("FSLogix configured") 178 | } 179 | } 180 | 181 | if ($config.NTFSPermission) { 182 | LogInfo("######################################################") 183 | LogInfo("## 4 - Set NTFS Permission on the share for FSLogix ##") 184 | LogInfo("######################################################") 185 | LogInfo($config.fileShareName) 186 | LogInfo($config.fileShareStorageAccountName) 187 | LogInfo($config.domain) 188 | LogInfo($config.targetGroup) 189 | 190 | $fileShareUri = "\\{0}.file.core.windows.net\{1}" -f $config.fileShareStorageAccountName, $config.fileShareName 191 | $storageAccountKey = ConvertTo-SecureString -String $DynParameters.storageaccountkey -AsPlainText -Force 192 | 193 | $fileShareInputObject = @{ 194 | storageAccountName = $config.fileShareStorageAccountName 195 | fileShareUri = $fileShareUri 196 | storageAccountKey = $storageAccountKey 197 | domain = $config.domain 198 | targetGroup = $config.targetGroup 199 | } 200 | $fileShareInputObject.Keys | ForEach-Object { LogInfo("Drive: Use param: '{0}' with value '{1}'" -f $_, $fileShareInputObject[$_]) } 201 | 202 | # & "$PSScriptRoot\Set-NTFSPermissions.ps1" @fileShareInputObject 203 | if ($PSCmdlet.ShouldProcess("NTFS Permissions on the share", "Set")) { 204 | & "$PSScriptRoot\Set-NTFSPermissions.ps1" @fileShareInputObject 205 | LogInfo("Permissions set") 206 | } 207 | } 208 | } -------------------------------------------------------------------------------- /Uploads/WVDScripts/003-NotepadPP/cse_run.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(SupportsShouldProcess = $true)] 2 | param ( 3 | [Parameter(Mandatory = $false)] 4 | [Hashtable] $DynParameters, 5 | 6 | [Parameter(Mandatory = $false)] 7 | [string] $AzureAdminUpn, 8 | 9 | [Parameter(Mandatory = $false)] 10 | [string] $AzureAdminPassword, 11 | 12 | [Parameter(Mandatory = $false)] 13 | [string] $domainJoinPassword, 14 | 15 | [Parameter(Mandatory = $false)] 16 | [ValidateNotNullOrEmpty()] 17 | [string] $ExecutableName = "npp.7.8.7.Installer.exe", 18 | 19 | [Parameter(Mandatory = $false)] 20 | [ValidateNotNullOrEmpty()] 21 | [string] $Switches = "/S /D=${Env:ProgramFiles(x86)}\Notepad++\" 22 | ) 23 | 24 | ##################################### 25 | 26 | ########## 27 | # Helper # 28 | ########## 29 | #region Functions 30 | function LogInfo($message) { 31 | Log "Info" $message 32 | } 33 | 34 | function LogError($message) { 35 | Log "Error" $message 36 | } 37 | 38 | function LogSkip($message) { 39 | Log "Skip" $message 40 | } 41 | function LogWarning($message) { 42 | Log "Warning" $message 43 | } 44 | 45 | function Log { 46 | 47 | <# 48 | .SYNOPSIS 49 | Creates a log file and stores logs based on categories with tab seperation 50 | 51 | .PARAMETER category 52 | Category to put into the trace 53 | 54 | .PARAMETER message 55 | Message to be loged 56 | 57 | .EXAMPLE 58 | Log 'Info' 'Message' 59 | 60 | #> 61 | 62 | Param ( 63 | $category = 'Info', 64 | [Parameter(Mandatory = $true)] 65 | $message 66 | ) 67 | 68 | $date = get-date 69 | $content = "[$date]`t$category`t`t$message`n" 70 | Write-Verbose "$content" -verbose 71 | 72 | if (! $script:Log) { 73 | $File = Join-Path $env:TEMP "log.log" 74 | Write-Error "Log file not found, create new $File" 75 | $script:Log = $File 76 | } 77 | else { 78 | $File = $script:Log 79 | } 80 | Add-Content $File $content -ErrorAction Stop 81 | } 82 | 83 | function Set-Logger { 84 | <# 85 | .SYNOPSIS 86 | Sets default log file and stores in a script accessible variable $script:Log 87 | Log File name "executionCustomScriptExtension_$date.log" 88 | 89 | .PARAMETER Path 90 | Path to the log file 91 | 92 | .EXAMPLE 93 | Set-Logger 94 | Create a logger in 95 | #> 96 | 97 | Param ( 98 | [Parameter(Mandatory = $true)] 99 | $Path 100 | ) 101 | 102 | # Create central log file with given date 103 | 104 | $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" 105 | 106 | $scriptName = (Get-Item $PSCommandPath ).Basename 107 | $scriptName = $scriptName -replace "-", "" 108 | 109 | Set-Variable logFile -Scope Script 110 | $script:logFile = "executionCustomScriptExtension_" + $scriptName + "_" + $date + ".log" 111 | 112 | if ((Test-Path $path ) -eq $false) { 113 | $null = New-Item -Path $path -type directory 114 | } 115 | 116 | $script:Log = Join-Path $path $logfile 117 | 118 | Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" 119 | } 120 | #endregion 121 | 122 | Set-Logger "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog\NPP" # inside "executionCustomScriptExtension_$scriptName_$date.log" 123 | 124 | ##################### 125 | # 1 Install Notepad ++ # 126 | ##################### 127 | LogInfo("########################") 128 | LogInfo("# 1. Install Notepad++ #") 129 | LogInfo("########################") 130 | 131 | # to get switches run cmd with '$path /?' 132 | 133 | #$Switches = "/S /D=${Env:ProgramFiles(x86)}\Notepad++\" 134 | #$ExecutableName = "npp.7.8.7.Installer.exe" 135 | $NPPExePath = Join-Path $PSScriptRoot $ExecutableName 136 | 137 | 138 | LogInfo("Trigger installation of file '$NPPExePath' with switches '$witches'") 139 | $Installer = Start-Process -FilePath $NPPExePath -ArgumentList $Switches -Wait -PassThru 140 | LogInfo("The exit code is $($Installer.ExitCode)") 141 | -------------------------------------------------------------------------------- /Uploads/WVDScripts/004-Teams/cse_run.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding(SupportsShouldProcess = $true)] 2 | param ( 3 | [Parameter(Mandatory = $false)] 4 | [Hashtable] $DynParameters, 5 | 6 | [Parameter(Mandatory = $false)] 7 | [string] $AzureAdminUpn, 8 | 9 | [Parameter(Mandatory = $false)] 10 | [string] $AzureAdminPassword, 11 | 12 | [Parameter(Mandatory = $false)] 13 | [string] $domainJoinPassword, 14 | 15 | [Parameter(Mandatory = $false)] 16 | [ValidateNotNullOrEmpty()] 17 | [string] $ExecutableName = "Teams_windows_x64.msi" 18 | 19 | #[Parameter(Mandatory = $false)] 20 | #[ValidateNotNullOrEmpty()] 21 | #[string] $Switches = "/S /D=${Env:ProgramFiles(x86)}\Notepad++\" 22 | ) 23 | 24 | ##################################### 25 | 26 | ########## 27 | # Helper # 28 | ########## 29 | #region Functions 30 | function LogInfo($message) { 31 | Log "Info" $message 32 | } 33 | 34 | function LogError($message) { 35 | Log "Error" $message 36 | } 37 | 38 | function LogSkip($message) { 39 | Log "Skip" $message 40 | } 41 | function LogWarning($message) { 42 | Log "Warning" $message 43 | } 44 | 45 | function Log { 46 | 47 | <# 48 | .SYNOPSIS 49 | Creates a log file and stores logs based on categories with tab seperation 50 | 51 | .PARAMETER category 52 | Category to put into the trace 53 | 54 | .PARAMETER message 55 | Message to be loged 56 | 57 | .EXAMPLE 58 | Log 'Info' 'Message' 59 | 60 | #> 61 | 62 | Param ( 63 | $category = 'Info', 64 | [Parameter(Mandatory = $true)] 65 | $message 66 | ) 67 | 68 | $date = get-date 69 | $content = "[$date]`t$category`t`t$message`n" 70 | Write-Verbose "$content" -verbose 71 | 72 | if (! $script:Log) { 73 | $File = Join-Path $env:TEMP "log.log" 74 | Write-Error "Log file not found, create new $File" 75 | $script:Log = $File 76 | } 77 | else { 78 | $File = $script:Log 79 | } 80 | Add-Content $File $content -ErrorAction Stop 81 | } 82 | 83 | function Set-Logger { 84 | <# 85 | .SYNOPSIS 86 | Sets default log file and stores in a script accessible variable $script:Log 87 | Log File name "executionCustomScriptExtension_$date.log" 88 | 89 | .PARAMETER Path 90 | Path to the log file 91 | 92 | .EXAMPLE 93 | Set-Logger 94 | Create a logger in 95 | #> 96 | 97 | Param ( 98 | [Parameter(Mandatory = $true)] 99 | $Path 100 | ) 101 | 102 | # Create central log file with given date 103 | 104 | $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" 105 | 106 | $scriptName = (Get-Item $PSCommandPath ).Basename 107 | $scriptName = $scriptName -replace "-", "" 108 | 109 | Set-Variable logFile -Scope Script 110 | $script:logFile = "executionCustomScriptExtension_" + $scriptName + "_" + $date + ".log" 111 | 112 | if ((Test-Path $path ) -eq $false) { 113 | $null = New-Item -Path $path -type directory 114 | } 115 | 116 | $script:Log = Join-Path $path $logfile 117 | 118 | Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" 119 | } 120 | #endregion 121 | 122 | Set-Logger "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog\Teams" # inside "executionCustomScriptExtension_$scriptName_$date.log" 123 | $MSIPath = "$($PSScriptRoot)\$ExecutableName" 124 | LogInfo("Installing teams from path $MSIPath") 125 | 126 | LogInfo("Setting registry key Teams") 127 | if ((Test-Path "HKLM:\Software\Microsoft\Teams") -eq $false) { 128 | New-Item -Path "HKLM:\Software\Microsoft\Teams" -Force 129 | } 130 | New-ItemProperty "HKLM:\Software\Microsoft\Teams" -Name "IsWVDEnvironment" -Value 1 -PropertyType DWord -Force 131 | LogInfo("Set IsWVDEnvironment DWord to value 1 successfully.") 132 | 133 | $scriptBlock = { msiexec /i $MSIPath /l*v "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog\Teams\InstallLog.txt" ALLUSER=1 ALLUSERS=1 } 134 | LogInfo("Invoking command with the following scriptblock: $scriptBlock") 135 | LogInfo("Install logs can be found in the InstallLog.txt file in this folder.") 136 | Invoke-Command $scriptBlock -Verbose 137 | 138 | LogInfo("Teams was successfully installed") -------------------------------------------------------------------------------- /Uploads/WVDScripts/downloads.parameters.json: -------------------------------------------------------------------------------- 1 | { 2 | "WVDSoftware": [ 3 | { 4 | "Url": "https://aka.ms/fslogix_download", 5 | "DestinationFilePath": "002-FSLogix/FSLogixApp.zip" 6 | }, 7 | { 8 | "Url": "https://github.com/notepad-plus-plus/notepad-plus-plus/releases/download/v7.8.7/npp.7.8.7.Installer.exe", 9 | "DestinationFilePath": "003-NotepadPP/npp.7.8.7.Installer.exe" 10 | }, 11 | { 12 | "Url": "https://teams.microsoft.com/downloads/desktopurl?env=production&plat=windows&arch=x64&managedInstaller=true&download=true", 13 | "DestinationFilePath": "004-Teams/Teams_windows_x64.msi" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /Uploads/WVDScripts/scriptExtensionMasterInstaller.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | Main script performing the Windows VM extension deployment. Executed steps: 4 | - find all ZIP files in the downloaded folder (including subfolder). 5 | - extract all ZIP files to _deploy folder by also creating the folder. 6 | - each ZIP is extracted to a subfolder _deploy\- where XXX is a number starting at 000. 7 | - find all CSE_Run.ps1 files in _deploy subfolders. 8 | - execute all CSE_Run.ps1 scripts found in _deploy subfolders in the order of folder names and passing the DynParameters parameter from this script. 9 | .Parameter DynParameters 10 | Hashtable parameter enabling to pass Key-Value parameter pairs. Example: @{"Environment"="Prod";"Debug"="True"} 11 | #> 12 | 13 | [CmdletBinding(DefaultParametersetName = 'None')] 14 | param( 15 | 16 | [Parameter(Mandatory = $true)] 17 | [string] $AzureAdminUpn, 18 | 19 | [Parameter(Mandatory = $true)] 20 | [string] $AzureAdminPassword, 21 | 22 | [Parameter(Mandatory = $true)] 23 | [string] $domainJoinPassword, 24 | 25 | $p = "", 26 | [Hashtable] [Parameter(Mandatory = $false)] 27 | $DynParameters 28 | ) 29 | 30 | ########## 31 | # Helper # 32 | ########## 33 | #region Functions 34 | function LogInfo($message) { 35 | Log "Info" $message 36 | } 37 | 38 | function LogError($message) { 39 | Log "Error" $message 40 | } 41 | 42 | function LogSkip($message) { 43 | Log "Skip" $message 44 | } 45 | function LogWarning($message) { 46 | Log "Warning" $message 47 | } 48 | 49 | function Log { 50 | 51 | <# 52 | .SYNOPSIS 53 | Creates a log file and stores logs based on categories with tab seperation 54 | 55 | .PARAMETER category 56 | Category to put into the trace 57 | 58 | .PARAMETER message 59 | Message to be loged 60 | 61 | .EXAMPLE 62 | Log 'Info' 'Message' 63 | 64 | #> 65 | 66 | Param ( 67 | $category = 'Info', 68 | [Parameter(Mandatory = $true)] 69 | $message 70 | ) 71 | 72 | $date = get-date 73 | $content = "[$date]`t$category`t`t$message`n" 74 | Write-Verbose "$content" -verbose 75 | 76 | if (! $script:Log) { 77 | $File = Join-Path $env:TEMP "log.log" 78 | Write-Error "Log file not found, create new $File" 79 | $script:Log = $File 80 | } 81 | else { 82 | $File = $script:Log 83 | } 84 | Add-Content $File $content -ErrorAction Stop 85 | } 86 | 87 | function Set-Logger { 88 | <# 89 | .SYNOPSIS 90 | Sets default log file and stores in a script accessible variable $script:Log 91 | Log File name "executionCustomScriptExtension_$date.log" 92 | 93 | .PARAMETER Path 94 | Path to the log file 95 | 96 | .EXAMPLE 97 | Set-Logger 98 | Create a logger in 99 | #> 100 | 101 | Param ( 102 | [Parameter(Mandatory = $true)] 103 | $Path 104 | ) 105 | 106 | # Create central log file with given date 107 | 108 | $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" 109 | Set-Variable logFile -Scope Script 110 | $script:logFile = "executionCustomScriptExtension_InitializeHost_$date.log" 111 | 112 | if ((Test-Path $path ) -eq $false) { 113 | $null = New-Item -Path $path -type directory 114 | } 115 | 116 | $script:Log = Join-Path $path $logfile 117 | 118 | Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" 119 | } 120 | #endregion 121 | 122 | Set-Logger "C:\WindowsAzure\Logs\Plugins\Microsoft.Compute.CustomScriptExtension\executionLog" # inside "executionCustomScriptExtension_$date.log" 123 | $ErrorActionPreference = 'Stop' 124 | LogInfo "Current working dir: $((Get-Location).Path)" 125 | 126 | LogInfo "Unpacking zip files" 127 | 128 | $zipPackages = Get-ChildItem -Filter "*.zip" -Recurse | sort -Property BaseName 129 | if($zipPackages){ 130 | LogInfo "Found $($zipPackages.count) zip packages" 131 | } 132 | else 133 | { 134 | LogError "No zip files found in the directory" 135 | } 136 | 137 | $i=0 138 | foreach ($zip in $zipPackages) 139 | { 140 | LogInfo "Unpacking $($zip.FullName)" 141 | Expand-Archive -Path $zip.FullName -DestinationPath "_deploy\$(($i++).ToString("000"))-$($zip.BaseName)" 142 | } 143 | LogInfo "Unpacking completed - Searching for CSE_Run.ps1 files" 144 | 145 | $PsScriptsToRun = Get-ChildItem -path "_deploy" -Filter "CSE_Run.ps1" -Recurse | sort -Property FullName 146 | 147 | if($PsScriptsToRun){ 148 | LogInfo "Found $($PsScriptsToRun.count) scripts" 149 | } 150 | else 151 | { 152 | LogError "No scripts found in the directory" 153 | } 154 | 155 | foreach ($scr in $PsScriptsToRun) 156 | { 157 | LogInfo "Running $($scr.FullName)" 158 | & $scr.FullName -DynParameters $DynParameters -AzureAdminUpn $AzureAdminUpn -AzureAdminPassword $AzureAdminPassword -domainJoinPassword $domainJoinPassword 159 | } 160 | LogInfo "Execution completed" --------------------------------------------------------------------------------