├── ARB ├── ARM │ ├── VMCopyOnPremiseWithExtensions.json │ ├── VMCopyOnPremiseWithExtensions_deploy.ps1 │ └── someCSE.ps1 └── readme.md ├── AVD ├── YT-Series │ ├── FSLogix │ │ ├── FSLogixGPO.ps1 │ │ ├── FSLogixNTFS.ps1 │ │ └── WriteRandomDataFile.ps1 │ ├── RDPShortpath │ │ └── RDPShortPathGPO.ps1 │ ├── customimage │ │ ├── 1-manualImageSelection.ps1 │ │ ├── 2-downloadDiskFromAzure.ps1 │ │ ├── 3-optimizeDisk.ps1 │ │ └── 4-optionalAddingLanguagePack.ps1 │ ├── readme.md │ └── remoteApp │ │ └── publishWindowsAppDetails.ps1 ├── intendedway │ ├── ARM │ │ ├── ARMDeploymentOfAVD.png │ │ ├── AVDOnPremise.json │ │ ├── deployAVDOnPremise.ps1 │ │ ├── proxy │ │ │ ├── AVDOnPremiseVMOnlyProxy.json │ │ │ ├── AvdOnHCIWithProxyDiffSubsARM.png │ │ │ └── deployAVDOnPremisevmOnlyProxy.ps1 │ │ └── readme.md │ ├── bicep │ │ └── bicepmodulespooled │ │ │ ├── addVm.bicep │ │ │ ├── appgrouproleassignment.bicep │ │ │ ├── applicationgroup.bicep │ │ │ ├── hostpool.bicep │ │ │ ├── hostpoolgalleryvm.bicep │ │ │ ├── vmroleassignment.bicep │ │ │ └── workspace.bicep │ └── readme.md ├── manually │ ├── ArcEnabledVMs.png │ ├── CreateAVDDesktop.ps1 │ ├── FSLogixGPO.ps1 │ ├── FSLogixNTFS.ps1 │ ├── NotePadPlusPlus.json │ ├── RdpShortPathGPO.ps1 │ ├── deploytoazure.png │ ├── readme.md │ └── vdivmsinhostpool.png └── readme.md ├── Learn.md ├── README.md ├── Setup ├── ARM │ └── customStorageIps │ │ ├── azuredeploy.json │ │ └── azuredeploy.parameters.json ├── best practices │ ├── PrepareNodeNics.ps1 │ ├── Test-RDMA │ │ ├── RDMAcounter.png │ │ ├── Test-RDMA.ps1 │ │ └── howto_test-rdma.md │ └── readme.md └── proxy │ ├── 23H2ProxyDeployment_12-07-2024.xlsx │ ├── ArcRegisterWithProxy.ps1 │ ├── Screenshot 2024-07-30 152348.png │ ├── Screenshot 2024-07-30 152444.png │ ├── Screenshot 2024-07-30 152522.png │ ├── SetProxy.ps1 │ ├── proxy.png │ └── readme.md └── TS ├── DumpHCIInfo.ps1 ├── DumpHCIInfo2.ps1 ├── GetNetworkATCEvents.ps1 └── readme.md /ARB/ARM/VMCopyOnPremiseWithExtensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "vmPrefix": { 6 | "type": "string", 7 | "defaultValue": "[take(toLower(resourceGroup().name), 10)]", 8 | "metadata": { 9 | "description": "This prefix will be used in combination with the VM number to create the VM name. This value includes the dash, so if using “rdsh” as the prefix, VMs would be named “rdsh-0”, “rdsh-1”, etc. You should use a unique prefix to reduce name collisions in Active Directory." 10 | } 11 | }, 12 | "virtualProcessorCount": { 13 | "type": "int", 14 | "metadata": { 15 | "description": "Virtual Processor Count" 16 | } 17 | }, 18 | "memoryMB": { 19 | "type": "int", 20 | "metadata": { 21 | "description": "The total amount of memory in megabytes" 22 | } 23 | }, 24 | "maximumMemoryMB": { 25 | "type": "int", 26 | "defaultValue": 0, 27 | "metadata": { 28 | "description": "This parameter is optional and only used if dynamicMemory = true. When using dynamic memory this setting is the maximum MB given to the VM." 29 | } 30 | }, 31 | "minimumMemoryMB": { 32 | "type": "int", 33 | "defaultValue": 0, 34 | "metadata": { 35 | "description": "This parameter is optional and only used if dynamicMemory = true. When using dynamic memory this setting is the minimum MB given to the VM." 36 | } 37 | }, 38 | "dynamicMemoryConfig": { 39 | "type": "bool", 40 | "defaultValue": false, 41 | "metadata": { 42 | "description": "True if you want to use a dynamic memory config." 43 | } 44 | }, 45 | "targetMemoryBuffer": { 46 | "type": "int", 47 | "defaultValue": 0, 48 | "metadata": { 49 | "description": "This parameter is optional and only used if dynamicMemory = true. When using dynamic memory this setting is the buffer of extra memory given to the VM." 50 | } 51 | }, 52 | "vmInstanceSuffixes": { 53 | "type": "array", 54 | "defaultValue": [ 55 | "0", 56 | "1" 57 | ], 58 | "metadata": { 59 | "description": "This is the suffix to the vm names. VM will be named '[vmPrefix]-[vmInstanceSuffixes]'" 60 | } 61 | }, 62 | "tagValues": { 63 | "type": "object", 64 | "defaultValue": {}, 65 | "metadata": { 66 | "description": "The tags to be assigned to the resources" 67 | } 68 | }, 69 | "location": { 70 | "type": "string", 71 | "metadata": { 72 | "description": "The location where the resources will be deployed." 73 | } 74 | }, 75 | "customLocationId": { 76 | "type": "string", 77 | "metadata": { 78 | "description": "A deployment target created and customized by your organization for creating virtual machines. The custom location is associated to an Azure Stack HCI cluster." 79 | } 80 | }, 81 | "vmAdministratorUsername": { 82 | "type": "string", 83 | "defaultValue": "", 84 | "metadata": { 85 | "description": "A username to be used as the virtual machine administrator account. The vmAdministratorAccountUsername and vmAdministratorAccountPassword parameters must both be provided. Otherwise, domain administrator credentials provided by domainAdministratorUsername and domainAdministratorPassword will be used." 86 | } 87 | }, 88 | "vmAdministratorPassword": { 89 | "type": "securestring", 90 | "defaultValue": "", 91 | "metadata": { 92 | "description": "The password associated with the virtual machine administrator account. The vmAdministratorAccountUsername and vmAdministratorAccountPassword parameters must both be provided. Otherwise, domain administrator credentials provided by domainAdministratorUsername and domainAdministratorPassword will be used." 93 | } 94 | }, 95 | "logicalNetworkId": { 96 | "type": "string", 97 | "metadata": { 98 | "description": "Full ARM resource ID of the AzureStackHCI virtual network used for the VMs." 99 | } 100 | }, 101 | "imageId": { 102 | "type": "string", 103 | "metadata": { 104 | "description": "Full ARM resource ID of the AzureStackHCI virtual machine image used for the VMs." 105 | } 106 | }, 107 | "cseURI": { 108 | "type": "string", 109 | "metadata": { 110 | "description": "description" 111 | } 112 | }, 113 | "sasToken": { 114 | "type": "string", 115 | "metadata": { 116 | "description": "description" 117 | } 118 | }, 119 | "param1": { 120 | "type": "string", 121 | "defaultValue": "Hello", 122 | "metadata": { 123 | "description": "description" 124 | } 125 | }, 126 | "param2": { 127 | "type": "string", 128 | "defaultValue": "World", 129 | "metadata": { 130 | "description": "description" 131 | } 132 | } 133 | }, 134 | "variables": {}, 135 | "resources": [ 136 | { 137 | "copy": { 138 | "name": "vmPrefix_vmInitialNumber_nic", 139 | "count": "[length(parameters('vmInstanceSuffixes'))]" 140 | }, 141 | "type": "Microsoft.AzureStackHCI/networkInterfaces", 142 | "apiVersion": "2023-09-01-preview", 143 | "name": "[format('{0}{1}-nic', parameters('vmPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 144 | "location": "[parameters('location')]", 145 | "extendedLocation": { 146 | "type": "CustomLocation", 147 | "name": "[parameters('customLocationId')]" 148 | }, 149 | "tags": "[parameters('tagValues')]", 150 | "properties": { 151 | "ipConfigurations": [ 152 | { 153 | "name": "[format('{0}{1}-nic', parameters('vmPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 154 | "properties": { 155 | "subnet": { 156 | "id": "[parameters('logicalNetworkId')]" 157 | } 158 | } 159 | } 160 | ] 161 | } 162 | }, 163 | { 164 | "copy": { 165 | "name": "vmPrefix_vmInitialNumber", 166 | "count": "[length(parameters('vmInstanceSuffixes'))]" 167 | }, 168 | "type": "Microsoft.HybridCompute/machines", 169 | "apiVersion": "2023-03-15-preview", 170 | "name": "[format('{0}{1}', parameters('vmPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 171 | "location": "[parameters('location')]", 172 | "kind": "HCI", 173 | "tags": "[parameters('tagValues')]", 174 | "identity": { 175 | "type": "SystemAssigned" 176 | } 177 | }, 178 | { 179 | "copy": { 180 | "name": "default", 181 | "count": "[length(parameters('vmInstanceSuffixes'))]" 182 | }, 183 | "scope": "[concat('Microsoft.HybridCompute/machines', '/', concat(parameters('vmPrefix'), parameters('vmInstanceSuffixes')[copyIndex()]))]", 184 | "type": "Microsoft.AzureStackHCI/virtualMachineInstances", 185 | "apiVersion": "2023-09-01-preview", 186 | "name": "default", 187 | "extendedLocation": { 188 | "type": "CustomLocation", 189 | "name": "[parameters('customLocationId')]" 190 | }, 191 | "properties": { 192 | "hardwareProfile": { 193 | "vmSize": "Custom", 194 | "processors": "[parameters('virtualProcessorCount')]", 195 | "memoryMB": "[parameters('memoryMB')]", 196 | "dynamicMemoryConfig": "[if(parameters('dynamicMemoryConfig'), createObject('maximumMemoryMB', parameters('maximumMemoryMB'), 'minimumMemoryMB', parameters('minimumMemoryMB'), 'targetMemoryBuffer', parameters('targetMemoryBuffer')), json('null'))]" 197 | }, 198 | "osProfile": { 199 | "adminUsername": "[parameters('vmAdministratorUsername')]", 200 | "adminPassword": "[parameters('vmAdministratorPassword')]", 201 | "windowsConfiguration": { 202 | "provisionVMAgent": true, 203 | "provisionVMConfigAgent": true 204 | }, 205 | "computerName": "[format('{0}{1}', parameters('vmPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]" 206 | }, 207 | "storageProfile": { 208 | "imageReference": { 209 | "id": "[parameters('imageId')]" 210 | } 211 | }, 212 | "networkProfile": { 213 | "networkInterfaces": [ 214 | { 215 | "id": "[resourceId('Microsoft.AzureStackHCI/networkinterfaces', format('{0}{1}-nic', parameters('vmPrefix'), parameters('vmInstanceSuffixes')[copyIndex()]))]" 216 | } 217 | ] 218 | } 219 | }, 220 | "dependsOn": [ 221 | "vmPrefix_vmInitialNumber", 222 | "vmPrefix_vmInitialNumber_nic" 223 | ] 224 | }, 225 | { 226 | "copy": { 227 | "name": "vmPrefix_vmInitialNumber_runcommand", 228 | "count": "[length(parameters('vmInstanceSuffixes'))]" 229 | }, 230 | "type": "Microsoft.HybridCompute/machines/runCommands", 231 | "apiVersion": "2023-10-03-preview", 232 | "name": "[format('{0}{1}/runCommands', parameters('vmPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 233 | "location": "[parameters('location')]", 234 | "properties": { 235 | "source": { 236 | "script": "$cap = Get-WindowsCapability -online -name OpenSSH.Server~~~~0.0.1.0; add-WindowsCapability -online -Name $cap.name ;Start-Service sshd;Set-Service -Name sshd -StartupType Automatic; New-NetFirewallRule -Name OpenSSH-Server-In-TCP -DisplayName OpenSSHServer -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22;" 237 | }, 238 | "timeoutInSeconds": 600, 239 | "treatFailureAsDeploymentFailure": false 240 | }, 241 | "dependsOn": [ 242 | "default" 243 | ] 244 | }, 245 | { 246 | "copy": { 247 | "name": "vmPrefix_vmInitialNumber_customscript", 248 | "count": "[length(parameters('vmInstanceSuffixes'))]" 249 | }, 250 | "type": "Microsoft.HybridCompute/machines/extensions", 251 | "apiVersion": "2023-03-15-preview", 252 | "name": "[format('{0}{1}/CustomScriptExtension', parameters('vmPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 253 | "location": "[parameters('location')]", 254 | "properties": { 255 | "publisher": "Microsoft.Compute", 256 | "type": "CustomScriptExtension", 257 | "autoUpgradeMinorVersion": true, 258 | "settings": {}, 259 | "protectedSettings": { 260 | "commandToExecute": "[concat('powershell.exe -ExecutionPolicy Unrestricted -File ',last(split(parameters('cseURI'),'/')), ' ', parameters('param1'), ' ',parameters('param2'))]", 261 | "fileUris": [ 262 | "[concat(parameters('cseURI'),'?',parameters('sasToken'))]" 263 | ] 264 | } 265 | }, 266 | "dependsOn": [ 267 | "vmPrefix_vmInitialNumber_runcommand" 268 | ] 269 | }, 270 | { 271 | "copy": { 272 | "name": "vmPrefix_vmInitialNumber_azmonitor", 273 | "count": "[length(parameters('vmInstanceSuffixes'))]" 274 | }, 275 | "type": "Microsoft.HybridCompute/machines/extensions", 276 | "apiVersion": "2023-03-15-preview", 277 | "name": "[format('{0}{1}/azmonitor', parameters('vmPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 278 | "location": "[parameters('location')]", 279 | "properties": { 280 | "publisher": "Microsoft.Azure.Monitor", 281 | "type": "AzureMonitorWindowsAgent", 282 | "typeHandlerVersion": "1.5", 283 | "autoUpgradeMinorVersion": true 284 | }, 285 | "dependsOn": [ 286 | "vmPrefix_vmInitialNumber_customscript" 287 | ] 288 | } 289 | ], 290 | "outputs": {} 291 | } -------------------------------------------------------------------------------- /ARB/ARM/VMCopyOnPremiseWithExtensions_deploy.ps1: -------------------------------------------------------------------------------- 1 | 2 | $SubscriptionId = 'a2b.....e6f' # your Azure subscription ID 3 | $tenantId = '47f489.....5aab0' # your Azure Entra ID tenant ID 4 | $resourceGroup = "rg-M.....2-res" # please change me! 5 | $location = "westeurope" 6 | $administratorAccountPassword = 'L.....!' # please change me! 7 | $administratorAccountUsername = "b.....k" # please change me! 8 | $vmPrefix = "HCIVM" # please change me! 9 | 10 | az login --use-device-code --tenant $tenantId 11 | az account set --subscription $SubscriptionId 12 | az group create --name $resourceGroup --location $resourceGroup 13 | 14 | # This will deploy an array of Azure Arc managed VMs 15 | # including a AzMonitor agent installed, a run commmand to install SSH access and a custom script extension to execute a script from a blob storage. 16 | # https://learn.microsoft.com/en-us/azure-stack/hci/manage/azure-arc-vm-management-overview 17 | az deployment group create ` 18 | --name $("vm_" + ([datetime]::Now).ToString('dd-MM-yy_HH_mm')) ` 19 | --template-file "$((Get-Location).Path)\VMCopyOnPremiseWithExtensions.json" ` 20 | --resource-group $resourceGroup ` 21 | --parameters location=$location ` 22 | vmPrefix=$vmPrefix ` 23 | vmAdministratorPassword=$administratorAccountPassword ` 24 | vmAdministratorUsername=$administratorAccountUsername ` 25 | logicalNetworkId='/subscriptions/a2b.....e6f/resourceGroups/rg-M.....2-res/providers/microsoft.azurestackhci/logicalnetworks/lnet.....static' ` 26 | customLocationId='/subscriptions/a2b.....e6f/resourceGroups/rg-M.....2-res/providers/Microsoft.ExtendedLocation/customLocations/.....cl' ` 27 | imageId="/subscriptions/a2b.....e6f/resourceGroups/rg-M.....2-res/providers/microsoft.azurestackhci/galleryimages/W11.....M365Opt" ` 28 | memoryMB=8192 ` 29 | virtualProcessorCount=6 ` 30 | vmInstanceSuffixes="[""1"",""2""]" ` 31 | param1='Hello' ` 32 | param2='World' ` 33 | cseURI='https://......blob.core.windows.net/somecon....tainer/someCSE.ps1' ` 34 | tagValues=$('{\"CreatedBy\": \"bfrank\",\"deploymentDate\": \"'+ $(([datetime]::Now).ToString('dd-MM-yyyy_HH_mm')) + '\",\"Service\": \"ARB\",\"Environment\": \"PoC\"}') ` 35 | sasToken='"sp=r&st=2024-0....."' # please replace with a valid SAS read acces token ! and yes keep the '"...."' format when launching az deployment group create from PowerShell 36 | 37 | 38 | -------------------------------------------------------------------------------- /ARB/ARM/someCSE.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory=$True,Position=1)] 3 | [string] $param1, 4 | 5 | [Parameter(Mandatory=$True,Position=2)] 6 | [string] $param2 7 | ) 8 | 9 | $tmpDir = "c:\temp\" 10 | 11 | #create folder if it doesn't exist 12 | if (!(Test-Path $tmpDir)) { mkdir $tmpDir -force} 13 | 14 | Start-Transcript "$tmpDir\CSE.log" 15 | 16 | "I was run at {0} called with param1: {1} and param2: {2}" -f (get-date), $param1, $param2 17 | 18 | stop-Transcript -------------------------------------------------------------------------------- /ARB/readme.md: -------------------------------------------------------------------------------- 1 | # Azure Arc VM management on Azure Stack HCI 23H2 (provided by Azure Arc Resource Bridge) 2 | [Azure Arc VM management](https://learn.microsoft.com/en-us/azure-stack/hci/manage/azure-arc-vm-management-overview) 3 | With this version of AzStackHCI (23H2) - Azure Arc VM management setup is included in the whole HCI setup, i.e. you do not need to use these scripts deposited here. 4 | 5 | ## Deprecated: Content related to the Azure Arc Resource Bridge (ARB) on AzStack 22H2 6 | [22H2 branch](https://github.com/bfrankMS/AzStackHCI/tree/22H2/ARB) 7 | >Note: Azure Arc Resource Bridge was never released on 22H2 - it was first officially launched on 23H2. Since Feb 2024 functionality (on 22H2) was limited. Over the time I expect it to go completely offline for 22H2. 8 | 9 | ## The component that allows you to provision VMs onto your HCI via the Azure Portal 10 | [What is Azure Arc resource bridge?](https://learn.microsoft.com/en-us/azure/azure-arc/resource-bridge/overview) -------------------------------------------------------------------------------- /AVD/YT-Series/FSLogix/FSLogixGPO.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | This will download the FSLogix GPO files and create a GPO with the settings for FSLogix. 3 | pls adjust variables to match your environment. 4 | use at your own risk. 5 | #> 6 | 7 | $OUSuffix = "OU=AVDHosts,OU=HCI" #the part after the "...,DC=powerkurs,DC=local" so e.g. "OU=HostPool1,OU=AVD" 8 | $SharePaths =@("\\SOFS\Profile1","\\SOFS\Profile2") 9 | 10 | #region Download FSLogix GPO Profile 11 | #this will be our temp folder - need it for download / logging 12 | $tmpDir = "c:\temp\" 13 | 14 | #create folder if it doesn't exist 15 | if (!(Test-Path $tmpDir)) { mkdir $tmpDir -force } 16 | 17 | #downloading FSLogix. 18 | Write-Output "downloading fslogix" 19 | 20 | $tempPath = "$tmpDir\FSLogix" 21 | $destinationPath = "$tmpDir\FSLogix.zip" 22 | if (!(Test-Path $destinationPath)) { 23 | "downloading fslogix" 24 | Invoke-WebRequest -Uri "https://aka.ms/fslogix_download" -OutFile $destinationPath -verbose 25 | Expand-Archive $destinationPath -DestinationPath $tempPath -Force -verbose 26 | } 27 | #endregion 28 | 29 | #region Copy the fslogix profile files to the right subfolder. 30 | 31 | $fqdn = (Get-WmiObject Win32_ComputerSystem).Domain 32 | $policyDestination = "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\" 33 | 34 | mkdir $policyDestination -Force 35 | mkdir "$policyDestination\en-us" -Force 36 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\*" -filter "*.admx" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions" -Force -Verbose 37 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\*" -filter "*.adml" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\en-us" -Force -Verbose 38 | #endregion 39 | 40 | #region Create & Modify GPO 41 | $gpoNamePrefix = "AVD FSLogix GPO" 42 | $gpoName = $gpoNamePrefix + " - {0}" -f [datetime]::Now.ToString('dd-MM-yy_HHmmss') 43 | New-GPO -Name $gpoName 44 | $FSLogixRegKeys = @{ 45 | Enabled = 46 | @{ 47 | Type = "DWord" 48 | Value = 1 #set to 1 to enable. 49 | } 50 | VHDLocations = 51 | @{ 52 | Type = "String" 53 | Value = $SharePaths -join ';' #"\\SOFS\Profile1;\\SOFS\Profile2" #needs to have a semicolon as separator 54 | } 55 | DeleteLocalProfileWhenVHDShouldApply = 56 | @{ 57 | Type = "DWord" 58 | Value = 1 59 | } 60 | VolumeType = 61 | @{ 62 | Type = "String" 63 | Value = "VHDX" 64 | } 65 | SizeInMBs = 66 | @{ 67 | Type = "DWord" 68 | Value = 30000 69 | } 70 | IsDynamic = 71 | @{ 72 | Type = "DWord" 73 | Value = 1 74 | } 75 | PreventLoginWithFailure = 76 | @{ 77 | Type = "DWord" 78 | Value = 0 79 | } 80 | LockedRetryInterval = 81 | @{ 82 | Type = "DWord" 83 | Value = 10 84 | } 85 | LockedRetryCount = 86 | @{ 87 | Type = "DWord" 88 | Value = 5 89 | } 90 | FlipFlopProfileDirectoryName = 91 | @{ 92 | Type = "DWord" 93 | Value = 1 94 | } 95 | } 96 | 97 | foreach ($item in $FSLogixRegKeys.GetEnumerator()) { 98 | "{0}:{1}:{2}" -f $item.Name, $item.Value.Type, $item.Value.Value 99 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\SOFTWARE\fslogix\profiles" -ValueName $($item.Name) -Value $($item.Value.Value) -Type $($item.Value.Type) 100 | } 101 | 102 | #enable path exclusions in windows defender for fslogix profiles 103 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows Defender\Exclusions" -ValueName "Exclusions_Paths" -Value 1 -Type DWord 104 | 105 | $excludeList = @" 106 | %ProgramFiles%\FSLogix\Apps\frxdrv.sys, 107 | %ProgramFiles%\FSLogix\Apps\frxdrvvt.sys, 108 | %ProgramFiles%\FSLogix\Apps\frxccd.sys, 109 | %TEMP%\*.VHD, 110 | %TEMP%\*.VHDX, 111 | {{SOFSPathsPlaceholder}} 112 | %Windir%\TEMP\*.VHD, 113 | %Windir%\TEMP\*.VHDX 114 | "@ 115 | 116 | #adding SOFS paths to the exclusion list 117 | $SOFSPathsPlaceholder = $SharePaths | % { "$_\*\*.VHD,"} 118 | $SOFSPathsPlaceholder += $SharePaths | % { "$_\*\*.VHDX,"} 119 | 120 | $excludeList = ($excludeList -replace "{{SOFSPathsPlaceholder}}", $SOFSPathsPlaceholder ).Replace(' ',"`n") 121 | 122 | foreach ($item in $($excludeList -split ',')) { 123 | $item.Trim() 124 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows Defender\Exclusions\Paths" -ValueName "$($item.Trim())" -Value 0 -Type String 125 | } 126 | 127 | #endregion 128 | 129 | #region Link GPO to correct OU (and delete orphaned GPOs) 130 | Import-Module ActiveDirectory 131 | $DomainPath = $((Get-ADDomain).DistinguishedName) # e.g."DC=contoso,DC=azure" 132 | 133 | $OUPath = $($($OUSuffix + "," + $DomainPath).Split(',').trim() | where { $_ -ne "" }) -join ',' 134 | Write-Output "creating FSLOGIX GPO to OU: $OUPath" 135 | 136 | $existingGPOs = (Get-GPInheritance -Target $OUPath).GpoLinks | Where-Object DisplayName -Like "$gpoNamePrefix*" 137 | 138 | if ($null -ne $existingGPOs) { 139 | Write-Output "removing conflicting GPOs" 140 | $existingGPOs | Remove-GPLink -Verbose 141 | } 142 | 143 | 144 | New-GPLink -Name $gpoName -Target $OUPath -LinkEnabled Yes -verbose 145 | 146 | #cleanup existing but unlinked fslogix GPOs 147 | $existingGPOs | % { [xml]$Report = Get-GPO -guid $_.GpoId | Get-GPOReport -ReportType XML; if (!($Report.GPO.LinksTo)) { Remove-GPO -Guid $_.GpoId -Verbose } } 148 | 149 | #endregion -------------------------------------------------------------------------------- /AVD/YT-Series/FSLogix/FSLogixNTFS.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | This script is used to set the NTFS permissions on the FSLogix profile share. 4 | pls adjust variables to match your environment. 5 | use at your own risk. 6 | #> 7 | 8 | $DomainName = "myavd" #pls enter your domain name. 9 | $AVDUsers = "AVDUsers" 10 | 11 | #1st remove all exiting permissions. 12 | $acl = Get-Acl "\\Sofs\Profile1" # "\\Sofs\Profile2" 13 | 14 | $acl.Access | % { $acl.RemoveAccessRule($_) } 15 | $acl.SetAccessRuleProtection($true, $false) 16 | $acl | Set-Acl 17 | #add full control for 'the usual suspects' 18 | $users = @("$DomainName\Domain Admins", "System", "Administrators", "Creator Owner" ) 19 | foreach ($user in $users) { 20 | $new = $user, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" 21 | $accessRule = new-object System.Security.AccessControl.FileSystemAccessRule $new 22 | $acl.AddAccessRule($accessRule) 23 | $acl | Set-Acl 24 | } 25 | 26 | #add read & write on parent folder ->required for FSLogix - no inheritence 27 | $allowAVD = "$AVDUsers", "ReadData, AppendData, ExecuteFile, ReadAttributes, Synchronize", "None", "None", "Allow" 28 | $accessRule = new-object System.Security.AccessControl.FileSystemAccessRule $allowAVD 29 | $acl.AddAccessRule($accessRule) 30 | $acl | Set-Acl -------------------------------------------------------------------------------- /AVD/YT-Series/FSLogix/WriteRandomDataFile.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | Using Powershell to write Random Data to Storage => $numOfOutputFiles X $totalOutputFileSizeInBytes 4 | Notes: 5 | This script will write random data to a file to test storage performance. 6 | 1 GB onto SSD with moderate CPU will take approx 40sec 7 | e.g. 7Zip will give 0% compression ratio, i.e. output is as large as original file. 8 | 9 | alternative C:\ClusterStorage\UserStorage_1\rubbish> fsutil file createNew test5.txt $size 10 | #> 11 | 12 | $outputFileSizeInBytes = 100MB 13 | $numOfOutputFiles = 10 14 | $outputPath = "$env:HOMEPATH\documents\random.data" #"c:\temp\random.data" 15 | 16 | Write-Host -ForegroundColor Green "Writing $numOfOutputFiles * $($outputFileSizeInBytes/1MB) (MB) = $(($numOfOutputFiles*$outputFileSizeInBytes)/1GB) (GB)" 17 | 18 | 19 | $starttime = Get-Date 20 | "Starting at {0:dd-MM-yyyy HH:mm:ss}" -f $(Get-Date) 21 | 22 | for ($i = 1; $i -le $numOfOutputFiles; $i++) 23 | { 24 | #create an area of random data. 25 | $data = New-Object 'byte[]' $outputFileSizeInBytes 26 | $rnd = [System.Random]::new() 27 | $rnd.NextBytes($data) 28 | 29 | #create Path if not exists 30 | $parentPath = Split-Path $outputPath -Parent 31 | if (!(Test-Path -Path $parentPath )) 32 | { 33 | mkdir $parentPath 34 | } 35 | 36 | #write out random data 37 | (split-path $outputPath -Leaf) -match "(.*)\.(.*)" #append $i to filename e.g. "random.1.data" 38 | $path = (split-path $outputPath) + "\" + $Matches[1] + ".$i." + $Matches[2] 39 | [System.IO.File]::WriteAllBytes($path,$data) 40 | } 41 | 42 | "Finished at {0:dd-MM-yyyy HH:mm:ss}" -f $(Get-Date) 43 | "Elapsed time: {0:dd'dy:'hh'hr:'mm'min:'ss'sec'}" -f $((Get-Date) - $starttime) 44 | -------------------------------------------------------------------------------- /AVD/YT-Series/RDPShortpath/RDPShortPathGPO.ps1: -------------------------------------------------------------------------------- 1 | # https://docs.microsoft.com/en-us/azure/virtual-desktop/shortpath 2 | # https://learn.microsoft.com/en-us/azure/virtual-desktop/administrative-template?tabs=group-policy-domain 3 | # you then need to do add the GPO to your AD see: 4 | # https://docs.microsoft.com/en-us/azure/virtual-desktop/shortpath#configure-rdp-shortpath-for-managed-networks 5 | 6 | 7 | #region Download RDP Shortpath GPO Profile 8 | $OUSuffix = "OU=AVDHosts,OU=HCI" #the part after the "...,DC=powerkurs,DC=local" so e.g. "OU=HostPool1,OU=AVD" 9 | $tmpDir = "c:\temp" 10 | Write-Output "downloading avdgpo" 11 | 12 | $tempPath = "$tmpDir\avdgpo" 13 | if (!(Test-Path $tempPath)) { 14 | "downloading avdgpo" 15 | Invoke-WebRequest -Uri "https://aka.ms/avdgpo" -OutFile "$tmpDir\avdgpo.cab" -Verbose 16 | expand "$tmpDir\avdgpo.cab" "$tmpDir\avdgpo.zip" 17 | Expand-Archive "$tmpDir\avdgpo.zip" -DestinationPath $tempPath -Force -Verbose 18 | } 19 | #endregion 20 | 21 | #region Copy the terminalserver-avd profile files to the right subfolder. 22 | 23 | $fqdn = (Get-WmiObject Win32_ComputerSystem).Domain 24 | $policyDestination = "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\" 25 | 26 | mkdir $policyDestination -Force 27 | mkdir "$policyDestination\en-us" -Force 28 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\*" -Filter "*.admx" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions" -Force -Verbose 29 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\en-us\terminalserver-avd.adml" -Filter "*.adml" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\en-us" -Force -Verbose 30 | #endregion 31 | 32 | #region Create & Modify GPO 33 | $gpoNamePrefix = "AVD RDP Shortpath GPO" 34 | $gpoName = $gpoNamePrefix + " - {0}" -f [datetime]::Now.ToString('dd-MM-yy_HHmmss') 35 | New-GPO -Name $gpoName 36 | 37 | $RDPShortPathRegKeys = @{ 38 | fUseUdpPortRedirector = 39 | @{ 40 | Type = "DWord" 41 | Value = 1 #set to 1 to enable. 42 | } 43 | UdpRedirectorPort = 44 | @{ 45 | Type = "DWord" 46 | Value = 3390 #set to 1 to enable. 47 | } 48 | } 49 | 50 | foreach ($item in $RDPShortPathRegKeys.GetEnumerator()) { 51 | "{0}:{1}:{2}" -f $item.Name, $item.Value.Type, $item.Value.Value 52 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" -ValueName $($item.Name) -Value $($item.Value.Value) -Type $($item.Value.Type) 53 | } 54 | 55 | $FWallRuleRegKeys = @{ 56 | 'RemoteDesktop-Shortpath-UDP-In' = 57 | @{ 58 | Type = 'String' 59 | Value = 'v2.31|Action=Allow|Active=TRUE|Dir=In|Protocol=17|LPort=3390|App=%SystemRoot%\system32\svchost.exe|Name=Remote Desktop - Shortpath (UDP-In)|EmbedCtxt=@FirewallAPI.dll,-28752|' 60 | } 61 | } 62 | foreach ($item in $FWallRuleRegKeys.GetEnumerator()) { 63 | "{0}:{1}:{2}" -f $item.Name, $item.Value.Type, $item.Value.Value 64 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\WindowsFirewall\FirewallRules" -ValueName $($item.Name) -Value $($item.Value.Value) -Type $($item.Value.Type) 65 | } 66 | #endregion 67 | 68 | #region Link GPO to correct OU (and delete orphaned GPOs) 69 | Import-Module ActiveDirectory 70 | $DomainPath = $((Get-ADDomain).DistinguishedName) # e.g."DC=contoso,DC=azure" 71 | 72 | $OUPath = $($($OUSuffix + "," + $DomainPath).Split(',').trim() | Where-Object { $_ -ne "" }) -join ',' 73 | Write-Output "creating avdgpo GPO to OU: $OUPath" 74 | 75 | 76 | $existingGPOs = (Get-GPInheritance -Target $OUPath).GpoLinks | Where-Object DisplayName -Like "$gpoNamePrefix*" 77 | 78 | if ($null -ne $existingGPOs) { 79 | Write-Output "removing conflicting GPOs" 80 | $existingGPOs | Remove-GPLink -Verbose 81 | } 82 | 83 | New-GPLink -Name $gpoName -Target $OUPath -LinkEnabled Yes -verbose 84 | 85 | #cleanup existing but unlinked fslogix GPOs 86 | $existingGPOs | % { [xml]$Report = Get-GPO -guid $_.GpoId | Get-GPOReport -ReportType XML; if (!($Report.GPO.LinksTo)) { Remove-GPO -Guid $_.GpoId -Verbose } } 87 | 88 | #endregion 89 | 90 | -------------------------------------------------------------------------------- /AVD/YT-Series/customimage/1-manualImageSelection.ps1: -------------------------------------------------------------------------------- 1 | #Make sure you have the Azure modules required 2 | 3 | $modules = @("Az.Accounts","Az.Resources","Az.Compute") 4 | 5 | foreach ($module in $modules) { 6 | if (!(Get-Module -Name $module -ListAvailable)) { 7 | Install-Module -Name $module -Force -Verbose 8 | } 9 | } 10 | 11 | #login to Azure 12 | Login-AzAccount -Environment AzureCloud -UseDeviceAuthentication 13 | 14 | #hit the right subscription 15 | Get-AzSubscription | Out-GridView -Title "Select the right subscription" -OutputMode Single | Select-AzSubscription 16 | 17 | #select a location near you 18 | $location = Get-AzLocation | Out-GridView -Title "Select your location (e.g. westeurope)" -OutputMode Single 19 | 20 | #region select an Azure AVD Image (e.g. Windows 11) and create an Azure disk of it for later download (to onprem) 21 | #get the AVDs group published images by selecting 'microsoftwindowsdesktop' 22 | $imPub = Get-AzVMImagePublisher -Location $($location.Location) | Out-GridView -Title "Select image publisher (e.g. 'microsoftwindowsdesktop')" -OutputMode Single 23 | 24 | #select the AVD Desktop OS of interest e.g. 'windows-11' 25 | $PublisherOffer = Get-AzVMImageOffer -Location $($location.Location) -PublisherName $($imPub.PublisherName) | Out-GridView -Title "Select your offer (e.g. windows-11)" -OutputMode Single 26 | 27 | # select the AVD version e.g. 'win11-21h2-avd' 28 | $VMImageSKU = (Get-AzVMImageSku -Location $($location.Location) -PublisherName $($imPub.PublisherName) -Offer $PublisherOffer.Offer).Skus | Out-GridView -Title "Select your imagesku (e.g. win11-22h2-avd)" -OutputMode Single 29 | 30 | #select latest version 31 | $VMImage = Get-AzVMImage -Location $($location.Location) -PublisherName $PublisherOffer.PublisherName -Offer $PublisherOffer.Offer -Skus $VMImageSKU | Out-GridView -Title "Select your version (highest build number)" -OutputMode Single 32 | 33 | #Create a VHDX (Gen2) from this image 34 | $imageOSDiskRef = @{Id = $vmImage.Id} 35 | $diskRG = Get-AzResourceGroup | Out-GridView -Title "Select The Target Resource Group" -OutputMode Single 36 | $diskName = "disk-" + $vmImage.Skus 37 | $newdisk = New-AzDisk -ResourceGroupName $diskRG.ResourceGroupName -DiskName "$diskName" -Disk $(New-AzDiskConfig -ImageReference $imageOSDiskRef -Location $location.Location -CreateOption FromImage -HyperVGeneration V2 -OsType Windows ) 38 | 39 | Write-Host "You should now have a new disk named $($newdisk.name) in your resourcegroup" -ForegroundColor Green 40 | #endregion -------------------------------------------------------------------------------- /AVD/YT-Series/customimage/2-downloadDiskFromAzure.ps1: -------------------------------------------------------------------------------- 1 | #region Create a temp. download link and download the disk as virtual disk (.vhd) 2 | $AccessSAS = $newdisk | Grant-AzDiskAccess -DurationInSecond ([System.TimeSpan]::Parse("05:00:00").TotalSeconds) -Access 'Read' 3 | Write-Host "Generating a temporary download access token for $($newdisk.Name)" -ForegroundColor Green 4 | $DiskURI = $AccessSAS.AccessSAS 5 | 6 | $folder = "\\....\c$\ClusterStorage\...\Images" #enter one of the nodes here - the path must be accessible by the user - beware that there is enough space (127GB) for the disk to be downloaded. 7 | 8 | $diskDestination = "$folder\$($newdisk.Name).vhd" 9 | Write-Host "Your disk will be placed into: $diskDestination" -ForegroundColor Green 10 | #"Start-BitsTransfer ""$DiskURI"" ""$diskDestination"" -Priority High -RetryInterval 60 -Verbose -TransferType Download" 11 | 12 | #or use azcopy as it is much faster!!! 13 | invoke-webrequest -uri "https://aka.ms/downloadazcopy-v10-windows" -OutFile "$env:TEMP\azcopy.zip" -verbose 14 | Expand-Archive "$env:TEMP\azcopy.zip" "$env:TEMP" -force -verbose 15 | copy-item "$env:TEMP\azcopy_windows_amd64_*\\azcopy.exe\\" -Destination "$env:TEMP" -verbose 16 | cd "$env:TEMP\" 17 | &.\azcopy.exe copy $DiskURI $diskDestination --log-level INFO 18 | Remove-Item "azcopy*" -Recurse #cleanup temp 19 | #endregion -------------------------------------------------------------------------------- /AVD/YT-Series/customimage/3-optimizeDisk.ps1: -------------------------------------------------------------------------------- 1 | # execute this on the hyper-v node 2 | 3 | # e.g. enter-pssession %YourNodeName% 4 | 5 | #region Convert to a dynamic vhdx! 6 | $sourceDiskPath = "C:\ClusterStorage\COLLECT\Images\disk-win11-24h2-avd-m365.vhd" 7 | $finalfolder = "C:\clusterstorage\COLLECT\Images" # pls enter an existing final destination to hold the AVD image. 8 | $diskFinalDestination = "$finalfolder\$((split-path -leaf $sourceDiskPath).Replace('vhd','vhdx'))" 9 | 10 | 11 | Convert-VHD -Path "$sourceDiskPath" -DestinationPath "$diskFinalDestination" -VHDType Dynamic 12 | 13 | try 14 | { 15 | $beforeMount = (Get-Volume).DriveLetter -split ' ' 16 | Mount-VHD -Path $diskFinalDestination 17 | $afterMount = (Get-Volume).DriveLetter -split ' ' 18 | $driveLetter = $([string](Compare-Object $beforeMount $afterMount -PassThru )).Trim() 19 | Write-Host "Optimizing disk ($($driveLetter)): $diskFinalDestination" -ForegroundColor Green 20 | &defrag "$($driveLetter):" /o /u /v 21 | } 22 | finally 23 | { 24 | Write-Host "dismounting ..." 25 | Dismount-VHD -Path $diskFinalDestination 26 | } 27 | 28 | Optimize-VHD $diskFinalDestination -Mode full 29 | #endregion -------------------------------------------------------------------------------- /AVD/YT-Series/customimage/4-optionalAddingLanguagePack.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [Parameter(Mandatory=$true, 3 | ValueFromPipelineByPropertyName=$true, 4 | Position=0)] 5 | [string] $targetLanguage # e.g. "de-DE" 6 | ) 7 | 8 | $tmpDir = "c:\temp\" 9 | 10 | # Ref: Add languages to a Windows 11 Enterprise image 11 | # https://learn.microsoft.com/en-us/azure/virtual-desktop/windows-11-language-packs 12 | 13 | #create folder if it doesn't exist 14 | if (!(Test-Path $tmpDir)) { mkdir $tmpDir -Force } 15 | 16 | #write a log file with the same name of the script 17 | Start-Transcript "$tmpDir\step_InstallLanguagePack.log" -Append 18 | "================" 19 | "Starting Language Pack ($targetLanguage) installation $(Get-Date)" 20 | 21 | $ErrorActionPreference = "Continue" 22 | 23 | #region Start Language Pack download 24 | $ProgressPreference = 'SilentlyContinue' 25 | $myjobs = @() 26 | 27 | #https://learn.microsoft.com/en-us/azure/virtual-desktop/windows-11-language-packs 28 | $LPdownloads = @{ 29 | 'LanguagePack' = "https://software-static.download.prss.microsoft.com/dbazure/888969d5-f34g-4e03-ac9d-1f9786c66749/26100.1.240331-1435.ge_release_amd64fre_CLIENT_LOF_PACKAGES_OEM.iso" #24h2 30 | #"https://software-static.download.prss.microsoft.com/dbazure/988969d5-f34g-4e03-ac9d-1f9786c66749/22621.1.220506-1250.ni_release_amd64fre_CLIENT_LOF_PACKAGES_OEM.iso"# Win11 22H2, 23H2 31 | 'InboxApps' = "https://software-static.download.prss.microsoft.com/dbazure/888969d5-f34g-4e03-ac9d-1f9786c66749/26100.1742.240904-1906.ge_release_svc_prod1_amd64fre_InboxApps.iso" #24h2 32 | #"https://software-static.download.prss.microsoft.com/dbazure/888969d5-f34g-4e03-ac9d-1f9786c66749/22621.2501.231009-1937.ni_release_svc_prod3_amd64fre_InboxApps.iso" # Win11 22H2 , 23H2 33 | 'ODT' = "https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_18129-20030.exe" 34 | #"https://download.microsoft.com/download/2/7/A/27AF1BE6-DD20-4CB4-B154-EBAB8A7D4A7E/officedeploymenttool_17531-20046.exe" #Office Deployment Tool-> https://www.microsoft.com/en-us/download/details.aspx?id=49117 35 | 36 | } 37 | 38 | Write-Output "Starting download jobs $(Get-Date)" 39 | foreach ($download in $LPdownloads.GetEnumerator()) { 40 | $downloadPath = $tmpDir + "\$(Split-Path $($download.Value) -Leaf)" 41 | if (!(Test-Path $downloadPath )) { 42 | #download if not there 43 | $myjobs += Start-Job -ArgumentList $($download.Value), $downloadPath -Name "download" -ScriptBlock { 44 | param([string] $downloadURI, 45 | [string]$downloadPath 46 | ) 47 | #Invoke-WebRequest -Uri $download -OutFile $downloadPath # is 10 slower than the webclient 48 | $wc = New-Object net.webclient 49 | $wc.Downloadfile( $downloadURI, $downloadPath) 50 | } 51 | } 52 | } 53 | 54 | do { 55 | Start-Sleep 15 56 | $running = @($myjobs | Where-Object { ($_.State -eq 'Running') }) 57 | $myjobs | Group-Object State | Select-Object count, name 58 | Write-Output "-----------------" 59 | } 60 | while ($running.count -gt 0) 61 | 62 | Write-Output "Finished downloads $(Get-Date)" 63 | #endregion 64 | 65 | #region Time Zone Redirection 66 | $Name = "fEnableTimeZoneRedirection" 67 | $value = "1" 68 | # Add Registry value 69 | try { 70 | New-ItemProperty -ErrorAction Stop -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" -Name $name -Value $value -PropertyType DWORD -Force 71 | if ((Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services").PSObject.Properties.Name -contains $name) { 72 | Write-Output "Added time zone redirection registry key" 73 | } 74 | else { 75 | Write-Output "Error locating the Timezone registry key" 76 | } 77 | } 78 | catch { 79 | $ErrorMessage = $_.Exception.message 80 | Write-Output "Error adding Timezone registry KEY: $ErrorMessage" 81 | } 82 | #endregion 83 | 84 | #see https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/languages-overview?view=windows-11#build-a-custom-fod-and-language-pack-repository 85 | 86 | #region Language pack installation 87 | Write-Output "Entering Language Pack installation $(Get-Date)" 88 | 89 | #region isomounting helper function 90 | function MountIso ($ISOPath) { 91 | $global:before = (Get-Volume | Where-Object Driveletter -NE $null ).DriveLetter 92 | Set-Variable -Name mountVolume -Scope Script -Value (Mount-DiskImage -ImagePath $ISOPath -StorageType ISO -PassThru) 93 | Start-Sleep -Seconds 1 94 | $global:after = (Get-Volume | Where-Object Driveletter -NE $null ).DriveLetter 95 | Set-Variable -Name driveLetter -Scope Script -Value (Compare-Object $global:before $global:after -PassThru) 96 | return @{ 97 | 'driveletter' = $driveLetter 98 | 'mountvolume' = $mountVolume 99 | } 100 | } 101 | #endregion 102 | 103 | ######################################################## 104 | ## Add Languages to running Windows Image for Capture## 105 | ######################################################## 106 | ##Disable Language Pack Cleanup## 107 | Disable-ScheduledTask -TaskPath "\Microsoft\Windows\AppxDeploymentClient\" -TaskName "Pre-staged app cleanup" 108 | Disable-ScheduledTask -TaskPath "\Microsoft\Windows\MUI\" -TaskName "LPRemove" 109 | Disable-ScheduledTask -TaskPath "\Microsoft\Windows\LanguageComponentsInstaller" -TaskName "Uninstallation" 110 | reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Control Panel\International" /v "BlockCleanupOfUnusedPreinstalledLangPacks" /t REG_DWORD /d 1 /f 111 | 112 | ##Set Language Pack Content Stores## 113 | $LanguagePack = $tmpDir + '\' + $(Split-Path $LPdownloads['LanguagePack'] -Leaf) 114 | #mount 115 | Write-Output "Mounting ISO Image: $LanguagePack" 116 | $iso = MountIso $LanguagePack 117 | $LanguagePackContent = "$($iso['driveletter'])" + ':\LanguagesAndOptionalFeatures\' 118 | 119 | 120 | ##List of additional features to be installed## 121 | $additionalFODList = @( 122 | "$LanguagePackContent\Microsoft-Windows-NetFx3-OnDemand-Package~31bf3856ad364e35~amd64~~.cab", 123 | "$LanguagePackContent\Microsoft-Windows-InternetExplorer-Optional-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab", 124 | "$LanguagePackContent\Microsoft-Windows-MSPaint-FoD-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab", 125 | "$LanguagePackContent\Microsoft-Windows-Notepad-FoD-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab", 126 | "$LanguagePackContent\Microsoft-Windows-Notepad-System-FoD-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab", 127 | "$LanguagePackContent\Microsoft-Windows-PowerShell-ISE-FOD-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab", 128 | "$LanguagePackContent\Microsoft-Windows-Printing-PMCPPC-FoD-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab", 129 | "$LanguagePackContent\Microsoft-Windows-Printing-WFS-FoD-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab", 130 | "$LanguagePackContent\Microsoft-Windows-SnippingTool-FoD-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab", 131 | "$LanguagePackContent\Microsoft-Windows-StepsRecorder-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab", 132 | "$LanguagePackContent\Microsoft-Windows-MediaPlayer-Package~31bf3856ad364e35~wow64~$targetLanguage~.cab" 133 | #"$LanguagePackContent\Microsoft-Windows-WordPad-FoD-Package~31bf3856ad364e35~amd64~$targetLanguage~.cab" #no longer in 24h2 134 | ) 135 | 136 | $additionalCapabilityList = @( 137 | "Language.Basic~~~$targetLanguage~0.0.1.0", 138 | "Language.Handwriting~~~$targetLanguage~0.0.1.0", 139 | "Language.OCR~~~$targetLanguage~0.0.1.0", 140 | "Language.Speech~~~$targetLanguage~0.0.1.0", 141 | "Language.TextToSpeech~~~$targetLanguage~0.0.1.0" 142 | ) 143 | 144 | ##Install all FODs or fonts from the CSV file### 145 | $LP = "$LanguagePackContent\Microsoft-Windows-Client-Language-Pack_x64_$targetLanguage.cab" 146 | "adding...$LP" 147 | Dism /Online /Add-Package /PackagePath:$LP 148 | 149 | foreach ($capability in $additionalCapabilityList) { 150 | "adding...$capability" 151 | Dism /Online /Add-Capability /CapabilityName:$capability /Source:$LanguagePackContent 152 | } 153 | 154 | foreach ($feature in $additionalFODList) { 155 | "adding...$feature" 156 | Dism /Online /Add-Package /PackagePath:$feature 157 | } 158 | 159 | Write-Output "Dismounting ISO Image." 160 | Dismount-DiskImage -InputObject $iso['mountvolume'] 161 | 162 | ##Add installed language to language list## 163 | $LanguageList = Get-WinUserLanguageList 164 | $LanguageList.Add($targetLanguage) 165 | Set-WinUserLanguageList $LanguageList -Force 166 | Set-SystemPreferredUILanguage -Language $targetLanguage 167 | 168 | Write-Output "Finished Language Pack installation $(Get-Date)" 169 | #endregion 170 | 171 | #region Update Inbox Apps for Multi Language 172 | $InboxApps = $tmpDir + '\' + $(Split-Path $LPdownloads['InboxApps'] -Leaf) 173 | #mount 174 | Write-Output "Mounting ISO Image: $InboxApps" 175 | $iso = MountIso $InboxApps 176 | [string] $AppsContent = "$($iso['driveletter'])`:\packages\" 177 | ##Update installed Inbox Store Apps## 178 | foreach ($App in (Get-AppxProvisionedPackage -Online)) { 179 | $AppPath = $AppsContent + $App.DisplayName + '_' + $App.PublisherId 180 | Write-Host "Handling: $($App.DisplayName) --> $AppPath" 181 | $licFile = Get-Item $AppPath*.xml 182 | if ($licFile.Count) { 183 | $lic = $true 184 | $licFilePath = $licFile.FullName 185 | } 186 | else { 187 | $lic = $false 188 | } 189 | $appxFile = Get-Item $AppPath*.appx* 190 | if ($appxFile.Count) { 191 | $appxFilePath = $appxFile.FullName 192 | if ($lic) { 193 | Add-AppxProvisionedPackage -Online -PackagePath $appxFilePath -LicensePath $licFilePath -Verbose 194 | } 195 | else { 196 | Add-AppxProvisionedPackage -Online -PackagePath $appxFilePath -SkipLicense -Verbose 197 | } 198 | } 199 | } 200 | Write-Output "Dismounting ISO Image." 201 | Dismount-DiskImage -InputObject $iso['mountvolume'] 202 | #endregion 203 | 204 | #region Office Language Pack installation 205 | #https://www.microsoft.com/en-us/download/details.aspx?id=49117 206 | #downloaded Office Deployment Tool -> created a config file (https://config.office.com/) to add $targetLanguage as language -> execute OCT tool to download & install LP 207 | Write-Output "Installing Office Language Pack $(Get-Date)" 208 | 209 | $ODTConfig = @" 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | "@ 232 | 233 | $ODTConfig 234 | 235 | Out-File -FilePath "$tmpDir\ODTConfig.xml" -InputObject $ODTConfig 236 | 237 | Start-Process -filepath "$tmpDir\officedeploymenttool*.exe" -ArgumentList "/extract:$tmpDir\ODT /quiet" -wait 238 | start-sleep 3 239 | 240 | try { 241 | Write-Output "Executing Office Deployment Tool...this will take a while... $(Get-Date)" 242 | Start-Process -FilePath "$tmpDir\ODT\setup.exe" -Wait -ErrorAction Stop -ArgumentList "/configure $tmpDir\ODTConfig.xml" -NoNewWindow 243 | } 244 | catch { 245 | $ErrorMessage = $_.Exception.message 246 | Write-Output "Error installing Office Language Pack $ErrorMessage" 247 | } 248 | Write-Output "End Office Language Pack $(Get-Date)" 249 | #endregion 250 | 251 | stop-tran 252 | -------------------------------------------------------------------------------- /AVD/YT-Series/readme.md: -------------------------------------------------------------------------------- 1 | # Welcome To Azure Virtual Desktop (AVD) On Azure Stack HCI (23H2) YT Series Code Repo 2 | 3 | >This code repo holds the artefacts used in YT Series for AVD on HCI. Use at your own risk! 4 | 5 | ## Table Of Contents 6 | - [customimage](./customimage) -------------------------------------------------------------------------------- /AVD/YT-Series/remoteApp/publishWindowsAppDetails.ps1: -------------------------------------------------------------------------------- 1 | ####################### 2 | # run this as Administrator on one AVD session host (ie. machine where the app is installed) 3 | # copy the output and use it when editing the Remote Application Group in the AVD portal 4 | ####################### 5 | 6 | $selectedPackage = Get-AppxPackage -AllUsers | Sort-Object Name | Select-Object Name, PackageFamilyName,InstallLocation | Out-GridView -OutputMode Single 7 | $packageFamilyName = $selectedPackage.PackageFamilyName 8 | $packagemanifest = (Get-AppxPackage -AllUsers | ? PackageFamilyName -eq $packageFamilyName | Get-AppxPackageManifest) 9 | 10 | $filePath = "shell:appsFolder\$($packageFamilyName)!$($packagemanifest.Package.Applications.Application.Id | Select-Object -First 1)" 11 | 12 | # find suitable logo for selected package 13 | $initialDirectory = "$($selectedPackage.InstallLocation)\$(split-path $(($packagemanifest | Select-Object -First 1).package.properties.logo) -Parent)" 14 | 15 | [void] [System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms') 16 | $OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog 17 | if ($initialDirectory) { $OpenFileDialog.initialDirectory = $initialDirectory } 18 | $OpenFileDialog.filter = "png files (*.png)|*.png|jpg files (*.jpg)|*.png|All files (*.*)|*.*" 19 | $OpenFileDialog.ShowDialog() 20 | $iconPath = $OpenFileDialog.FileName 21 | 22 | $output = @" 23 | ============= 24 | Filepath: $filePath 25 | - - - - 26 | Application Identifier: $packageFamilyName 27 | - - - - 28 | Iconpath: $iconPath 29 | ============= 30 | "@ 31 | 32 | Write-Host $output -ForegroundColor Magenta -------------------------------------------------------------------------------- /AVD/intendedway/ARM/ARMDeploymentOfAVD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/AVD/intendedway/ARM/ARMDeploymentOfAVD.png -------------------------------------------------------------------------------- /AVD/intendedway/ARM/deployAVDOnPremise.ps1: -------------------------------------------------------------------------------- 1 | 2 | $SubscriptionId = 'a2ba...???...57e6f' # your Azure subscription ID 3 | $tenantId = '47f4...???...65aab0' # your Azure Entra ID tenant ID 4 | $resourceGroup = "...???..." # please change me! 5 | $locationHostPool = "westeurope" 6 | $administratorAccountPassword = "...???..." # please change me! 7 | $administratorAccountUsername = "...???..." # please change me! 8 | $hostpoolName = "...???..." # please change me! 9 | $imageName = "win11-avd" # please change me! 10 | $imageRG = "...???..." # please change me! 11 | 12 | az login --use-device-code --tenant $tenantId 13 | az account set --subscription $SubscriptionId 14 | az group create --name $resourceGroup --location $locationHostPool 15 | 16 | $principalID = $(az ad group show --group 'AVD Users...???...') | ConvertFrom-Json #Change to a that exists in your Azure AD 17 | 18 | $imageID = $(az stack-hci-vm image show --name $imageName --resource-group $imageRG) | ConvertFrom-Json #Change to a that exists in your Azure AD 19 | 20 | 21 | if ($null -eq $principalID) { 22 | Write-Host "You need to create a group called 'AVD Users' in the Azure AD and assign the users to it. Then run this script again." -ForegroundColor red 23 | break 24 | } 25 | 26 | 27 | az deployment group create ` 28 | --name $("avd_" + ([datetime]::Now).ToString('dd-MM-yy_HH_mm')) ` 29 | --template-file "$((Get-Location).Path)\AVDOnPremise.json" ` 30 | --resource-group $resourceGroup ` 31 | --parameters location=$locationHostPool ` 32 | administratorAccountPassword=$administratorAccountPassword ` 33 | administratorAccountUsername=$administratorAccountUsername ` 34 | domain='my...???...org' ` 35 | oUPath='OU=AVD,DC=my...???...,DC=org' ` 36 | logicalNetworkId='/subscriptions/a2...???...e6f/resourceGroups/rg...???.../providers/microsoft.azurestackhci/logicalnetworks/lnet...???...atic' ` 37 | configurationZipUri='https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_1.0.02608.268.zip' ` 38 | customLocationId='/subscriptions/a2ba2440-3367-4053-86aa-07bcecc57e6f/resourceGroups/rg...???.../providers/Microsoft.ExtendedLocation/customLocations/...???...location' ` 39 | domainAdministratorPassword='...???...' ` 40 | domainAdministratorUsername='djoiner@my...???...org' ` 41 | fileUri='https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/HCIScripts_1.0.02608.268/HciCustomScript.ps1' ` 42 | imageId=$($imageID.id) ` 43 | memoryMB=8192 ` 44 | virtualProcessorCount=4 ` 45 | hostpoolName=$hostpoolName ` 46 | hostPoolRG=$resourceGroup ` 47 | principalID=$($principalID.id) ` 48 | workspaceName="$hostpoolName-WS" ` 49 | workspaceFriendlyName="Cloud Workspace hosting $hostpoolName" ` 50 | currentDate=$(([datetime]::Now).ToString('dd-MM_HH_mm')) tagValues=$('{\"CreatedBy\": \"someone\",\"deploymentDate\": \"'+ $(([datetime]::Now).ToString('dd-MM-yyyy_HH_mm')) + '\",\"Service\": \"AVD\",\"Environment\": \"HCI\"}') 51 | 52 | -------------------------------------------------------------------------------- /AVD/intendedway/ARM/proxy/AVDOnPremiseVMOnlyProxy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "rdshPrefix": { 6 | "type": "string", 7 | "defaultValue": "[take(toLower(resourceGroup().name), 10)]", 8 | "metadata": { 9 | "description": "This prefix will be used in combination with the VM number to create the VM name. This value includes the dash, so if using “rdsh” as the prefix, VMs would be named “rdsh-0”, “rdsh-1”, etc. You should use a unique prefix to reduce name collisions in Active Directory." 10 | } 11 | }, 12 | "virtualProcessorCount": { 13 | "type": "int", 14 | "metadata": { 15 | "description": "Virtual Processor Count" 16 | } 17 | }, 18 | "memoryMB": { 19 | "type": "int", 20 | "metadata": { 21 | "description": "The total amount of memory in megabytes" 22 | } 23 | }, 24 | "maximumMemoryMB": { 25 | "type": "int", 26 | "defaultValue": 0, 27 | "metadata": { 28 | "description": "This parameter is optional and only used if dynamicMemory = true. When using dynamic memory this setting is the maximum MB given to the VM." 29 | } 30 | }, 31 | "minimumMemoryMB": { 32 | "type": "int", 33 | "defaultValue": 0, 34 | "metadata": { 35 | "description": "This parameter is optional and only used if dynamicMemory = true. When using dynamic memory this setting is the minimum MB given to the VM." 36 | } 37 | }, 38 | "dynamicMemoryConfig": { 39 | "type": "bool", 40 | "defaultValue": false, 41 | "metadata": { 42 | "description": "True if you want to use a dynamic memory config." 43 | } 44 | }, 45 | "targetMemoryBuffer": { 46 | "type": "int", 47 | "defaultValue": 0, 48 | "metadata": { 49 | "description": "This parameter is optional and only used if dynamicMemory = true. When using dynamic memory this setting is the buffer of extra memory given to the VM." 50 | } 51 | }, 52 | "vmInstanceSuffixes": { 53 | "type": "array", 54 | "defaultValue": [ 55 | "0", 56 | "1" 57 | ], 58 | "metadata": { 59 | "description": "This is the suffix to the vm names. VM will be named '[vmPrefix]-[vmInstanceSuffixes]'" 60 | } 61 | }, 62 | "tagValues": { 63 | "type": "object", 64 | "defaultValue": {}, 65 | "metadata": { 66 | "description": "The tags to be assigned to the resources" 67 | } 68 | }, 69 | "location": { 70 | "type": "string", 71 | "metadata": { 72 | "description": "The location where the resources will be deployed." 73 | } 74 | }, 75 | "customLocationId": { 76 | "type": "string", 77 | "metadata": { 78 | "description": "A deployment target created and customized by your organization for creating virtual machines. The custom location is associated to an Azure Stack HCI cluster." 79 | } 80 | }, 81 | "domainAdministratorUsername": { 82 | "type": "string", 83 | "metadata": { 84 | "description": "The username for the domain admin." 85 | } 86 | }, 87 | "domainAdministratorPassword": { 88 | "type": "securestring", 89 | "metadata": { 90 | "description": "The password that corresponds to the existing domain username." 91 | } 92 | }, 93 | "vmAdministratorAccountUsername": { 94 | "type": "string", 95 | "defaultValue": "", 96 | "metadata": { 97 | "description": "A username to be used as the virtual machine administrator account. The vmAdministratorAccountUsername and vmAdministratorAccountPassword parameters must both be provided. Otherwise, domain administrator credentials provided by domainAdministratorUsername and domainAdministratorPassword will be used." 98 | } 99 | }, 100 | "vmAdministratorAccountPassword": { 101 | "type": "securestring", 102 | "defaultValue": "", 103 | "metadata": { 104 | "description": "The password associated with the virtual machine administrator account. The vmAdministratorAccountUsername and vmAdministratorAccountPassword parameters must both be provided. Otherwise, domain administrator credentials provided by domainAdministratorUsername and domainAdministratorPassword will be used." 105 | } 106 | }, 107 | "logicalNetworkId": { 108 | "type": "string", 109 | "metadata": { 110 | "description": "Full ARM resource ID of the AzureStackHCI virtual network used for the VMs." 111 | } 112 | }, 113 | "imageId": { 114 | "type": "string", 115 | "metadata": { 116 | "description": "Full ARM resource ID of the AzureStackHCI virtual machine image used for the VMs." 117 | } 118 | }, 119 | "hostpoolName": { 120 | "type": "string", 121 | "metadata": { 122 | "description": "The name of the hostpool" 123 | } 124 | }, 125 | "hostpoolToken": { 126 | "type": "string", 127 | "metadata": { 128 | "description": "The token for adding VMs to the hostpool" 129 | } 130 | }, 131 | "oUPath": { 132 | "type": "string", 133 | "metadata": { 134 | "description": "OU Path for the domain join" 135 | } 136 | }, 137 | "domain": { 138 | "type": "string", 139 | "metadata": { 140 | "description": "Domain to join" 141 | } 142 | }, 143 | "fileUri": { 144 | "type": "string", 145 | "metadata": { 146 | "description": "Uri to download file that is executed in the custom script extension" 147 | } 148 | }, 149 | "fileName": { 150 | "type": "string", 151 | "defaultValue": "HciCustomScript.ps1", 152 | "metadata": { 153 | "description": "Uri to download file that is executed in the custom script extension" 154 | } 155 | }, 156 | "configurationZipUri": { 157 | "type": "string", 158 | "metadata": { 159 | "description": "The base URI where the Configuration.zip script is located to install the AVD agent on the VM" 160 | } 161 | }, 162 | "aadJoin": { 163 | "type": "bool", 164 | "defaultValue": false, 165 | "metadata": { 166 | "description": "IMPORTANT: You can use this parameter for the test purpose only as AAD Join is public preview. True if AAD Join, false if AD join" 167 | } 168 | }, 169 | "intune": { 170 | "type": "bool", 171 | "defaultValue": false, 172 | "metadata": { 173 | "description": "IMPORTANT: Please don't use this parameter as intune enrollment is not supported yet. True if intune enrollment is selected. False otherwise" 174 | } 175 | }, 176 | "httpProxy": { 177 | "type": "string", 178 | "metadata": { 179 | "description": "http or https proxy" 180 | } 181 | }, 182 | "systemData": { 183 | "type": "object", 184 | "defaultValue": {}, 185 | "metadata": { 186 | "description": "System data is used for internal purposes, such as support preview features." 187 | } 188 | } 189 | }, 190 | "variables": { 191 | "domain_var": "[if(equals(parameters('domain'), ''), last(split(parameters('domainAdministratorUsername'), '@')), parameters('domain'))]", 192 | "hostPoolNameArgument": "[format('-HostPoolName {0}', parameters('hostpoolName'))]", 193 | "registrationTokenArgument": "[format(' -RegistrationInfoToken {0}', parameters('hostpoolToken'))]", 194 | "artifactUriArgument": "[format(' -ArtifactUri {0}', parameters('configurationZipUri'))]", 195 | "customScriptParentFolder": "[split(parameters('fileUri'), '/')[4]]", 196 | "customScriptFilePath": "[format('{0}/{1}', variables('customScriptParentFolder'), parameters('fileName'))]", 197 | "arguments": "[format('{0}{1}{2}', variables('hostPoolNameArgument'), variables('registrationTokenArgument'), variables('artifactUriArgument'))]", 198 | "isVMAdminAccountCredentialsProvided": "[and(not(equals(parameters('vmAdministratorAccountUsername'), '')), not(equals(parameters('vmAdministratorAccountPassword'), '')))]", 199 | "vmAdministratorUsername": "[if(variables('isVMAdminAccountCredentialsProvided'), parameters('vmAdministratorAccountUsername'), first(split(parameters('domainAdministratorUsername'), '@')))]", 200 | "vmAdministratorPassword": "[if(variables('isVMAdminAccountCredentialsProvided'), parameters('vmAdministratorAccountPassword'), parameters('domainAdministratorPassword'))]" 201 | }, 202 | "resources": [ 203 | { 204 | "copy": { 205 | "name": "rdshPrefix_vmInitialNumber_nic", 206 | "count": "[length(parameters('vmInstanceSuffixes'))]" 207 | }, 208 | "type": "Microsoft.AzureStackHCI/networkInterfaces", 209 | "apiVersion": "2023-09-01-preview", 210 | "name": "[format('{0}{1}-nic', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 211 | "location": "[parameters('location')]", 212 | "extendedLocation": { 213 | "type": "CustomLocation", 214 | "name": "[parameters('customLocationId')]" 215 | }, 216 | "tags": "[parameters('tagValues')]", 217 | "properties": { 218 | "ipConfigurations": [ 219 | { 220 | "name": "[format('{0}{1}-nic', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 221 | "properties": { 222 | "subnet": { 223 | "id": "[parameters('logicalNetworkId')]" 224 | } 225 | } 226 | } 227 | ] 228 | } 229 | }, 230 | { 231 | "copy": { 232 | "name": "rdshPrefix_vmInitialNumber", 233 | "count": "[length(parameters('vmInstanceSuffixes'))]" 234 | }, 235 | "type": "Microsoft.HybridCompute/machines", 236 | "apiVersion": "2023-03-15-preview", 237 | "name": "[format('{0}{1}', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 238 | "location": "[parameters('location')]", 239 | "kind": "HCI", 240 | "tags": "[parameters('tagValues')]", 241 | "identity": { 242 | "type": "SystemAssigned" 243 | } 244 | }, 245 | { 246 | "copy": { 247 | "name": "default", 248 | "count": "[length(parameters('vmInstanceSuffixes'))]" 249 | }, 250 | "scope": "[concat('Microsoft.HybridCompute/machines', '/', concat(parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()]))]", 251 | "type": "Microsoft.AzureStackHCI/virtualMachineInstances", 252 | "apiVersion": "2023-09-01-preview", 253 | "name": "default", 254 | "extendedLocation": { 255 | "type": "CustomLocation", 256 | "name": "[parameters('customLocationId')]" 257 | }, 258 | "properties": { 259 | "hardwareProfile": { 260 | "vmSize": "Custom", 261 | "processors": "[parameters('virtualProcessorCount')]", 262 | "memoryMB": "[parameters('memoryMB')]", 263 | "dynamicMemoryConfig": "[if(parameters('dynamicMemoryConfig'), createObject('maximumMemoryMB', parameters('maximumMemoryMB'), 'minimumMemoryMB', parameters('minimumMemoryMB'), 'targetMemoryBuffer', parameters('targetMemoryBuffer')), json('null'))]" 264 | }, 265 | "osProfile": { 266 | "adminUsername": "[variables('vmAdministratorUsername')]", 267 | "adminPassword": "[variables('vmAdministratorPassword')]", 268 | "windowsConfiguration": { 269 | "provisionVMAgent": true, 270 | "provisionVMConfigAgent": true 271 | }, 272 | "computerName": "[format('{0}{1}', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]" 273 | }, 274 | "storageProfile": { 275 | "imageReference": { 276 | "id": "[parameters('imageId')]" 277 | } 278 | }, 279 | "networkProfile": { 280 | "networkInterfaces": [ 281 | { 282 | "id": "[resourceId('Microsoft.AzureStackHCI/networkinterfaces', format('{0}{1}-nic', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()]))]" 283 | } 284 | ] 285 | }, 286 | "httpProxyConfig": { 287 | "httpProxy": "[parameters('httpProxy')]", 288 | "httpsProxy": "[parameters('httpProxy')]" 289 | } 290 | }, 291 | "dependsOn": [ 292 | "rdshPrefix_vmInitialNumber", 293 | "rdshPrefix_vmInitialNumber_nic" 294 | ] 295 | }, 296 | { 297 | "copy": { 298 | "name": "rdshPrefix_vmInitialNumber_CustomScriptExtension", 299 | "count": "[length(parameters('vmInstanceSuffixes'))]" 300 | }, 301 | "type": "Microsoft.HybridCompute/machines/extensions", 302 | "apiVersion": "2023-03-15-preview", 303 | "name": "[format('{0}{1}/CustomScriptExtension', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 304 | "location": "[parameters('location')]", 305 | "properties": { 306 | "publisher": "Microsoft.Compute", 307 | "type": "CustomScriptExtension", 308 | "autoUpgradeMinorVersion": true, 309 | "settings": { 310 | "fileUris": [ 311 | "[parameters('fileUri')]" 312 | ] 313 | }, 314 | "protectedSettings": { 315 | "commandToExecute": "[format('powershell -ExecutionPolicy Unrestricted -File {0} {1}', variables('customScriptFilePath'), variables('arguments'))]" 316 | } 317 | }, 318 | "dependsOn": [ 319 | "default" 320 | ] 321 | }, 322 | { 323 | "copy": { 324 | "name": "rdshPrefix_vmInitialNumber_AADLoginForWindows", 325 | "count": "[length(parameters('vmInstanceSuffixes'))]" 326 | }, 327 | "condition": "[and(parameters('aadJoin'), if(contains(parameters('systemData'), 'aadJoinPreview'), not(parameters('systemData').aadJoinPreview), bool('true')))]", 328 | "type": "Microsoft.HybridCompute/machines/extensions", 329 | "apiVersion": "2023-03-15-preview", 330 | "name": "[format('{0}{1}/AADLoginForWindows', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 331 | "location": "[parameters('location')]", 332 | "properties": { 333 | "publisher": "Microsoft.Azure.ActiveDirectory", 334 | "type": "AADLoginForWindows", 335 | "typeHandlerVersion": "2.0", 336 | "autoUpgradeMinorVersion": true, 337 | "settings": "[if(parameters('intune'), createObject('mdmId', '0000000a-0000-0000-c000-000000000000'), createObject('mdmId', ''))]" 338 | }, 339 | "dependsOn": [ 340 | "rdshPrefix_vmInitialNumber_CustomScriptExtension" 341 | ] 342 | }, 343 | { 344 | "copy": { 345 | "name": "rdshPrefix_vmInitialNumber_joindomain", 346 | "count": "[length(parameters('vmInstanceSuffixes'))]" 347 | }, 348 | "condition": "[not(parameters('aadJoin'))]", 349 | "type": "Microsoft.HybridCompute/machines/extensions", 350 | "apiVersion": "2023-03-15-preview", 351 | "name": "[format('{0}{1}/joindomain', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 352 | "location": "[parameters('location')]", 353 | "properties": { 354 | "publisher": "Microsoft.Compute", 355 | "type": "JsonADDomainExtension", 356 | "typeHandlerVersion": "1.3", 357 | "autoUpgradeMinorVersion": true, 358 | "settings": { 359 | "name": "[variables('domain_var')]", 360 | "oUPath": "[parameters('oUPath')]", 361 | "user": "[parameters('domainAdministratorUsername')]", 362 | "restart": "true", 363 | "options": "3" 364 | }, 365 | "protectedSettings": { 366 | "password": "[parameters('domainAdministratorPassword')]" 367 | } 368 | }, 369 | "dependsOn": [ 370 | "rdshPrefix_vmInitialNumber_CustomScriptExtension" 371 | ] 372 | }, 373 | { 374 | "copy": { 375 | "name": "rdshPrefix_vmInitialNumber_azmonitor", 376 | "count": "[length(parameters('vmInstanceSuffixes'))]" 377 | }, 378 | "type": "Microsoft.HybridCompute/machines/extensions", 379 | "apiVersion": "2023-03-15-preview", 380 | "name": "[format('{0}{1}/azmonitor', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 381 | "location": "[parameters('location')]", 382 | "properties": { 383 | "publisher": "Microsoft.Azure.Monitor", 384 | "type": "AzureMonitorWindowsAgent", 385 | "typeHandlerVersion": "1.5", 386 | "autoUpgradeMinorVersion": true 387 | }, 388 | "dependsOn": [ 389 | "rdshPrefix_vmInitialNumber_joindomain" 390 | ] 391 | }, 392 | { 393 | "copy": { 394 | "name": "rdshPrefix_vmInitialNumber_RunCommandSetProxy", 395 | "count": "[length(parameters('vmInstanceSuffixes'))]" 396 | }, 397 | "type": "Microsoft.HybridCompute/machines/runCommands", 398 | "apiVersion": "2023-10-03-preview", 399 | "name": "[format('{0}{1}/runCommands', parameters('rdshPrefix'), parameters('vmInstanceSuffixes')[copyIndex()])]", 400 | "location": "[parameters('location')]", 401 | "properties": { 402 | "parameters": [ 403 | { 404 | "name": "proxy", 405 | "value": "[parameters('httpProxy')]" 406 | } 407 | ], 408 | "source": { 409 | "script": "param ([string]$proxyParam);$PSBoundParameters;bitsadmin /util /setieproxy LOCALSYSTEM Manual_Proxy $proxyParam null; bitsadmin /util /setieproxy NETWORKSERVICE Manual_Proxy $proxyParam null" 410 | }, 411 | "timeoutInSeconds": 600, 412 | "treatFailureAsDeploymentFailure": false 413 | }, 414 | "dependsOn": [ 415 | "rdshPrefix_vmInitialNumber_azmonitor" 416 | ] 417 | } 418 | ] 419 | } -------------------------------------------------------------------------------- /AVD/intendedway/ARM/proxy/AvdOnHCIWithProxyDiffSubsARM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/AVD/intendedway/ARM/proxy/AvdOnHCIWithProxyDiffSubsARM.png -------------------------------------------------------------------------------- /AVD/intendedway/ARM/proxy/deployAVDOnPremisevmOnlyProxy.ps1: -------------------------------------------------------------------------------- 1 | $SubscriptionId = 'a2ba2.....cc57e6f' # your Azure subscription ID 2 | $tenantId = '47f489.f65aab0' # your Azure Entra ID tenant ID 3 | $resourceGroup = "rg-MX.....H2-res" # please change me! 4 | $locationHostPool = "westeurope" 5 | $administratorAccountPassword = "L.....1!" # please change me! 6 | $administratorAccountUsername = "bfrank" # please change me! 7 | $hostpoolName = "HCIMX....." # please change me! 8 | 9 | az login --use-device-code --tenant $tenantId 10 | az account set --subscription $SubscriptionId 11 | az group create --name $resourceGroup --location $locationHostPool 12 | 13 | 14 | az deployment group create ` 15 | --name $("avd_" + ([datetime]::Now).ToString('dd-MM-yy_HH_mm')) ` 16 | --template-file "$((Get-Location).Path)\AVDOnPremiseVMOnlyProxy.json" ` 17 | --resource-group $resourceGroup ` 18 | --parameters location=$locationHostPool ` 19 | vmAdministratorAccountPassword=$administratorAccountPassword ` 20 | vmAdministratorAccountUsername=$administratorAccountUsername ` 21 | domainAdministratorPassword='L.....1!' ` 22 | domainAdministratorUsername='djoiner@my.....avd.org' ` 23 | domain='my.....avd.org' ` 24 | oUPath='OU=HCIMX.....,OU=AVD.....,DC=my.....avd,DC=org' ` 25 | customLocationId='/subscriptions/a2ba.....7e6f/resourceGroups/rg-MX1020-23H2/providers/Microsoft.ExtendedLocation/customLocations/MX-cl' ` 26 | logicalNetworkId='/subscriptions/a2ba.....e6f/resourceGroups/rg-MX1020-23H2/providers/microsoft.azurestackhci/logicalnetworks/lnetmyavdstaticproxy' ` 27 | imageId="/subscriptions/a2ba.....7e6f/resourceGroups/rg-MX1020-23H2/providers/microsoft.azurestackhci/galleryimages/W11AvdM365Opt" ` 28 | configurationZipUri='https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_1.0.02608.268.zip' ` 29 | fileUri='https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/HCIScripts_1.0.02608.268/HciCustomScript.ps1' ` 30 | httpProxy='http://172.......1:3128' ` 31 | memoryMB=16384 ` 32 | virtualProcessorCount=8 ` 33 | hostpoolName=$hostpoolName ` 34 | hostpoolToken='eyJhbGciOiJSUzI1Ni.....-G-OFA' ` 35 | vmInstanceSuffixes="[""10"",""11""]" ` 36 | tagValues=$('{\"CreatedBy\": \"bfrank\",\"deploymentDate\": \"'+ $(([datetime]::Now).ToString('dd-MM-yyyy_HH_mm')) + '\",\"Service\": \"AVD\",\"Environment\": \"PoC\"}') 37 | 38 | -------------------------------------------------------------------------------- /AVD/intendedway/ARM/readme.md: -------------------------------------------------------------------------------- 1 | ## This Should Create AVD Session Hosts On AzStack HCI Using An ARM Template 2 | 3 | ![ARM deployment of AVD session hosts on HCI](./ARMDeploymentOfAVD.png) -------------------------------------------------------------------------------- /AVD/intendedway/bicep/bicepmodulespooled/addVm.bicep: -------------------------------------------------------------------------------- 1 | @description('This prefix will be used in combination with the VM number to create the VM name. This value includes the dash, so if using “rdsh” as the prefix, VMs would be named “rdsh-0”, “rdsh-1”, etc. You should use a unique prefix to reduce name collisions in Active Directory.') 2 | param rdshPrefix string = take(toLower(resourceGroup().name), 10) 3 | 4 | @description('Virtual Processor Count') 5 | param virtualProcessorCount int 6 | 7 | @description('The total amount of memory in megabytes') 8 | param memoryMB int 9 | 10 | @description('This parameter is optional and only used if dynamicMemory = true. When using dynamic memory this setting is the maximum MB given to the VM.') 11 | param maximumMemoryMB int = 0 12 | 13 | @description('This parameter is optional and only used if dynamicMemory = true. When using dynamic memory this setting is the minimum MB given to the VM.') 14 | param minimumMemoryMB int = 0 15 | 16 | @description('True if you want to use a dynamic memory config.') 17 | param dynamicMemoryConfig bool = false 18 | 19 | @description('This parameter is optional and only used if dynamicMemory = true. When using dynamic memory this setting is the buffer of extra memory given to the VM.') 20 | param targetMemoryBuffer int = 0 21 | 22 | @description('VM name prefix initial number.') 23 | param vmInitialNumber int = 0 24 | 25 | @description('The tags to be assigned to the network interfaces') 26 | param networkInterfaceTags object = {} 27 | 28 | @description('The tags to be assigned to the virtual machines') 29 | param virtualMachineTags object = {} 30 | 31 | @description('The location where the resources will be deployed.') 32 | param location string 33 | 34 | @description('A deployment target created and customized by your organization for creating virtual machines. The custom location is associated to an Azure Stack HCI cluster.') 35 | param customLocationId string 36 | 37 | @description('The username for the domain admin.') 38 | param domainAdministratorUsername string 39 | 40 | @description('The password that corresponds to the existing domain username.') 41 | @secure() 42 | param domainAdministratorPassword string 43 | 44 | @description('A username to be used as the virtual machine administrator account. The vmAdministratorAccountUsername and vmAdministratorAccountPassword parameters must both be provided. Otherwise, domain administrator credentials provided by domainAdministratorUsername and domainAdministratorPassword will be used.') 45 | param vmAdministratorAccountUsername string = '' 46 | 47 | @description('The password associated with the virtual machine administrator account. The vmAdministratorAccountUsername and vmAdministratorAccountPassword parameters must both be provided. Otherwise, domain administrator credentials provided by domainAdministratorUsername and domainAdministratorPassword will be used.') 48 | @secure() 49 | param vmAdministratorAccountPassword string = '' 50 | 51 | @description('Full ARM resource ID of the AzureStackHCI virtual network used for the VMs.') 52 | param logicalNetworkId string 53 | 54 | @description('Full ARM resource ID of the AzureStackHCI virtual machine image used for the VMs.') 55 | param imageId string 56 | 57 | @description('The name of the hostpool') 58 | param hostpoolName string 59 | 60 | @description('The token for adding VMs to the hostpool') 61 | param hostpoolToken string 62 | 63 | @description('Session host configuration version of the host pool.') 64 | param SessionHostConfigurationVersion string = '' 65 | 66 | @description('Number of session hosts that will be created and added to the hostpool.') 67 | param rdshNumberOfInstances int 68 | 69 | @description('OU Path for the domain join') 70 | param oUPath string = '' 71 | 72 | @description('Domain to join') 73 | param domain string = '' 74 | 75 | @description('Uri to download file that is executed in the custom script extension') 76 | param fileUri string 77 | 78 | @description('Uri to download file that is executed in the custom script extension') 79 | param fileName string = 'HciCustomScript.ps1' 80 | 81 | @description('The base URI where the Configuration.zip script is located to install the AVD agent on the VM') 82 | param configurationZipUri string 83 | 84 | @description('IMPORTANT: You can use this parameter for the test purpose only as AAD Join is public preview. True if AAD Join, false if AD join') 85 | param aadJoin bool = false 86 | 87 | @description('IMPORTANT: Please don\'t use this parameter as intune enrollment is not supported yet. True if intune enrollment is selected. False otherwise') 88 | param intune bool = false 89 | 90 | @description('System data is used for internal purposes, such as support preview features.') 91 | param systemData object = {} 92 | 93 | var domain_var = ((domain == '') ? last(split(domainAdministratorUsername, '@')) : domain) 94 | var hostPoolNameArgument = '-HostPoolName ${hostpoolName}' 95 | var registrationTokenArgument = ' -RegistrationInfoToken ${hostpoolToken}' 96 | var sessionHostConfigurationLastUpdateTimeArgument = ' -SessionHostConfigurationLastUpdateTime ${SessionHostConfigurationVersion}' 97 | var artifactUriArgument = ' -ArtifactUri ${configurationZipUri}' 98 | var customScriptParentFolder = split(fileUri, '/')[4] 99 | var customScriptFilePath = '${customScriptParentFolder}/${fileName}' 100 | var arguments = concat(hostPoolNameArgument, registrationTokenArgument, artifactUriArgument) 101 | var isVMAdminAccountCredentialsProvided = ((vmAdministratorAccountUsername != '') && (vmAdministratorAccountPassword != '')) 102 | var vmAdministratorUsername = (isVMAdminAccountCredentialsProvided ? vmAdministratorAccountUsername : first(split(domainAdministratorUsername, '@'))) 103 | var vmAdministratorPassword = (isVMAdminAccountCredentialsProvided ? vmAdministratorAccountPassword : domainAdministratorPassword) 104 | 105 | resource rdshPrefix_vmInitialNumber_nic 'Microsoft.AzureStackHCI/networkinterfaces@2023-09-01-preview' = [for i in range(0, rdshNumberOfInstances): { 106 | name: '${rdshPrefix}${(i + vmInitialNumber)}-nic' 107 | location: location 108 | extendedLocation: { 109 | type: 'CustomLocation' 110 | name: customLocationId 111 | } 112 | tags: networkInterfaceTags 113 | properties: { 114 | ipConfigurations: [ 115 | { 116 | name: '${rdshPrefix}${(i + vmInitialNumber)}-nic' 117 | properties: { 118 | subnet: { 119 | id: logicalNetworkId 120 | } 121 | } 122 | } 123 | ] 124 | } 125 | }] 126 | 127 | resource rdshPrefix_vmInitialNumber 'Microsoft.HybridCompute/machines@2023-03-15-preview' = [for i in range(0, rdshNumberOfInstances): { 128 | name: concat(rdshPrefix, (i + vmInitialNumber)) 129 | location: location 130 | kind: 'HCI' 131 | identity: { 132 | type: 'SystemAssigned' 133 | } 134 | }] 135 | 136 | resource default 'microsoft.azurestackhci/virtualmachineinstances@2023-09-01-preview' = [for i in range(0, rdshNumberOfInstances): { 137 | scope: 'Microsoft.HybridCompute/machines/${rdshPrefix}${(i + vmInitialNumber)}' 138 | name: 'default' 139 | extendedLocation: { 140 | type: 'CustomLocation' 141 | name: customLocationId 142 | } 143 | identity: { 144 | type: 'SystemAssigned' 145 | } 146 | properties: { 147 | hardwareProfile: { 148 | vmSize: 'Custom' 149 | processors: virtualProcessorCount 150 | memoryMB: memoryMB 151 | dynamicMemoryConfig: (dynamicMemoryConfig ? { 152 | maximumMemoryMB: maximumMemoryMB 153 | minimumMemoryMB: minimumMemoryMB 154 | targetMemoryBuffer: targetMemoryBuffer 155 | } : json('null')) 156 | } 157 | osProfile: { 158 | adminUsername: vmAdministratorUsername 159 | adminPassword: vmAdministratorPassword 160 | windowsConfiguration: { 161 | provisionVMAgent: true 162 | } 163 | computerName: concat(rdshPrefix, (i + vmInitialNumber)) 164 | } 165 | storageProfile: { 166 | imageReference: { 167 | id: imageId 168 | } 169 | } 170 | networkProfile: { 171 | networkInterfaces: [ 172 | { 173 | id: resourceId('Microsoft.AzureStackHCI/networkinterfaces', '${rdshPrefix}${(i + vmInitialNumber)}-nic') 174 | } 175 | ] 176 | } 177 | } 178 | dependsOn: [ 179 | rdshPrefix_vmInitialNumber 180 | 'Microsoft.AzureStackHCI/networkInterfaces/${rdshPrefix}${(i + vmInitialNumber)}-nic' 181 | ] 182 | }] 183 | 184 | resource rdshPrefix_vmInitialNumber_CustomScriptExtension 'Microsoft.HybridCompute/machines/extensions@2023-03-15-preview' = [for i in range(0, rdshNumberOfInstances): { 185 | name: '${rdshPrefix}${(i + vmInitialNumber)}/CustomScriptExtension' 186 | location: location 187 | properties: { 188 | publisher: 'Microsoft.Compute' 189 | type: 'CustomScriptExtension' 190 | autoUpgradeMinorVersion: true 191 | settings: { 192 | fileUris: [ 193 | fileUri 194 | ] 195 | } 196 | protectedSettings: { 197 | commandToExecute: 'powershell -ExecutionPolicy Unrestricted -File ${customScriptFilePath} ${arguments}' 198 | } 199 | } 200 | dependsOn: [ 201 | default 202 | ] 203 | }] 204 | 205 | resource rdshPrefix_vmInitialNumber_AADLoginForWindows 'Microsoft.HybridCompute/machines/extensions@2023-03-15-preview' = [for i in range(0, rdshNumberOfInstances): if (aadJoin && (contains(systemData, 'aadJoinPreview') ? (!systemData.aadJoinPreview) : bool('true'))) { 206 | name: '${rdshPrefix}${(i + vmInitialNumber)}/AADLoginForWindows' 207 | location: location 208 | properties: { 209 | publisher: 'Microsoft.Azure.ActiveDirectory' 210 | type: 'AADLoginForWindows' 211 | typeHandlerVersion: '2.0' 212 | autoUpgradeMinorVersion: true 213 | settings: (intune ? { 214 | mdmId: '0000000a-0000-0000-c000-000000000000' 215 | } : { 216 | mdmId: '' 217 | }) 218 | } 219 | dependsOn: [ 220 | rdshPrefix_vmInitialNumber_CustomScriptExtension 221 | ] 222 | }] 223 | 224 | resource rdshPrefix_vmInitialNumber_joindomain 'Microsoft.HybridCompute/machines/extensions@2023-03-15-preview' = [for i in range(0, rdshNumberOfInstances): if (!aadJoin) { 225 | name: '${rdshPrefix}${(i + vmInitialNumber)}/joindomain' 226 | location: location 227 | properties: { 228 | publisher: 'Microsoft.Compute' 229 | type: 'JsonADDomainExtension' 230 | typeHandlerVersion: '1.3' 231 | autoUpgradeMinorVersion: true 232 | settings: { 233 | name: domain_var 234 | oUPath: oUPath 235 | user: domainAdministratorUsername 236 | restart: 'true' 237 | options: '3' 238 | } 239 | protectedSettings: { 240 | password: domainAdministratorPassword 241 | } 242 | } 243 | dependsOn: [ 244 | rdshPrefix_vmInitialNumber_CustomScriptExtension 245 | ] 246 | }] 247 | 248 | resource rdshPrefix_vmInitialNumber_azmonitor 'Microsoft.HybridCompute/machines/extensions@2023-03-15-preview' = [for i in range(0, rdshNumberOfInstances): { 249 | name: '${rdshPrefix}${(i + vmInitialNumber)}/azmonitor' 250 | location: location 251 | properties: { 252 | publisher: 'Microsoft.Azure.Monitor' 253 | type: 'AzureMonitorWindowsAgent' 254 | typeHandlerVersion: '1.5' 255 | autoUpgradeMinorVersion: true 256 | } 257 | dependsOn: [ 258 | rdshPrefix_vmInitialNumber_joindomain 259 | ] 260 | }] -------------------------------------------------------------------------------- /AVD/intendedway/bicep/bicepmodulespooled/appgrouproleassignment.bicep: -------------------------------------------------------------------------------- 1 | param principalID string 2 | param appGroupName string 3 | 4 | var roledefinitionId = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1d18fff3-a72a-46b5-b4a9-0b38a3cd7e63') 5 | 6 | resource targetscope 'Microsoft.DesktopVirtualization/applicationGroups@2023-09-05' existing = { 7 | name: appGroupName 8 | } 9 | 10 | resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { 11 | name: guid('avdadminrole-${targetscope.name}') 12 | scope: targetscope 13 | properties: { 14 | roleDefinitionId: roledefinitionId 15 | principalId: principalID 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /AVD/intendedway/bicep/bicepmodulespooled/applicationgroup.bicep: -------------------------------------------------------------------------------- 1 | param hostPoolName string = '' 2 | param hostPoolRG string = resourceGroup().name 3 | param location string = resourceGroup().location 4 | param tagValues object = {} 5 | 6 | var applicationgroupName = '${hostPoolName}-DAG' 7 | var hostpoolsID = '/subscriptions/${subscription().subscriptionId}/resourceGroups/${hostPoolRG}/providers/Microsoft.DesktopVirtualization/hostpools/${hostPoolName}' 8 | var friendlyName = '${hostPoolName} Desktop Application Group' 9 | 10 | resource applicationgroup 'Microsoft.DesktopVirtualization/applicationgroups@2023-09-05' = { 11 | name: applicationgroupName 12 | location: location 13 | kind: 'Desktop' 14 | tags: tagValues 15 | properties: { 16 | hostPoolArmPath: hostpoolsID 17 | description: 'Desktop Application Group created through ARM template' 18 | friendlyName: friendlyName 19 | applicationGroupType: 'Desktop' 20 | } 21 | } 22 | output applicationGroupID string = applicationgroup.id 23 | output applicationGroupName string = applicationgroup.name 24 | -------------------------------------------------------------------------------- /AVD/intendedway/bicep/bicepmodulespooled/hostpool.bicep: -------------------------------------------------------------------------------- 1 | @description('The name of the hostpool') 2 | param hostpoolName string 3 | 4 | @description('Location for all resources to be created in.') 5 | param location string = resourceGroup().location 6 | 7 | @description('The tags to be assigned to the resources') 8 | param tagValues object = { 9 | creator: 'userxyz' 10 | env: 'avdPoc' 11 | } 12 | 13 | param tokenExpirationTime string = dateTimeAdd(utcNow('yyyy-MM-dd T00:00:00'),'P1D','o') 14 | 15 | resource hostPool 'Microsoft.DesktopVirtualization/hostPools@2023-09-05' = { 16 | name: hostpoolName 17 | location: location 18 | tags: tagValues 19 | properties:{ 20 | hostPoolType: 'Pooled' 21 | loadBalancerType: 'BreadthFirst' 22 | description: 'first personal avd aadonly host pool' 23 | friendlyName: 'friendly name' 24 | preferredAppGroupType: 'Desktop' 25 | customRdpProperty: 'targetisaadjoined:i:1;drivestoredirect:s:*;audiomode:i:0;videoplaybackmode:i:1;redirectclipboard:i:1;redirectprinters:i:1;devicestoredirect:s:*;redirectcomports:i:1;redirectsmartcards:i:1;usbdevicestoredirect:s:*;enablecredsspsupport:i:1;redirectwebauthn:i:1;use multimon:i:1;autoreconnection enabled:i:1;bandwidthautodetect:i:1;networkautodetect:i:1;audiocapturemode:i:1;encode redirected video capture:i:1;redirected video capture encoding quality:i:2;camerastoredirect:s:*;enablerdsaadauth:i:1' 26 | registrationInfo: { 27 | expirationTime: tokenExpirationTime 28 | registrationTokenOperation: 'Update' 29 | } 30 | } 31 | } 32 | 33 | output registrationInfoToken string = reference(hostPool.id).registrationInfo.token 34 | 35 | -------------------------------------------------------------------------------- /AVD/intendedway/bicep/bicepmodulespooled/hostpoolgalleryvm.bicep: -------------------------------------------------------------------------------- 1 | @description('The name of the hostpool') 2 | param hostpoolName string 3 | 4 | @description('The token for adding VMs to the hostpool') 5 | param hostpoolToken string = '' 6 | 7 | @description('(Required when vmImageType = Gallery) Gallery image Offer.') 8 | param vmGalleryImageOffer string = 'office-365' 9 | //param vmGalleryImageOffer string = 'windows-10' 10 | 11 | @description('(Required when vmImageType = Gallery) Gallery image Publisher.') 12 | param vmGalleryImagePublisher string = 'MicrosoftWindowsDesktop' 13 | 14 | @description('(Required when vmImageType = Gallery) Gallery image SKU.') 15 | param vmGalleryImageSKU string = 'win11-23h2-avd-m365' 16 | //param vmGalleryImageSKU string = 'win10-22h2-avd-g2' 17 | 18 | @description('This prefix will be used in combination with the VM number to create the VM name. This value includes the dash, so if using \'rdsh\' as the prefix, VMs would be named \'rdsh-0\', \'rdsh-1\', etc. You should use a unique prefix to reduce name collisions in Active Directory.') 19 | param vmPrefix string = take(toLower('$hostpoolName-vm'), 10) 20 | 21 | @description('This is the suffix to the vm names. VM will be named \'[vmPrefix]-[vmInstanceSuffixes]\'') 22 | param vmInstanceSuffixes array = [ 23 | '0' 24 | '1' 25 | ] 26 | 27 | @allowed([ 28 | 'Premium_LRS' 29 | 'StandardSSD_LRS' 30 | 'Standard_LRS' 31 | 'UltraSSD_LRS' 32 | ]) 33 | @description('The VM disk type for the VM: HDD or SSD.') 34 | param vmDiskType string = 'Standard_LRS' 35 | 36 | @description('The size of the session host VMs.') 37 | param vmSize string = 'Standard_D2s_v4' // 'Standard_A2' 38 | 39 | @description('intune mdm id') 40 | param intune bool = false 41 | /* 42 | Standard_B2ms 43 | Standard_B2s 44 | Standard_DS2_v2 45 | Standard_F2s 46 | Standard_D2s_v3 47 | Standard_D2s_v4 48 | Standard_F2s_v2 49 | Standard_D2as_v4 50 | */ 51 | 52 | @description('Enables Accelerated Networking feature, notice that VM size must support it, this is supported in most of general purpose and compute-optimized instances with 2 or more vCPUs, on instances that supports hyperthreading it is required minimum of 4 vCPUs.') 53 | param enableAcceleratedNetworking bool = true 54 | 55 | @description('The username for the admin.') 56 | param administratorAccountUsername string 57 | 58 | @description('The password that corresponds to the existing domain username.') 59 | @secure() 60 | param administratorAccountPassword string 61 | 62 | @description('The unique id of the subnet for the nics.') 63 | param subnet_id string 64 | 65 | @description('Location for all resources to be created in.') 66 | param location string = resourceGroup().location 67 | 68 | //@description('The rules to be given to the new network security group') 69 | //param networkSecurityGroupRules array = [] 70 | 71 | //@description('OUPath for the domain join') 72 | //param ouPath string = '' 73 | 74 | //@description('Domain to join') 75 | //param domain string = '' 76 | 77 | @description('The tags to be assigned to the resources') 78 | param tagValues object = { 79 | creator: 'bfrank' 80 | env: 'avdPoc' 81 | } 82 | 83 | var WvdAgentArtifactsLocation = 'https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_1.0.02454.213.zip' //'https://wvdportalstorageblob.blob.core.windows.net/galleryartifacts/Configuration_7-12-2021.zip' 84 | var artifactsLocationSASToken = '' 85 | //var existingDomainUsername = first(split(administratorAccountUsername, '@')) 86 | //var domainName = ((domain == '') ? last(split(administratorAccountUsername, '@')) : domain) 87 | var storageAccountType = vmDiskType 88 | //var newNsgNameVariable = '${vmPrefix}-nsg' 89 | 90 | /* 91 | resource newNsgName 'Microsoft.Network/networkSecurityGroups@2023-06-01' = { 92 | name: newNsgNameVariable 93 | location: location 94 | tags: tagValues 95 | properties: { 96 | securityRules: networkSecurityGroupRules 97 | } 98 | } 99 | */ 100 | 101 | resource vmPrefix_vmInstanceSuffixes_nic 'Microsoft.Network/networkInterfaces@2023-06-01' = [for item in vmInstanceSuffixes: { 102 | name: '${vmPrefix}${item}-nic' 103 | location: location 104 | tags: tagValues 105 | properties: { 106 | ipConfigurations: [ 107 | { 108 | name: 'ipconfig' 109 | properties: { 110 | privateIPAllocationMethod: 'Dynamic' 111 | subnet: { 112 | id: subnet_id 113 | } 114 | } 115 | } 116 | ] 117 | enableAcceleratedNetworking: enableAcceleratedNetworking 118 | //networkSecurityGroup: json('{"id": "${newNsgName.id}"}') 119 | } 120 | /* 121 | dependsOn: [ 122 | newNsgName 123 | ] 124 | */ 125 | }] 126 | 127 | resource vmPrefix_vmInstanceSuffixes 'Microsoft.Compute/virtualMachines@2023-09-01' = [for item in vmInstanceSuffixes: { 128 | name: '${vmPrefix}${item}' 129 | location: location 130 | tags: tagValues 131 | identity: { 132 | type: 'SystemAssigned' 133 | } 134 | properties: { 135 | hardwareProfile: { 136 | vmSize: vmSize 137 | } 138 | osProfile: { 139 | computerName: '${vmPrefix}${item}' 140 | adminUsername: administratorAccountUsername 141 | adminPassword: administratorAccountPassword 142 | } 143 | storageProfile: { 144 | imageReference: { 145 | publisher: vmGalleryImagePublisher 146 | offer: vmGalleryImageOffer 147 | sku: vmGalleryImageSKU 148 | version: 'latest' 149 | } 150 | osDisk: { 151 | createOption: 'FromImage' 152 | name: '${vmPrefix}${item}-osdisk' 153 | managedDisk: { 154 | storageAccountType: storageAccountType 155 | } 156 | } 157 | } 158 | networkProfile: { 159 | networkInterfaces: [ 160 | { 161 | id: resourceId('Microsoft.Network/networkInterfaces', '${vmPrefix}${item}-nic') 162 | } 163 | ] 164 | } 165 | diagnosticsProfile: { 166 | bootDiagnostics: { 167 | enabled: false 168 | } 169 | } 170 | licenseType: 'Windows_Client' 171 | } 172 | dependsOn: [ 173 | vmPrefix_vmInstanceSuffixes_nic 174 | ] 175 | }] 176 | 177 | /* 178 | resource vmPrefix_vmInstanceSuffixes_joindomain 'Microsoft.Compute/virtualMachines/extensions@2023-09-01' = [for item in vmInstanceSuffixes: { 179 | name: '${vmPrefix}${item}/joindomain' 180 | location: location 181 | properties: { 182 | publisher: 'Microsoft.Compute' 183 | type: 'JsonADDomainExtension' 184 | typeHandlerVersion: '1.3' 185 | autoUpgradeMinorVersion: true 186 | settings: { 187 | name: domainName 188 | ouPath: ouPath 189 | user: '${administratorAccountUsername}@${domainName}' 190 | restart: 'true' 191 | options: '3' 192 | } 193 | protectedSettings: { 194 | password: administratorAccountPassword 195 | } 196 | } 197 | dependsOn: [ 198 | vmPrefix_vmInstanceSuffixes 199 | ] 200 | }] 201 | */ 202 | 203 | resource vmPrefix_vmInstanceSuffixes_aadjoin 'Microsoft.Compute/virtualMachines/extensions@2023-09-01' = [for item in vmInstanceSuffixes: { 204 | name: '${vmPrefix}${item}/AADLoginForWindows' 205 | location: location 206 | properties: { 207 | publisher: 'Microsoft.Azure.ActiveDirectory' 208 | type: 'AADLoginForWindows' 209 | typeHandlerVersion: '2.0' 210 | autoUpgradeMinorVersion: true 211 | settings: intune ?{ 212 | mdmId: '0000000a-0000-0000-c000-000000000000' 213 | }: null 214 | } 215 | dependsOn: [ 216 | vmPrefix_vmInstanceSuffixes_dscextension 217 | ] 218 | }] 219 | 220 | resource vmPrefix_vmInstanceSuffixes_dscextension 'Microsoft.Compute/virtualMachines/extensions@2023-09-01' = [for item in vmInstanceSuffixes: { 221 | name: '${vmPrefix}${item}/Microsoft.PowerShell.DSC' 222 | location: location 223 | properties: { 224 | publisher: 'Microsoft.Powershell' 225 | type: 'DSC' 226 | typeHandlerVersion: '2.73' 227 | autoUpgradeMinorVersion: true 228 | settings: { 229 | modulesUrl: '${WvdAgentArtifactsLocation}${artifactsLocationSASToken}' 230 | configurationFunction: 'Configuration.ps1\\AddSessionHost' 231 | properties: { 232 | hostPoolName: hostpoolName 233 | registrationInfoToken: hostpoolToken 234 | mdmId: intune ? '0000000a-0000-0000-c000-000000000000' : '' 235 | aadJoin: true 236 | aadJoinPreview:false 237 | UseAgentDownloadEndpoint: true 238 | sessionHostConfigurationLastUpdateTime: false 239 | } 240 | } 241 | } 242 | dependsOn: [ 243 | vmPrefix_vmInstanceSuffixes 244 | ] 245 | }] 246 | 247 | output vmNames array = [for item in vmInstanceSuffixes: '${vmPrefix}${item}'] 248 | 249 | 250 | -------------------------------------------------------------------------------- /AVD/intendedway/bicep/bicepmodulespooled/vmroleassignment.bicep: -------------------------------------------------------------------------------- 1 | param principalID string 2 | param vms array = [] 3 | 4 | var VirtualMachineUserLogin_role = subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'fb879df8-f326-4884-b1cf-06f3ad86be52') 5 | 6 | resource targetscope 'Microsoft.Compute/virtualMachines@2023-09-01' existing = [for item in vms: { 7 | name: item 8 | }] 9 | 10 | 11 | resource role1 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for i in range(0, length(vms)): { 12 | name: guid('avduserrole-${targetscope[i].name}') 13 | scope: targetscope[i] 14 | properties: { 15 | roleDefinitionId: VirtualMachineUserLogin_role 16 | principalId: principalID 17 | } 18 | }] 19 | 20 | -------------------------------------------------------------------------------- /AVD/intendedway/bicep/bicepmodulespooled/workspace.bicep: -------------------------------------------------------------------------------- 1 | param workspaceName string = '' 2 | param tagValues object 3 | param workspaceFriendlyName string = 'Cloud Workspace' 4 | 5 | @description('description') 6 | param applicationGroups array = [ 7 | ] 8 | @description('name of your avd workspace') 9 | param location string = resourceGroup().location 10 | 11 | resource workspacesName_resource 'Microsoft.DesktopVirtualization/workspaces@2023-09-05' = { 12 | name: workspaceName 13 | location: location 14 | tags: tagValues 15 | properties: { 16 | friendlyName: workspaceFriendlyName 17 | applicationGroupReferences: applicationGroups 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /AVD/intendedway/readme.md: -------------------------------------------------------------------------------- 1 | # How To Deploy AVD On AzStack HCI (using the Azure Portal) 2 | With the advent of 23H2 and the Azure Resource Bridge being a released part of it - the provisioning of AVD VMs (aka Session Hosts) is less manual. 3 | So compared to the [manual](../manually/readme.md) approach the VMs will be created and hence managable by the Azure Resource Bridge. A lot of steps can be done now from the Azure Portal. 4 | This does not mean that it cannot be automated - infact it's the opposite as like other Azure resources you now have the option to automate deployments using ARM, Bicep, Terraform to deploy AVD on HCI. 5 | So you can: 6 | - Use the Azure portal to ... 7 | - Use ARM, Bicep or Terraform to ... 8 | ...deploy AVD on HCI. -------------------------------------------------------------------------------- /AVD/manually/ArcEnabledVMs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/AVD/manually/ArcEnabledVMs.png -------------------------------------------------------------------------------- /AVD/manually/CreateAVDDesktop.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | Attention: Don't run all of this script as at once!!! 3 | copy and replace the '????????' with your values. 4 | read the comments to follow where, what the script should be executed + what it does. 5 | intended to give show the required steps to setup an AVD Desktop onprem on AzStack HCI 6 | #> 7 | 8 | #region helper functions (pls run if you want to have a folder picker dialog) 9 | function ShowFolderOpenDialog ([string]$Title) 10 | { 11 | $ui = new-object -ComObject "Shell.Application" 12 | $path = $ui.BrowseForFolder(0,"$Title",0,0) 13 | $path.Self.Path 14 | } 15 | #endregion 16 | 17 | #install required Azure PowerShell modules in case the following throws errors. 18 | #Install-Module Az 19 | 20 | #login to Azure 21 | Login-AzAccount -Environment AzureCloud #-UseDeviceAuthentication 22 | 23 | #hit the right subscription 24 | Get-AzSubscription | Out-GridView -Title "Select the right subscription" -OutputMode Single | Select-AzSubscription 25 | 26 | #we need the context info later 27 | $azureContext = Get-AzContext 28 | 29 | #select a location near you 30 | $location = Get-AzLocation | Out-GridView -Title "Select your location (e.g. westeurope)" -OutputMode Single 31 | 32 | #region select an Azure AVD Image (e.g. Windows 11) and create an Azure disk of it for later download (to onprem) 33 | #get the AVDs group published images by selecting 'microsoftwindowsdesktop' 34 | $imPub = Get-AzVMImagePublisher -Location $($location.Location) | Out-GridView -Title "Select image publisher (e.g. 'microsoftwindowsdesktop')" -OutputMode Single 35 | 36 | #select the AVD Desktop OS of interest e.g. 'windows-11' 37 | $PublisherOffer = Get-AzVMImageOffer -Location $($location.Location) -PublisherName $($imPub.PublisherName) | Out-GridView -Title "Select your offer (e.g. windows-11)" -OutputMode Single 38 | 39 | # select the AVD version e.g. 'win11-21h2-avd' 40 | $VMImageSKU = (Get-AzVMImageSku -Location $($location.Location) -PublisherName $($imPub.PublisherName) -Offer $PublisherOffer.Offer).Skus | Out-GridView -Title "Select your imagesku (e.g. win11-21h2-avd)" -OutputMode Single 41 | 42 | #select latest version 43 | $VMImage = Get-AzVMImage -Location $($location.Location) -PublisherName $PublisherOffer.PublisherName -Offer $PublisherOffer.Offer -Skus $VMImageSKU | Out-GridView -Title "Select your version" -OutputMode Single 44 | 45 | #Create a VHDX (Gen2) from this image 46 | $imageOSDiskRef = @{Id = $vmImage.Id} 47 | $diskRG = Get-AzResourceGroup | Out-GridView -Title "Select The Target Resource Group" -OutputMode Single 48 | $diskName = "disk-" + $vmImage.Skus 49 | $newdisk = New-AzDisk -ResourceGroupName $diskRG.ResourceGroupName -DiskName "$diskName" -Disk $(New-AzDiskConfig -ImageReference $imageOSDiskRef -Location $location.Location -CreateOption FromImage -HyperVGeneration V2 -OsType Windows) 50 | 51 | Write-Host "You should now have a new disk named $(newdisk.name) in your resourcegroup" -ForegroundColor Green 52 | #endregion 53 | 54 | #region Create a temp. download link and download the disk as virtual disk (.vhd) 55 | $AccessSAS = $newdisk | Grant-AzDiskAccess -DurationInSecond ([System.TimeSpan]::Parse("05:00:00").TotalSeconds) -Access 'Read' 56 | $newdisk.Name 57 | $DiskURI = $AccessSAS.AccessSAS 58 | 59 | $folder = ShowFolderOpenDialog "Where should your disk go to?" 60 | $diskDestination = "$folder\$($newdisk.Name).vhd" 61 | Write-Host "Your disk will be placed into: $diskDestination" -ForegroundColor Green 62 | #"Start-BitsTransfer ""$DiskURI"" ""$diskDestination"" -Priority High -RetryInterval 60 -Verbose -TransferType Download" 63 | 64 | #or use azcopy as it is much faster!!! 65 | invoke-webrequest -uri "https://aka.ms/downloadazcopy-v10-windows" -OutFile "$folder\azcopy.zip" -verbose 66 | Expand-Archive "$folder\azcopy.zip" "$folder" -force -verbose 67 | copy-item "$folder\azcopy_windows_amd64_*\\azcopy.exe\\" -Destination "$folder" -verbose 68 | cd "$folder\" 69 | .\azcopy.exe copy $DiskURI $diskDestination 70 | #endregion 71 | 72 | #region Convert to a dynamic vhdx! 73 | $finalfolder = ShowFolderOpenDialog "Where should your compressed dynamic vhdx disk go?" 74 | $diskFinalDestination = "$finalfolder\$($newdisk.Name).vhdx" 75 | Convert-VHD -Path "$diskDestination" -DestinationPath "$diskFinalDestination" -VHDType Dynamic 76 | #endregion 77 | 78 | # Do copy the disk to your AzStack HCI cluster storage 79 | # Do create a new VM (Gen2) based on this new disk. 80 | # Do a Enter-PSSession to your AzStack HCI node e.g by 81 | etsn azhci1node1 # '????????' requires your node name here 82 | sleep 3 83 | 84 | #region Do domain join of VM - manual! or using a script e.g. below 85 | #which VM? 86 | do { 87 | $vms = @("") 88 | Get-VM | foreach -Begin { $i = 0 } -Process { 89 | $i++ 90 | $vms += "{0}. {1}" -f $i, $_.Name 91 | } -outvariable menu 92 | $vms | Format-Wide { $_ } -Column 4 -Force 93 | $r = Read-Host "Select a vm" 94 | $vm = $vms[$r].Split()[1] 95 | if ($vm -eq $null) { Write-Host "You must make a valid selection" -ForegroundColor Red } 96 | else { 97 | Write-Host "Selecting vm $($vms[$r])" -ForegroundColor Green 98 | } 99 | } 100 | until ($vm -ne $null) 101 | 102 | #enter powershell session into VM 103 | etsn -VMName $vm 104 | 105 | #make IP Addresses & DNS settings 106 | $InterfaceAlias = (Get-NetAdapter | Get-NetIPAddress | where Addressfamily -eq "IPv4").InterfaceAlias 107 | Set-DnsClientServerAddress -InterfaceAlias "$InterfaceAlias" -ServerAddresses "192.168.xxx.xxx" #'????????' 108 | 109 | New-NetIPAddress -InterfaceAlias $InterfaceAlias -IPAddress 192.168.xxx.xxx -PrefixLength 24 -DefaultGateway 192.168.xxx.xxx -AddressFamily IPv4 110 | $newVMName = "W11-0815" # '????????' 111 | $DomainName = "contoso" # '????????' 112 | $credential = get-credential -Message "domain creds" -UserName "$DomainName\administrator" 113 | Add-Computer -ComputerName localhost -NewName $newVMName -DomainName $DomainName -Credential $credential -Verbose -Restart #-OUPath "OU=Hostpool1,OU=AVD,DC=contoso,DC=local" 114 | #endregion 115 | 116 | 117 | #vm has restarted? Tunnel into it again! 118 | Enter-PSSession -VMName $vm 119 | 120 | #region Do Arc onboarding - !!!Must fill in the ???????? values before!!! 121 | # Download the installation package 122 | Invoke-WebRequest -Uri "https://aka.ms/azcmagent-windows" -TimeoutSec 30 -OutFile "$env:TEMP\install_windows_azcmagent.ps1" 123 | 124 | # Install the hybrid agent 125 | & "$env:TEMP\install_windows_azcmagent.ps1" 126 | if($LASTEXITCODE -ne 0) { 127 | throw "Failed to install the hybrid agent" 128 | } 129 | # Run connect command 130 | & "$env:ProgramW6432\AzureConnectedMachineAgent\azcmagent.exe" connect ` 131 | --resource-group "RG-????????" ` 132 | --tenant-id "16a6152e-????????" ` 133 | --location "westeurope" ` 134 | --subscription-id "ad8d0fcf-????????" ` 135 | --cloud "AzureCloud" ` 136 | --tags "Datacenter=RZ1,City=SomeCity,CountryOrRegion=Germany" 137 | 138 | if($LastExitCode -eq 0){Write-Host -ForegroundColor yellow "To view your onboarded server(s), navigate to https://portal.azure.com/#blade/HubsExtension/BrowseResource/resourceType/Microsoft.HybridCompute%2Fmachines"} 139 | #endregion 140 | 141 | #region AVD agent download (do this in the VM) 142 | #https://docs.microsoft.com/en-us/azure/virtual-desktop/create-host-pools-powershell?tabs=azure-powershell#register-the-virtual-machines-to-the-azure-virtual-desktop-host-pool 143 | 144 | #this will be our temp folder - need it for download / logging 145 | $tmpDir = "c:\temp\" 146 | 147 | #create folder if it doesn't exist 148 | if (!(Test-Path $tmpDir)) { mkdir $tmpDir -force } 149 | 150 | $agentURIs = @{ 151 | 'agent.msi' = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrmXv" 152 | 'bootloader.msi' = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrxrH"} 153 | 154 | foreach ($uri in $agentURIs.GetEnumerator()) 155 | { 156 | Write-Output "starting download...." 157 | Invoke-WebRequest "$($uri.value)" -OutFile "$tmpDir\$($uri.key)" 158 | } 159 | 160 | $RegistrationToken = "eyJhbGciOiJ...." # a valid hostpool registration token [Azure portal] -> Azure Virtual Desktop -> Host Pool -> %Your Hostpool% -> Registration token 161 | #unattended install 162 | start-process -filepath msiexec -ArgumentList "/i ""$tmpDir\agent.msi"" /l*v ""$tmpDir\agent.msi.log"" REGISTRATIONTOKEN=$RegistrationToken /passive /qn /quiet /norestart " -Wait 163 | start-process -filepath msiexec -ArgumentList "/i ""$tmpDir\bootloader.msi"" /l*v ""$tmpDir\bootloader.msi.log"" /quiet /qn /norestart /passive" -Wait 164 | 165 | #endregion 166 | 167 | # Log on to the VM 168 | # Do Install the c:\temp\agent.msi 169 | # Do copy & paste the Azure -> Azure Virtual Desktop -> Hostpool -> Registration Token -> into the Agent install window 170 | # Do Install the C:\temp\bootloader.msi 171 | # Now your VM should show up in Azure -> Azure Virtual Desktop -> Hostpool -> Session Host 172 | 173 | 174 | #region RDP Shortpath FW rule in Desktop (part1) 175 | # https://docs.microsoft.com/en-us/azure/virtual-desktop/shortpath 176 | New-NetFirewallRule -DisplayName 'Remote Desktop - Shortpath (UDP-In)' -Action Allow -Description 'Inbound rule for the Remote Desktop service to allow RDP traffic. [UDP 3390]' -Group '@FirewallAPI.dll,-28752' -Name 'RemoteDesktop-UserMode-In-Shortpath-UDP' -PolicyStore PersistentStore -Profile Domain, Private -Service TermService -Protocol udp -LocalPort 3390 -Program '%SystemRoot%\system32\svchost.exe' -Enabled:True 177 | 178 | # you then need to do add the GPO to your AD see: 179 | # https://docs.microsoft.com/en-us/azure/virtual-desktop/shortpath#configure-rdp-shortpath-for-managed-networks 180 | #endregion -------------------------------------------------------------------------------- /AVD/manually/FSLogixGPO.ps1: -------------------------------------------------------------------------------- 1 | $OUSuffix = "OU=AVDHosts,OU=HCI" #the part after the "...,DC=powerkurs,DC=local" so e.g. "OU=HostPool1,OU=AVD" 2 | 3 | 4 | #this will be our temp folder - need it for download / logging 5 | $tmpDir = "c:\temp\" 6 | 7 | #create folder if it doesn't exist 8 | if (!(Test-Path $tmpDir)) { mkdir $tmpDir -force } 9 | 10 | #downloading FSLogix. 11 | Write-Output "downloading fslogix" 12 | 13 | $tempPath = "$tmpDir\FSLogix" 14 | $destinationPath = "$tmpDir\FSLogix.zip" 15 | if (!(Test-Path $destinationPath)) { 16 | "downloading fslogix" 17 | Invoke-WebRequest -Uri "https://aka.ms/fslogix_download" -OutFile $destinationPath -verbose 18 | Expand-Archive $destinationPath -DestinationPath $tempPath -Force -verbose 19 | } 20 | 21 | $fqdn = (Get-WmiObject Win32_ComputerSystem).Domain 22 | $policyDestination = "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\" 23 | 24 | mkdir $policyDestination -Force 25 | mkdir "$policyDestination\en-us" -Force 26 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\*" -filter "*.admx" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions" -Force -Verbose 27 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\*" -filter "*.adml" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\en-us" -Force -Verbose 28 | 29 | 30 | $gpoName = "FSLogixDefExcl{0}" -f [datetime]::Now.ToString('dd-MM-yy_HHmmss') 31 | New-GPO -Name $gpoName 32 | $FSLogixRegKeys = @{ 33 | Enabled = 34 | @{ 35 | Type = "DWord" 36 | Value = 1 #set to 1 to enable. 37 | } 38 | VHDLocations = 39 | @{ 40 | Type = "String" 41 | Value = "\\SOFS\Profile1;\\SOFS\Profile2" 42 | } 43 | DeleteLocalProfileWhenVHDShouldApply = 44 | @{ 45 | Type = "DWord" 46 | Value = 1 47 | } 48 | VolumeType = 49 | @{ 50 | Type = "String" 51 | Value = "VHDX" 52 | } 53 | SizeInMBs = 54 | @{ 55 | Type = "DWord" 56 | Value = 30000 57 | } 58 | IsDynamic = 59 | @{ 60 | Type = "DWord" 61 | Value = 1 62 | } 63 | PreventLoginWithFailure = 64 | @{ 65 | Type = "DWord" 66 | Value = 0 67 | } 68 | LockedRetryInterval = 69 | @{ 70 | Type = "DWord" 71 | Value = 10 72 | } 73 | LockedRetryCount = 74 | @{ 75 | Type = "DWord" 76 | Value = 5 77 | } 78 | FlipFlopProfileDirectoryName = 79 | @{ 80 | Type = "DWord" 81 | Value = 1 82 | } 83 | } 84 | 85 | foreach ($item in $FSLogixRegKeys.GetEnumerator()) { 86 | "{0}:{1}:{2}" -f $item.Name, $item.Value.Type, $item.Value.Value 87 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\SOFTWARE\fslogix\profiles" -ValueName $($item.Name) -Value $($item.Value.Value) -Type $($item.Value.Type) 88 | } 89 | 90 | #enable path exclusions in windows defender for fslogix profiles 91 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows Defender\Exclusions" -ValueName "Exclusions_Paths" -Value 1 -Type DWord 92 | 93 | $excludeList = @" 94 | %ProgramFiles%\FSLogix\Apps\frxdrv.sys, 95 | %ProgramFiles%\FSLogix\Apps\frxdrvvt.sys, 96 | %ProgramFiles%\FSLogix\Apps\frxccd.sys, 97 | %TEMP%\*.VHD, 98 | %TEMP%\*.VHDX, 99 | %Windir%\TEMP\*.VHD, 100 | %Windir%\TEMP\*.VHDX, 101 | \\SOFS\Profile1\*\*.VHD, 102 | \\SOFS\Profile1\*\*.VHDX, 103 | \\SOFS\Profile2\*\*.VHD, 104 | \\SOFS\Profile2\*\*.VHDX 105 | "@ 106 | foreach ($item in $($excludeList -split ',')) { 107 | $item.Trim() 108 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows Defender\Exclusions\Paths" -ValueName "$($item.Trim())" -Value 0 -Type String 109 | } 110 | 111 | Import-Module ActiveDirectory 112 | $DomainPath = $((Get-ADDomain).DistinguishedName) # e.g."DC=contoso,DC=azure" 113 | 114 | $OUPath = $($($OUSuffix + "," + $DomainPath).Split(',').trim() | where { $_ -ne "" }) -join ',' 115 | Write-Output "creating FSLOGIX GPO to OU: $OUPath" 116 | 117 | $existingGPOs = (Get-GPInheritance -Target $OUPath).GpoLinks | Where-Object DisplayName -Like "FSLogixDefExcl*" 118 | 119 | if ($existingGPOs -ne $null) { 120 | Write-Output "removing conflicting GPOs" 121 | $existingGPOs | Remove-GPLink -Verbose 122 | } 123 | 124 | 125 | New-GPLink -Name $gpoName -Target $OUPath -LinkEnabled Yes -verbose 126 | 127 | #cleanup existing but unlinked fslogix GPOs 128 | $existingGPOs | % { [xml]$Report = Get-GPO -guid $_.GpoId | Get-GPOReport -ReportType XML; if (!($Report.GPO.LinksTo)) { Remove-GPO -Guid $_.GpoId -Verbose } } 129 | 130 | -------------------------------------------------------------------------------- /AVD/manually/FSLogixNTFS.ps1: -------------------------------------------------------------------------------- 1 | $DomainName = "myavd" #pls enter your domain name. 2 | 3 | #1st remove all exiting permissions. 4 | $acl = Get-Acl "\\Sofs\fslogix1" 5 | 6 | $acl.Access | % { $acl.RemoveAccessRule($_) } 7 | $acl.SetAccessRuleProtection($true, $false) 8 | $acl | Set-Acl 9 | #add full control for 'the usual suspects' 10 | $users = @("$DomainName\Domain Admins", "System", "Administrators", "Creator Owner" ) 11 | foreach ($user in $users) { 12 | $new = $user, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" 13 | $accessRule = new-object System.Security.AccessControl.FileSystemAccessRule $new 14 | $acl.AddAccessRule($accessRule) 15 | $acl | Set-Acl 16 | } 17 | 18 | #add read & write on parent folder ->required for FSLogix - no inheritence 19 | $allowWVD = "AVD Users", "ReadData, AppendData, ExecuteFile, ReadAttributes, Synchronize", "None", "None", "Allow" 20 | $accessRule = new-object System.Security.AccessControl.FileSystemAccessRule $allowWVD 21 | $acl.AddAccessRule($accessRule) 22 | $acl | Set-Acl -------------------------------------------------------------------------------- /AVD/manually/NotePadPlusPlus.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", 3 | "contentVersion": "1.0.0.0", 4 | "parameters": { 5 | "AppGroupName": { 6 | "type": "string", 7 | "metadata": { 8 | "description": "Your AVD Application Group For Notepad++" 9 | } 10 | } 11 | }, 12 | "resources": [ 13 | { 14 | "type": "Microsoft.DesktopVirtualization/applicationgroups/applications", 15 | "apiVersion": "2022-10-14-preview", 16 | "name": "[concat(parameters('AppGroupName'),'/NotepadPlusPlus')]", 17 | "properties": { 18 | "applicationType": "Inbuilt", 19 | "friendlyName": "", 20 | "description": "", 21 | "filePath": "C:\\Program Files\\Notepad++\\notepad++.exe", 22 | "iconPath": "C:\\Program Files\\Notepad++\\notepad++.exe", 23 | "iconIndex": 0, 24 | "commandLineSetting": "DoNotAllow", 25 | "commandLineArguments": "", 26 | "showInPortal": true 27 | } 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /AVD/manually/RdpShortPathGPO.ps1: -------------------------------------------------------------------------------- 1 | 2 | #region RDP Shortpath FW rule in Desktop (part1) 3 | # https://docs.microsoft.com/en-us/azure/virtual-desktop/shortpath 4 | #New-NetFirewallRule -DisplayName 'Remote Desktop - Shortpath (UDP-In)' -Action Allow -Description 'Inbound rule for the Remote Desktop service to allow RDP traffic. [UDP 3390]' -Group '@FirewallAPI.dll,-28752' -Name 'RemoteDesktop-UserMode-In-Shortpath-UDP' -PolicyStore PersistentStore -Profile Domain, Private, Public -Service TermService -Protocol udp -LocalPort 3390 -Program '%SystemRoot%\system32\svchost.exe' -Enabled:True 5 | 6 | # https://learn.microsoft.com/en-us/azure/virtual-desktop/administrative-template?tabs=group-policy-domain 7 | # you then need to do add the GPO to your AD see: 8 | # https://docs.microsoft.com/en-us/azure/virtual-desktop/shortpath#configure-rdp-shortpath-for-managed-networks 9 | 10 | 11 | $OUSuffix = "OU=HCIMX,OU=AVD" #the part after the "...,DC=powerkurs,DC=local" so e.g. "OU=HostPool1,OU=AVD" 12 | $tmpDir = "c:\temp" 13 | Write-Output "downloading avdgpo" 14 | 15 | $tempPath = "$tmpDir\avdgpo" 16 | if (!(Test-Path $destinationPath)) { 17 | "downloading avdgpo" 18 | Invoke-WebRequest -Uri "https://aka.ms/avdgpo" -OutFile "$tmpDir\avdgpo.cab" -Verbose 19 | expand "$tmpDir\avdgpo.cab" "$tmpDir\avdgpo.zip" 20 | Expand-Archive "$tmpDir\avdgpo.zip" -DestinationPath $tempPath -Force -Verbose 21 | } 22 | 23 | 24 | #terminalserver-avd.admx 25 | #Then copy the terminalserver-avd.adml file to the en-us subfolder. 26 | 27 | $fqdn = (Get-WmiObject Win32_ComputerSystem).Domain 28 | $policyDestination = "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\" 29 | 30 | mkdir $policyDestination -Force 31 | mkdir "$policyDestination\en-us" -Force 32 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\*" -Filter "*.admx" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions" -Force -Verbose 33 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\en-us\terminalserver-avd.adml" -Filter "*.adml" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\en-us" -Force -Verbose 34 | 35 | $gpoName = "avdRdpShortPathGpo{0}" -f [datetime]::Now.ToString('dd-MM-yy_HHmmss') 36 | New-GPO -Name $gpoName 37 | 38 | $RDPShortPathRegKeys = @{ 39 | fUseUdpPortRedirector = 40 | @{ 41 | Type = "DWord" 42 | Value = 1 #set to 1 to enable. 43 | } 44 | UdpRedirectorPort = 45 | @{ 46 | Type = "DWord" 47 | Value = 3390 #set to 1 to enable. 48 | } 49 | } 50 | 51 | foreach ($item in $RDPShortPathRegKeys.GetEnumerator()) { 52 | "{0}:{1}:{2}" -f $item.Name, $item.Value.Type, $item.Value.Value 53 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services" -ValueName $($item.Name) -Value $($item.Value.Value) -Type $($item.Value.Type) 54 | } 55 | 56 | $FWallRuleRegKeys = @{ 57 | 'RemoteDesktop-Shortpath-UDP-In' = 58 | @{ 59 | Type = 'String' 60 | Value = 'v2.31|Action=Allow|Active=TRUE|Dir=In|Protocol=17|LPort=3390|App=%SystemRoot%\system32\svchost.exe|Name=Remote Desktop - Shortpath (UDP-In)|EmbedCtxt=@FirewallAPI.dll,-28752|' 61 | } 62 | } 63 | foreach ($item in $FWallRuleRegKeys.GetEnumerator()) { 64 | "{0}:{1}:{2}" -f $item.Name, $item.Value.Type, $item.Value.Value 65 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\WindowsFirewall\FirewallRules" -ValueName $($item.Name) -Value $($item.Value.Value) -Type $($item.Value.Type) 66 | } 67 | 68 | Import-Module ActiveDirectory 69 | $DomainPath = $((Get-ADDomain).DistinguishedName) # e.g."DC=contoso,DC=azure" 70 | 71 | $OUPath = $($($OUSuffix + "," + $DomainPath).Split(',').trim() | Where-Object { $_ -ne "" }) -join ',' 72 | Write-Output "creating avdgpo GPO to OU: $OUPath" 73 | 74 | 75 | $existingGPOs = (Get-GPInheritance -Target $OUPath).GpoLinks | Where-Object DisplayName -Like "avdRdpShortPathGpo*" 76 | 77 | if ($null -ne $existingGPOs) { 78 | Write-Output "removing conflicting GPOs" 79 | $existingGPOs | Remove-GPLink -Verbose 80 | } 81 | 82 | 83 | New-GPLink -Name $gpoName -Target $OUPath -LinkEnabled Yes -verbose 84 | 85 | 86 | #endregion -------------------------------------------------------------------------------- /AVD/manually/deploytoazure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/AVD/manually/deploytoazure.png -------------------------------------------------------------------------------- /AVD/manually/readme.md: -------------------------------------------------------------------------------- 1 | # How To Deploy AVD On AzStack HCI (manually) 2 | 3 | > Warning: Scripts are samples - no warranties - Use at your own risk!!! 4 | > Note: The caveat to the manual process is that your VMs are 'bybassing' and hence not known to the Azure Resource Bridge - hence cannot be managed by it - hence you will e.g. not be able to start and stop these VMs from the Azure Portal. That might be ok for learning, test and demo purposes but it is **not** the [intended](../intendedway/readme.md) usage. 5 | 6 | ## Watch it done on YT: [Azure Stack HCI - Hybrid Series - Azure Virtual Desktop (AVD)](https://www.youtube.com/watch?v=pXI576Idx-c&list=PLDk1IPeq9PPeVwlvJZgo4n8Mw9Qj5gW0L) 7 | 8 | 9 | ## Manually 10 | See: [Set up Azure Virtual Desktop for Azure Stack HCI (preview) - manual](https://learn.microsoft.com/en-us/azure/virtual-desktop/azure-stack-hci?tabs=manual-setup) 11 | 12 | 0. You require: A registered HCI cluster, AD sync'ed with AAD, a valid Azure subscription 13 | 1. Download the VDI image from the Azure marketplace you want to use. 14 | 2. (optional) Optimize image e.g. convert to dynamically expanding vhdx to save disk space. 15 | 3. (optional - but likely) Create a VM for golden image creation: E.g. to run windows update, install your language packs, applications, frameworks, runtimes -> sysprep (!!!important!!! using: mode:vm) -> Checkpoint for later re-use. 16 | > Note: Only install what will 'survive' a sysprep (e.g. don't do ARC Agent nor AVD Hostpool registration yet) 17 | 4. (optional) FSLogix: Prepare a SMB file share (provide profile share with correct ACLs) 18 | 5. (optional) FSLogix: Prepare a GPO for the AD OrgUnit (OU) the VDIs will be joined to. 19 | 6. Deploy a empty AVD Hostpool in Azure. 20 | 7. Create a Application Group for this Hostpool and allow an AAD User Group access to it. 21 | 8. Create a Workspace containing the App Group created before. 22 | 9. Deploy a VDI VM on HCI using the VDI image (from vhdx obtained in 1./2. or 3.) 23 | 10. Deploy the AVD Agents into the VDI VM 24 | 11. Arc-enable your AVD session hosts. Why?!... 25 | 12. Deploy the Remote Desktop App for the User 26 | 13. (optional) when you are using proxies for the session hosts. 27 | 14. (optional) Publish NotepadPlusPlus in your AVD Application Group 28 | 15. Enable [Azure verification for VMs](https://learn.microsoft.com/en-us/azure-stack/hci/deploy/azure-verification?tabs=wac) for **activation** of your AVD session host OS 29 | 30 | ## 1. Download the VDI image from the Azure marketplace you want to use 31 | Do this on an admin box 32 | ```Powershell 33 | #Make sure you have the Azure modules required 34 | 35 | $modules = @("Az.Accounts","Az.Resources","Az.Compute") 36 | 37 | foreach ($module in $modules) { 38 | if (!(Get-Module -Name $module -ListAvailable)) { 39 | Install-Module -Name $module -Force -Verbose 40 | } 41 | } 42 | 43 | #login to Azure 44 | Login-AzAccount -Environment AzureCloud #-Tenant blabla.onmicrosoft.com -UseDeviceAuthentication 45 | 46 | #hit the right subscription 47 | Get-AzSubscription | Out-GridView -Title "Select the right subscription" -OutputMode Single | Select-AzSubscription 48 | 49 | #we need the context info later 50 | $azureContext = Get-AzContext 51 | 52 | #select a location near you 53 | $location = Get-AzLocation | Out-GridView -Title "Select your location (e.g. westeurope)" -OutputMode Single 54 | 55 | #region select an Azure AVD Image (e.g. Windows 11) and create an Azure disk of it for later download (to onprem) 56 | #get the AVDs group published images by selecting 'microsoftwindowsdesktop' 57 | $imPub = Get-AzVMImagePublisher -Location $($location.Location) | Out-GridView -Title "Select image publisher (e.g. 'microsoftwindowsdesktop')" -OutputMode Single 58 | 59 | #select the AVD Desktop OS of interest e.g. 'windows-11' 60 | $PublisherOffer = Get-AzVMImageOffer -Location $($location.Location) -PublisherName $($imPub.PublisherName) | Out-GridView -Title "Select your offer (e.g. windows-11)" -OutputMode Single 61 | 62 | # select the AVD version e.g. 'win11-21h2-avd' 63 | $VMImageSKU = (Get-AzVMImageSku -Location $($location.Location) -PublisherName $($imPub.PublisherName) -Offer $PublisherOffer.Offer).Skus | Out-GridView -Title "Select your imagesku (e.g. win11-22h2-avd)" -OutputMode Single 64 | 65 | #select latest version 66 | $VMImage = Get-AzVMImage -Location $($location.Location) -PublisherName $PublisherOffer.PublisherName -Offer $PublisherOffer.Offer -Skus $VMImageSKU | Out-GridView -Title "Select your version (highest build number)" -OutputMode Single 67 | 68 | #Create a VHDX (Gen2) from this image 69 | $imageOSDiskRef = @{Id = $vmImage.Id} 70 | $diskRG = Get-AzResourceGroup | Out-GridView -Title "Select The Target Resource Group" -OutputMode Single 71 | $diskName = "disk-" + $vmImage.Skus 72 | $newdisk = New-AzDisk -ResourceGroupName $diskRG.ResourceGroupName -DiskName "$diskName" -Disk $(New-AzDiskConfig -ImageReference $imageOSDiskRef -Location $location.Location -CreateOption FromImage -HyperVGeneration V2 -OsType Windows ) 73 | 74 | Write-Host "You should now have a new disk named $($newdisk.name) in your resourcegroup" -ForegroundColor Green 75 | #endregion 76 | ``` 77 | Next is to download the Azure disk to one of your HCI nodes: 78 | ```PowerShell 79 | #region Create a temp. download link and download the disk as virtual disk (.vhd) 80 | $AccessSAS = $newdisk | Grant-AzDiskAccess -DurationInSecond ([System.TimeSpan]::Parse("05:00:00").TotalSeconds) -Access 'Read' 81 | Write-Host "Generating a temporary download access token for $($newdisk.Name)" -ForegroundColor Green 82 | $DiskURI = $AccessSAS.AccessSAS 83 | 84 | $folder = "\\...OneHCINodeHere....\c$\temp" #enter one of the nodes here - the path must be accessible by the user - beware that there is enough space (127GB) for the disk to be downloaded. 85 | 86 | $diskDestination = "$folder\$($newdisk.Name).vhd" 87 | Write-Host "Your disk will be placed into: $diskDestination" -ForegroundColor Green 88 | #"Start-BitsTransfer ""$DiskURI"" ""$diskDestination"" -Priority High -RetryInterval 60 -Verbose -TransferType Download" 89 | 90 | #or use azcopy as it is much faster!!! 91 | invoke-webrequest -uri "https://aka.ms/downloadazcopy-v10-windows" -OutFile "$env:TEMP\azcopy.zip" -verbose 92 | Expand-Archive "$env:TEMP\azcopy.zip" "$env:TEMP" -force -verbose 93 | copy-item "$env:TEMP\azcopy_windows_amd64_*\\azcopy.exe\\" -Destination "$env:TEMP" -verbose 94 | cd "$env:TEMP\" 95 | &.\azcopy.exe copy $DiskURI $diskDestination --log-level INFO 96 | Remove-Item "azcopy*" -Recurse #cleanup temp 97 | #endregion 98 | ``` 99 | ## 2. (optional) Optimize image e.g. convert to dynamically expanding vhdx to save disk space. 100 | **Do this on system with Hyper-V installed, e.g. on a cluster node.** 101 | 102 | ```PowerShell 103 | #region Convert to a dynamic vhdx! 104 | $finalfolder = "C:\clusterstorage\CSV\Images" # pls enter an existing final destination to hold the AVD image. 105 | $diskFinalDestination = "$finalfolder\Win11-multi-opt.vhdx" 106 | $sourceDiskPath = "c:\temp\disk-win11-22h2-avd.vhd" 107 | 108 | Convert-VHD -Path "$sourceDiskPath" -DestinationPath "$diskFinalDestination" -VHDType Dynamic 109 | 110 | try 111 | { 112 | $beforeMount = (Get-Volume).DriveLetter -split ' ' 113 | Mount-VHD -Path $diskFinalDestination 114 | $afterMount = (Get-Volume).DriveLetter -split ' ' 115 | $driveLetter = $([string](Compare-Object $beforeMount $afterMount -PassThru )).Trim() 116 | Write-Host "Optimizing disk ($($driveLetter)): $diskFinalDestination" -ForegroundColor Green 117 | &defrag "$($driveLetter):" /o /u /v 118 | } 119 | finally 120 | { 121 | Write-Host "dismounting ..." 122 | Dismount-VHD -Path $diskFinalDestination 123 | } 124 | 125 | Optimize-VHD $diskFinalDestination -Mode full 126 | #endregion 127 | ``` 128 | 129 | ## 3. (optional) Create a VM for golden image creation 130 | You probably want update or use your apps in the VDIs. So you before creating desktops from the image you e.g. might want to ...: 131 | - ...do a windows update run first... 132 | - ...install your language packs... 133 | - ...install SW deployment agents, applications, frameworks, runtimes... 134 | ...onto the image - before you finalize it with a sysprep. 135 | 136 | Yes?! -> 137 | 1. Create a VM on HCI using the vhdx file you have just dowloaded|optimized. 138 | 2. Then perform the actions you want as described above. 139 | 3. (optional - recommended) Shutdown the VM in HCI - do a checkpoint - so that you can return to this state later. Boot up again. 140 | 4. Then sysprep the vm to get a generalized version you can create VDI clones from. 141 | ``` 142 | c:\Windows\System32\Sysprep\sysprep.exe /oobe /generalize /shutdown /mode:vm 143 | ``` 144 | > Important to us the **mode:vm** switch (it'll tell the vm that the virtualization platform (HCI) has not changed)otherwise you might experience long boot times. 145 | 5. Export the vm's .vhdx to your HCI cluster's image folder (some place on a CSV) 146 | 147 | ## 4. (optional) FSLogix: Prepare a SMB file share (provide profile share with correct ACLs) 148 | 149 | >Note: There is another YT series: [Azure Stack HCI - Workload Series - Scale-Out File Server (SOFS) For Highly Available - SMB Shares](https://www.youtube.com/watch?v=Rv7BcNUpy_U&list=PLDk1IPeq9PPeWG5m7yjh7ojwJMxalbXTa) that might be useful for hosting AVD profiles. 150 | 151 | Here is a script sample that would set the required NTFS permissions for a folder to be used for FSLogix profiles. 152 | Make sure the domain, path and AD group name are correct for your system. 153 | 154 | ```PowerShell 155 | $DomainName = "myavd" #pls enter your domain name. 156 | 157 | #1st remove all exiting permissions. 158 | $acl = Get-Acl "\\Sofs\fslogix1" # change to your path!!! 159 | 160 | $acl.Access | % { $acl.RemoveAccessRule($_) } 161 | $acl.SetAccessRuleProtection($true, $false) 162 | $acl | Set-Acl 163 | #add full control for 'the usual suspects' 164 | $users = @("$DomainName\Domain Admins", "System", "Administrators", "Creator Owner" ) 165 | foreach ($user in $users) { 166 | $new = $user, "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" 167 | $accessRule = new-object System.Security.AccessControl.FileSystemAccessRule $new 168 | $acl.AddAccessRule($accessRule) 169 | $acl | Set-Acl 170 | } 171 | 172 | # make sure you have an 'AVD Users' group !!! 173 | # this will add read & write on parent folder ->required for FSLogix - no inheritence 174 | $allowWVD = "AVD Users", "ReadData, AppendData, ExecuteFile, ReadAttributes, Synchronize", "None", "None", "Allow" 175 | $accessRule = new-object System.Security.AccessControl.FileSystemAccessRule $allowWVD 176 | $acl.AddAccessRule($accessRule) 177 | $acl | Set-Acl 178 | ``` 179 | 180 | ## 5. (optional) FSLogix: Prepare a GPO for the AD OrgUnit (OU) the VDIs will be joined to. 181 | This is a script sample to create a group policy object suitable for FSLogix profiles in AD. Make sure the variables (e.g. OU) and paths match yours. 182 | 183 | ```PowerShell 184 | $OUSuffix = "OU=AVDHosts,OU=HCI" #the part after the "...,DC=powerkurs,DC=local" so e.g. "OU=HostPool1,OU=AVD" 185 | 186 | 187 | #this will be our temp folder - need it for download / logging 188 | $tmpDir = "c:\temp\" 189 | 190 | #create folder if it doesn't exist 191 | if (!(Test-Path $tmpDir)) { mkdir $tmpDir -force } 192 | 193 | #downloading FSLogix. 194 | Write-Output "downloading fslogix" 195 | 196 | $tempPath = "$tmpDir\FSLogix" 197 | $destinationPath = "$tmpDir\FSLogix.zip" 198 | if (!(Test-Path $destinationPath)) { 199 | "downloading fslogix" 200 | Invoke-WebRequest -Uri "https://aka.ms/fslogix_download" -OutFile $destinationPath -verbose 201 | Expand-Archive $destinationPath -DestinationPath $tempPath -Force -verbose 202 | } 203 | 204 | $fqdn = (Get-WmiObject Win32_ComputerSystem).Domain 205 | $policyDestination = "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\" 206 | 207 | mkdir $policyDestination -Force 208 | mkdir "$policyDestination\en-us" -Force 209 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\*" -filter "*.admx" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions" -Force -Verbose 210 | Copy-Item "Microsoft.PowerShell.Core\FileSystem::$tempPath\*" -filter "*.adml" -Destination "Microsoft.PowerShell.Core\FileSystem::\\$fqdn\SYSVOL\$fqdn\policies\PolicyDefinitions\en-us" -Force -Verbose 211 | 212 | 213 | $gpoName = "FSLogixDefExcl{0}" -f [datetime]::Now.ToString('dd-MM-yy_HHmmss') 214 | New-GPO -Name $gpoName 215 | $FSLogixRegKeys = @{ 216 | Enabled = 217 | @{ 218 | Type = "DWord" 219 | Value = 1 #set to 1 to enable. 220 | } 221 | VHDLocations = 222 | @{ 223 | Type = "String" 224 | Value = "\\SOFS\Profile1;\\SOFS\Profile2" 225 | } 226 | DeleteLocalProfileWhenVHDShouldApply = 227 | @{ 228 | Type = "DWord" 229 | Value = 1 230 | } 231 | VolumeType = 232 | @{ 233 | Type = "String" 234 | Value = "VHDX" 235 | } 236 | SizeInMBs = 237 | @{ 238 | Type = "DWord" 239 | Value = 30000 240 | } 241 | IsDynamic = 242 | @{ 243 | Type = "DWord" 244 | Value = 1 245 | } 246 | PreventLoginWithFailure = 247 | @{ 248 | Type = "DWord" 249 | Value = 0 250 | } 251 | LockedRetryInterval = 252 | @{ 253 | Type = "DWord" 254 | Value = 10 255 | } 256 | LockedRetryCount = 257 | @{ 258 | Type = "DWord" 259 | Value = 5 260 | } 261 | FlipFlopProfileDirectoryName = 262 | @{ 263 | Type = "DWord" 264 | Value = 1 265 | } 266 | } 267 | 268 | foreach ($item in $FSLogixRegKeys.GetEnumerator()) { 269 | "{0}:{1}:{2}" -f $item.Name, $item.Value.Type, $item.Value.Value 270 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\SOFTWARE\fslogix\profiles" -ValueName $($item.Name) -Value $($item.Value.Value) -Type $($item.Value.Type) 271 | } 272 | 273 | #enable path exclusions in windows defender for fslogix profiles 274 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows Defender\Exclusions" -ValueName "Exclusions_Paths" -Value 1 -Type DWord 275 | 276 | $excludeList = @" 277 | %ProgramFiles%\FSLogix\Apps\frxdrv.sys, 278 | %ProgramFiles%\FSLogix\Apps\frxdrvvt.sys, 279 | %ProgramFiles%\FSLogix\Apps\frxccd.sys, 280 | %TEMP%\*.VHD, 281 | %TEMP%\*.VHDX, 282 | %Windir%\TEMP\*.VHD, 283 | %Windir%\TEMP\*.VHDX, 284 | \\SOFS\Profile1\*\*.VHD, 285 | \\SOFS\Profile1\*\*.VHDX, 286 | \\SOFS\Profile2\*\*.VHD, 287 | \\SOFS\Profile2\*\*.VHDX 288 | "@ 289 | foreach ($item in $($excludeList -split ',')) { 290 | $item.Trim() 291 | Set-GPRegistryValue -Name $gpoName -Key "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows Defender\Exclusions\Paths" -ValueName "$($item.Trim())" -Value 0 -Type String 292 | } 293 | 294 | Import-Module ActiveDirectory 295 | $DomainPath = $((Get-ADDomain).DistinguishedName) # e.g."DC=contoso,DC=azure" 296 | 297 | $OUPath = $($($OUSuffix + "," + $DomainPath).Split(',').trim() | where { $_ -ne "" }) -join ',' 298 | Write-Output "creating FSLOGIX GPO to OU: $OUPath" 299 | 300 | $existingGPOs = (Get-GPInheritance -Target $OUPath).GpoLinks | Where-Object DisplayName -Like "FSLogixDefExcl*" 301 | 302 | if ($existingGPOs -ne $null) { 303 | Write-Output "removing conflicting GPOs" 304 | $existingGPOs | Remove-GPLink -Verbose 305 | } 306 | 307 | 308 | New-GPLink -Name $gpoName -Target $OUPath -LinkEnabled Yes -verbose 309 | 310 | #cleanup existing but unlinked fslogix GPOs 311 | $existingGPOs | % { [xml]$Report = Get-GPO -guid $_.GpoId | Get-GPOReport -ReportType XML; if (!($Report.GPO.LinksTo)) { Remove-GPO -Guid $_.GpoId -Verbose } } 312 | 313 | 314 | ``` 315 | 316 | ## 10. Deploy the AVD Agents into the VDI VM 317 | Execute this PS script inside a VDI VM to make it part of a Hostpool. 318 | >Note: Your VDI VM needs to be domain joined before + must have outbound internet access (it will download stuff and register). You also need to provide a valid %RegistrationToken% 319 | 320 | ```PowerShell 321 | #region AVD agent download (do this in the VM) 322 | #https://docs.microsoft.com/en-us/azure/virtual-desktop/create-host-pools-powershell?tabs=azure-powershell#register-the-virtual-machines-to-the-azure-virtual-desktop-host-pool 323 | 324 | #this will be our temp folder - need it for download / logging 325 | $tmpDir = "c:\temp\" 326 | 327 | #create folder if it doesn't exist 328 | if (!(Test-Path $tmpDir)) { mkdir $tmpDir -force } 329 | 330 | $agentURIs = @{ 331 | 'agent.msi' = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrmXv" 332 | 'bootloader.msi' = "https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrxrH"} 333 | 334 | foreach ($uri in $agentURIs.GetEnumerator()) 335 | { 336 | Write-Output "starting download...." 337 | Invoke-WebRequest "$($uri.value)" -OutFile "$tmpDir\$($uri.key)" 338 | } 339 | 340 | $RegistrationToken = "eyJhbGciOiJ...." # a valid hostpool registration token [Azure portal] -> Azure Virtual Desktop -> Host Pool -> %Your Hostpool% -> Registration token 341 | #unattended install 342 | start-process -filepath msiexec -ArgumentList "/i ""$tmpDir\agent.msi"" /l*v ""$tmpDir\agent.msi.log"" REGISTRATIONTOKEN=$RegistrationToken /passive /qn /quiet /norestart " -Wait 343 | start-process -filepath msiexec -ArgumentList "/i ""$tmpDir\bootloader.msi"" /l*v ""$tmpDir\bootloader.msi.log"" /quiet /qn /norestart /passive" -Wait 344 | 345 | #endregion 346 | ``` 347 | After some minutes they should show up in the AVD hostpool: 348 | ![VDI VMs (on HCI) in AVD Hostpool](vdivmsinhostpool.png) 349 | 350 | ## 11. Arc-enable Your AVD Session Hosts 351 | Technically it is installing another agent (aka ***Azure connected machine agent***) into each desktop and connect it to Azure. Your desktop will show up in your Azure subscription. 352 | ![Arc enabled AVD Session Hosts](ArcEnabledVMs.png) 353 | 354 | **1. Why Should You Do This?:** 355 | Your AVDs may work without this but it is a best practice as: 356 | - ...it is also **required for Health checks** 357 | - ...it is the starting point for Azure-backed monitoring, protection, configuration,... -operations 358 | - e.g. You can run VM extensions and attach additional Azure services like Azure Policy, defender for cloud etc. to the VMs. 359 | - see what you can do...[Supported cloud operations](https://learn.microsoft.com/en-us/azure/azure-arc/servers/overview#supported-cloud-operations) 360 | 361 | **2. How Can You Do This?** 362 | There are a couple of [onboarding methods](https://learn.microsoft.com/en-us/azure/azure-arc/servers/deployment-options#onboarding-methods) available. 363 | In a **mass** (unattended,scripted) **deployment** you **probably create an Azure service principal** (an identity for apps with specific rights **in your Azure subscription** that works even if MFA is required) and **then** **run PowerShell inside each desktop** - e.g. like: 364 | ```PowerShell 365 | #### adjust the variables !!! #### 366 | $ServicePrincipalId = "8b42....7eee" 367 | $ServicePrincipalClientSecret ="GyG...KUI" 368 | $subScriptionId = "a2ba24....57e6f" 369 | $tenantId = "47f4......f65aab0" 370 | $resourceGroup = "rg-AVD-HPool1-WE" 371 | $location = "westeurope" 372 | $tags = "Datacenter=DC0815,City=SomeTown,StateOrDistrict=SomeDistrict,CountryOrRegion=SomeCountry,Purpose=AVD" 373 | 374 | 375 | try { 376 | $env:AUTH_TYPE = "principal"; 377 | $env:CLOUD = "AzureCloud"; 378 | # downloading the azure connected machine agent 379 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072; 380 | Invoke-WebRequest -UseBasicParsing -Uri "https://aka.ms/azcmagent-windows" -TimeoutSec 30 -OutFile "$env:TEMP\install_windows_azcmagent.ps1"; 381 | & "$env:TEMP\install_windows_azcmagent.ps1"; #installing 382 | if ($LASTEXITCODE -ne 0) { exit 1; } 383 | & "$env:ProgramW6432\AzureConnectedMachineAgent\azcmagent.exe" connect --service-principal-id "$ServicePrincipalId" --service-principal-secret "$ServicePrincipalClientSecret" --resource-group "$resourceGroup" --tenant-id "$tenantId" --location "$location" --subscription-id "$subScriptionId" --cloud "$env:CLOUD" --tags $tags ; #connecting to azure 384 | } 385 | catch { 386 | #report error in case something fails 387 | $logBody = @{subscriptionId="$subScriptionId";resourceGroup="$resourceGroup";tenantId="$tenantId";location="$location";authType="$env:AUTH_TYPE";operation="onboarding";messageType=$_.FullyQualifiedErrorId;message="$_";}; 388 | Invoke-WebRequest -UseBasicParsing -Uri "https://gbl.his.arc.azure.com/log" -Method "PUT" -Body ($logBody | ConvertTo-Json) | out-null; 389 | Write-Host -ForegroundColor red $_.Exception; 390 | } 391 | ``` 392 | 393 | ## 13. adding proxy... 394 | in order to make the RDagent and RD bootagent use proxies - you may need to run this in the session host: 395 | ``` 396 | bitsadmin /util /setieproxy LOCALSYSTEM Manual_Proxy proxy1:8080 null 397 | bitsadmin /util /setieproxy NETWORKSERVICE Manual_Proxy proxy1:8080 null 398 | ``` 399 | Whereas proxy1:8080 is to be replaced with your proxy and port. 400 | 401 | 402 | ## 14. Publish NotepadPlusPlus in your AVD Application Group 403 | When using the wizard in the Azure Portal notepad++ might produce an error: 404 | `"message": "The resource you are looking for has been removed, had its name changed, or is temporarily unavailable."` 405 | However with a slight name change and using an ARM template you can add NotepadPlusPlus to your AVD App Group. 406 | [![Deploy To Azure](deploytoazure.png)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FbfrankMS%2FAzStackHCI%2Fmain%2FAVD%2FNotePadPlusPlus.json) -------------------------------------------------------------------------------- /AVD/manually/vdivmsinhostpool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/AVD/manually/vdivmsinhostpool.png -------------------------------------------------------------------------------- /AVD/readme.md: -------------------------------------------------------------------------------- 1 | # Using AVD On Azure Stack HCI23H2 2 | 3 | ## Contents 4 | In this repo you will find 2 ways of deploying AVD on HCI: 5 | - intended way [How To Deploy AVD On AzStack HCI (using the Azure Portal) ](./intendedway/readme.md) 6 | - manual way [from onprem - bypassing the Azure Resource Bridge - for learning](./manually/readme.md) 7 | -------------------------------------------------------------------------------- /Learn.md: -------------------------------------------------------------------------------- 1 | ## New to AzStack HCI? Here is a learn path 2 | - ([Introduction to Azure Stack](https://docs.microsoft.com/en-us/learn/modules/intro-to-azure-stack/) ) 3 | - [Introduction to Azure Stack HCI core technologies](https://docs.microsoft.com/en-us/learn/modules/azure-stack-hci-technologies/) 4 | - [Plan and deploy Azure Stack HC](https://docs.microsoft.com/en-us/learn/modules/azure-stack-hci-plan-deploy/) 5 | - [Integrate Azure Arc and Azure Stack HCI](https://docs.microsoft.com/en-us/learn/modules/stack-hci-arc-integration/) 6 | - [Integrate Azure services with Azure Stack HCI](https://docs.microsoft.com/en-us/learn/modules/integrate-azure-services-with-azure-stack-hci/) 7 | - [Manage Azure Stack HCI virtual machine workloads](https://docs.microsoft.com/en-us/learn/modules/manage-azure-stack-hci-virtual-machine-workloads/) 8 | 9 | 10 | to test 11 | https://docs.microsoft.com/en-us/learn/modules/manage-azure-stack-hci-virtual-machine-workloads/ 12 | https://docs.microsoft.com/en-us/learn/modules/manage-azure-stack-hci-storage/ 13 | 14 | 15 | [Validate the network operational state by using Test-NetStack]https://github.com/microsoft/Test-NetStack) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to my code repository on AzStack HCI 2 | 3 | The artefacts here are meant to: 4 | - store my findings on automating installations of HCI, AKS, AVD,.... all on Azure Stack HCI - for later re-use. 5 | - may help others to grab ideas from when trying to automate installations. -------------------------------------------------------------------------------- /Setup/ARM/customStorageIps/azuredeploy.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 | "apiVersion": { 6 | "value": "2024-04-01" 7 | }, 8 | "deploymentMode": { 9 | "value": "Validate" 10 | }, 11 | "keyVaultName": { 12 | "value": "hci..." 13 | }, 14 | "softDeleteRetentionDays": { 15 | "value": 30 16 | }, 17 | "diagnosticStorageAccountName": { 18 | "value": "sa....." 19 | }, 20 | "logsRetentionInDays": { 21 | "value": 30 22 | }, 23 | "storageAccountType": { 24 | "value": "Standard_LRS" 25 | }, 26 | "clusterName": { 27 | "value": "hcinested" 28 | }, 29 | "location": { 30 | "value": "westeurope" 31 | }, 32 | "tenantId": { 33 | "value": "..." 34 | }, 35 | "witnessType": { 36 | "value": "Cloud" 37 | }, 38 | "clusterWitnessStorageAccountName": { 39 | "value": "sa....." 40 | }, 41 | "localAdminUserName": { 42 | "value": "asLocalAdmin" 43 | }, 44 | "localAdminPassword": { 45 | "value": "" 46 | }, 47 | "AzureStackLCMAdminUsername": { 48 | "value": "asLCMUser" 49 | }, 50 | "AzureStackLCMAdminPasssword": { 51 | "value": "" 52 | }, 53 | "arbDeploymentAppID": { 54 | "value": "..." 55 | }, 56 | "arbDeploymentAppSecret": { 57 | "value": "..." 58 | }, 59 | "arbDeploymentSPNObjectID": { 60 | "value": "..." 61 | }, 62 | "hciResourceProviderObjectID": { 63 | "value": "..." 64 | }, 65 | "arcNodeResourceIds": { 66 | "value": [ 67 | "/subscriptions/YOURSubscription/resourceGroups/YOURresourcegroup/providers/Microsoft.HybridCompute/machines/YOURnode1", 68 | "/subscriptions/YOURSubscription/resourceGroups/YOURresourcegroup/providers/Microsoft.HybridCompute/machines/YOURnode2" 69 | ] 70 | }, 71 | "domainFqdn": { 72 | "value": "hci00.org" 73 | }, 74 | "namingPrefix": { 75 | "value": "HCI" 76 | }, 77 | "adouPath": { 78 | "value": "OU=HCI,DC=HCI00,DC=org" 79 | }, 80 | "securityLevel": { 81 | "value": "Customized" 82 | }, 83 | "driftControlEnforced": { 84 | "value": false 85 | }, 86 | "credentialGuardEnforced": { 87 | "value": false 88 | }, 89 | "smbSigningEnforced": { 90 | "value": false 91 | }, 92 | "smbClusterEncryption": { 93 | "value": false 94 | }, 95 | "bitlockerBootVolume": { 96 | "value": false 97 | }, 98 | "bitlockerDataVolumes": { 99 | "value": false 100 | }, 101 | "wdacEnforced": { 102 | "value": false 103 | }, 104 | "streamingDataClient": { 105 | "value": true 106 | }, 107 | "euLocation": { 108 | "value": true 109 | }, 110 | "episodicDataUpload": { 111 | "value": true 112 | }, 113 | "configurationMode": { 114 | "value": "Express" 115 | }, 116 | "subnetMask": { 117 | "value": "255.255.255.0" 118 | }, 119 | "defaultGateway": { 120 | "value": "192.168.0.1" 121 | }, 122 | "startingIPAddress": { 123 | "value": "192.168.0.10" 124 | }, 125 | "endingIPAddress": { 126 | "value": "192.168.0.30" 127 | }, 128 | "dnsServers": { 129 | "value": [ 130 | "192.168.0.1" 131 | ] 132 | }, 133 | "useDhcp": { 134 | "value": false 135 | }, 136 | "physicalNodesSettings": { 137 | "value": [ 138 | { 139 | "name": "YOURnode1", 140 | "ipv4Address": "192.168.0.2" 141 | }, 142 | { 143 | "name": "YOURnode2", 144 | "ipv4Address": "192.168.0.3" 145 | } 146 | ] 147 | }, 148 | "networkingType": { 149 | "value": "switchlessMultiServerDeployment" 150 | }, 151 | "networkingPattern": { 152 | "value": "custom" 153 | }, 154 | "intentList": { 155 | "value": [ 156 | { 157 | "name": "mgmt", 158 | "trafficType": [ 159 | "Management" 160 | ], 161 | "adapter": [ 162 | "aMGMT" 163 | ], 164 | "overrideVirtualSwitchConfiguration": false, 165 | "virtualSwitchConfigurationOverrides": { 166 | "enableIov": "", 167 | "loadBalancingAlgorithm": "" 168 | }, 169 | "overrideQosPolicy": false, 170 | "qosPolicyOverrides": { 171 | "priorityValue8021Action_Cluster": "7", 172 | "priorityValue8021Action_SMB": "3", 173 | "bandwidthPercentage_SMB": "50" 174 | }, 175 | "overrideAdapterProperty": true, 176 | "adapterPropertyOverrides": { 177 | "jumboPacket": "1514", 178 | "networkDirect": "Disabled", 179 | "networkDirectTechnology": "" 180 | } 181 | }, 182 | { 183 | "name": "compute", 184 | "trafficType": [ 185 | "Compute" 186 | ], 187 | "adapter": [ 188 | "Comp1", 189 | "Comp2" 190 | ], 191 | "overrideVirtualSwitchConfiguration": false, 192 | "virtualSwitchConfigurationOverrides": { 193 | "enableIov": "", 194 | "loadBalancingAlgorithm": "" 195 | }, 196 | "overrideQosPolicy": false, 197 | "qosPolicyOverrides": { 198 | "priorityValue8021Action_Cluster": "7", 199 | "priorityValue8021Action_SMB": "3", 200 | "bandwidthPercentage_SMB": "50" 201 | }, 202 | "overrideAdapterProperty": true, 203 | "adapterPropertyOverrides": { 204 | "jumboPacket": "1514", 205 | "networkDirect": "Disabled", 206 | "networkDirectTechnology": "" 207 | } 208 | }, 209 | { 210 | "name": "smb", 211 | "trafficType": [ 212 | "Storage" 213 | ], 214 | "adapter": [ 215 | "SMB1", 216 | "SMB2" 217 | ], 218 | "overrideVirtualSwitchConfiguration": false, 219 | "virtualSwitchConfigurationOverrides": { 220 | "enableIov": "", 221 | "loadBalancingAlgorithm": "" 222 | }, 223 | "overrideQosPolicy": false, 224 | "qosPolicyOverrides": { 225 | "priorityValue8021Action_Cluster": "7", 226 | "priorityValue8021Action_SMB": "3", 227 | "bandwidthPercentage_SMB": "50" 228 | }, 229 | "overrideAdapterProperty": true, 230 | "adapterPropertyOverrides": { 231 | "jumboPacket": "1514", 232 | "networkDirect": "Disabled", 233 | "networkDirectTechnology": "" 234 | } 235 | } 236 | ] 237 | }, 238 | "storageNetworkList": { 239 | "value": [ 240 | { 241 | "name": "StorageNetwork1", 242 | "networkAdapterName": "SMB1", 243 | "vlanId": "711", 244 | "storageAdapterIPInfo": [ 245 | { 246 | "physicalNode": "YOURnode1", 247 | "ipv4Address": "10.0.1.10", 248 | "subnetMask": "255.255.255.0" 249 | }, 250 | { 251 | "physicalNode": "YOURnode2", 252 | "ipv4Address": "10.0.1.11", 253 | "subnetMask": "255.255.255.0" 254 | } 255 | ] 256 | }, 257 | { 258 | "name": "StorageNetwork2", 259 | "networkAdapterName": "SMB2", 260 | "vlanId": "712", 261 | "storageAdapterIPInfo": [ 262 | { 263 | "physicalNode": "YOURnode1", 264 | "ipv4Address": "10.0.2.10", 265 | "subnetMask": "255.255.255.0" 266 | }, 267 | { 268 | "physicalNode": "YOURnode2", 269 | "ipv4Address": "10.0.2.11", 270 | "subnetMask": "255.255.255.0" 271 | } 272 | ] 273 | } 274 | ] 275 | }, 276 | "storageConnectivitySwitchless": { 277 | "value": true 278 | }, 279 | "enableStorageAutoIp": { 280 | "value": false 281 | }, 282 | "customLocation": { 283 | "value": "hci00location" 284 | }, 285 | "sbeVersion": { 286 | "value": "" 287 | }, 288 | "sbeFamily": { 289 | "value": "" 290 | }, 291 | "sbePublisher": { 292 | "value": "" 293 | }, 294 | "sbeManifestSource": { 295 | "value": "" 296 | }, 297 | "sbeManifestCreationDate": { 298 | "value": "" 299 | }, 300 | "partnerProperties": { 301 | "value": [] 302 | }, 303 | "partnerCredentiallist": { 304 | "value": [] 305 | } 306 | } 307 | } -------------------------------------------------------------------------------- /Setup/best practices/PrepareNodeNics.ps1: -------------------------------------------------------------------------------- 1 | # Renames Network Adapters, disables IPv6 and disables unused adapters 2 | # Do this on every node. 3 | 4 | install-module psmenu #will provide you with a menu to chose 5 | 6 | $nicNames =@("COMP1","COMP2","SMB1","SMB2") #replace with your names should be the same on all nodes 7 | 8 | foreach ($nicName in $nicNames) 9 | { 10 | Write-Host -ForegroundColor Cyan "Which adapter should be renamed to >> $nicName << ?" 11 | $selectedNic = $null 12 | $selectedNic = show-Menu -MenuItems $(get-netadapter | sort-object MacAddress) -MenuItemFormatter { "{0}`t{1}`t{2}`t{3}" -f $args.MacAddress, $args.Name, $args.InterfaceDescription, $args.LinkSpeed} 13 | if ($null -ne $selectedNic) 14 | { 15 | Rename-NetAdapter -InputObject $selectedNic -NewName $nicName 16 | } 17 | "" 18 | } 19 | 20 | Write-Host -ForegroundColor Cyan "Which adapters should get unique names ?" 21 | $selectedNics = show-Menu -MenuItems $(get-netadapter | sort-object MacAddress) -MenuItemFormatter { "{0}`t{1}`t{2}" -f $args.MacAddress, $args.Name, $args.InterfaceDescription} -multiselect 22 | 23 | foreach ($nic in $selectedNics) 24 | { 25 | Rename-NetAdapter -InputObject $nic -NewName $("$($nic.Name)" + "_" + $env:COMPUTERNAME) 26 | } 27 | 28 | Write-Host -ForegroundColor Cyan "Which adapters should not be used (will be disabled) ?" 29 | $toDisableNics = show-Menu -MenuItems $(get-netadapter | sort-object MacAddress) -MenuItemFormatter { "{0}`t{1}`t{2}" -f $args.MacAddress, $args.Name, $args.InterfaceDescription} -multiselect 30 | 31 | foreach ($nic in $toDisableNics) 32 | { 33 | Disable-NetAdapter -InputObject $nic -Verbose 34 | } 35 | Write-Host -ForegroundColor Cyan "Disabling IPv6 on all adapters." 36 | Disable-NetAdapterBinding -InterfaceAlias * -ComponentID ms_tcpip6 -Verbose 37 | 38 | Write-Host -ForegroundColor Cyan "Select your management adapter. (to set IP, DNS, GW)" 39 | $mgmtNic = $null 40 | $mgmtNic = show-Menu -MenuItems $(get-netadapter | sort-object MacAddress) -MenuItemFormatter { "{0}`t{1}`t{2}`t{3}" -f $args.MacAddress, $args.Name, $args.InterfaceDescription, $args.LinkSpeed} 41 | 42 | do { 43 | $iPAddress = Read-Host -Prompt "Enter a valid IP address for the management adapter" 44 | } while (-not ($iPAddress -match "\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?")) 45 | 46 | do { 47 | $dnsIP = Read-Host -Prompt "Enter a valid DNS IP address" 48 | } while (-not ($dnsIP -match "\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?")) 49 | 50 | do { 51 | $prefixLength = Read-Host -Prompt "Enter the IP's subnet mask as prefix (e.g. 24 for 255.255.255.0)" 52 | } while (-not ($prefixLength -match [regex]'^[1-2][0-9]?$|^[3][0-2]?$|^[4-9]$')) 53 | 54 | do { 55 | $defaultGateway = Read-Host -Prompt "Enter a valid defaultGateway IP address" 56 | } while (-not ($defaultGateway -match "\d\d?\d?\.\d\d?\d?\.\d\d?\d?\.\d\d?\d?")) 57 | 58 | 59 | Set-NetIPInterface -InterfaceAlias $($mgmtNic.Name) -Dhcp Enabled -Verbose 60 | Start-Sleep 3 61 | Set-NetIPInterface -InterfaceAlias $($mgmtNic.Name) -Dhcp Disabled -Verbose 62 | New-NetIPAddress -InterfaceAlias $($mgmtNic.Name) -IPAddress $iPAddress -AddressFamily IPv4 -PrefixLength $prefixLength -Verbose -DefaultGateway $defaultGateway 63 | Set-DnsClientServerAddress -InterfaceAlias $($mgmtNic.Name) -ServerAddresses $dnsIP 64 | Disable-NetAdapterBinding -InterfaceAlias $($mgmtNic.Name) -ComponentID ms_tcpip6 #disable IPv6 65 | 66 | 67 | <# 68 | Write-Host -ForegroundColor Cyan "Disabling DHCP on all other adapters." 69 | $toDisableNics = show-Menu -MenuItems $(get-netadapter | sort-object MacAddress) -MenuItemFormatter { "{0}`t{1}`t{2}" -f $args.MacAddress, $args.Name, $args.InterfaceDescription} -multiselect 70 | foreach ($nic in $toDisableNics) 71 | { 72 | Disable-NetAdapter -InputObject $nic -Verbose 73 | } 74 | #> 75 | 76 | <# 77 | $SMB2NetAdapterName = "SMB2" 78 | Set-NetIPInterface -InterfaceAlias $SMB2NetAdapterName -Dhcp Enabled -Verbose 79 | Start-Sleep 3 80 | Set-NetIPInterface -InterfaceAlias $SMB2NetAdapterName -Dhcp Disabled -Verbose 81 | New-NetIPAddress -InterfaceAlias $SMB2NetAdapterName -IPAddress $($NodeInfo.$($env:COMPUTERNAME).SMB2IP) -AddressFamily IPv4 -PrefixLength $($NodeInfo.$($env:COMPUTERNAME).SMBMask) -Verbose 82 | Set-DnsClient -InterfaceAlias $SMB2NetAdapterName -RegisterThisConnectionsAddress $false 83 | Disable-NetAdapterBinding -InterfaceAlias $SMB2NetAdapterName -ComponentID ms_tcpip6 #disable IPv6 84 | #> -------------------------------------------------------------------------------- /Setup/best practices/Test-RDMA/RDMAcounter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/Setup/best practices/Test-RDMA/RDMAcounter.png -------------------------------------------------------------------------------- /Setup/best practices/Test-RDMA/Test-RDMA.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [Parameter(Mandatory=$True, Position=1, HelpMessage="Interface index of the adapter for which RDMA config is to be verified")] 4 | [string] $IfIndex, 5 | [Parameter(Mandatory=$True, Position=2, HelpMessage="True if underlying fabric type is RoCE. False for iWarp or IB")] 6 | [bool] $IsRoCE, 7 | [Parameter(Mandatory=$True, Position=3, HelpMessage="IP address of the remote RDMA adapter")] 8 | [string] $RemoteIpAddress, 9 | [Parameter(Mandatory=$False, Position=4, HelpMessage="Full path to the folder containing diskspd.exe")] 10 | [string] $PathToDiskspd, 11 | [Parameter(Mandatory=$False, Position=5, HelpMessage="Output level [none|verbose|debug]")] 12 | [string] $OutputLevel, 13 | [Parameter(Mandatory=$False, Position=6, HelpMessage="Interface ID of VF driver in Guest OS (mandatory for Guest RDMA tests only)")] 14 | [string] $VfIndex) 15 | 16 | if ($OutputLevel -eq "none") 17 | { 18 | $OutputLevel = 0 19 | } 20 | elseif ($OutputLevel -eq "debug" ) 21 | { 22 | $OutputLevel = 2 23 | } 24 | else 25 | { 26 | $OutputLevel = 1 # verbose 27 | } 28 | 29 | if ($RemoteIpAddress -ne $null) 30 | { 31 | if (($PathToDiskspd -eq $null) -Or ($PathToDiskspd -eq '')) 32 | { 33 | $PathToDiskspd = "C:\Windows\System32" 34 | } 35 | 36 | $FullPathToDiskspd = $PathToDiskspd + "\diskspd.exe" 37 | if ((Test-Path $FullPathToDiskspd) -eq $false) 38 | { 39 | Write-Host "ERROR: Diskspd.exe not found at" $FullPathToDiskspd ". Please download diskspd.exe and place it in the specified location. Exiting." -ForegroundColor Red 40 | return 41 | } 42 | elseif ($OutputLevel -gt 0) 43 | { 44 | Write-Host "VERBOSE: Diskspd.exe found at" $FullPathToDiskspd 45 | } 46 | } 47 | 48 | 49 | $rdmaAdapter = Get-NetAdapter -IfIndex $IfIndex 50 | 51 | if ($outputLevel -eq 2) 52 | { 53 | Write-Host "DEBUG: Name is " $rdmaAdapter.Name 54 | Write-Host "DEBUG: IfDesc is " $rdmaAdapter.InterfaceDescription 55 | } 56 | 57 | if ($rdmaAdapter -eq $null) 58 | { 59 | Write-Host "ERROR: The adapter with interface index $IfIndex not found" -ForegroundColor Red 60 | return 61 | } 62 | 63 | $rdmaAdapterName = $rdmaAdapter.Name 64 | 65 | if ($rdmaAdapter.InterfaceDescription -Match "Hyper-V Virtual Ethernet Adapter") 66 | { 67 | $rdmaAdapterType = "vNIC" 68 | } 69 | elseif ($rdmaAdapter.InterfaceDescription -Match "Microsoft Hyper-V Network Adapter") 70 | { 71 | $rdmaAdapterType = "vmNIC" 72 | if( $VfIndex -eq "" ) 73 | { 74 | Write-Host "ERROR: VF adapter Interface Index missing" -ForegroundColor Red 75 | return 76 | } 77 | $VFAdapter = Get-NetAdapter -IfIndex $VfIndex 78 | if ($outputLevel -eq 2) 79 | { 80 | Write-Host "DEBUG: VF Name is " $VFAdapter.Name 81 | Write-Host "DEBUG: IfDesc of VF is " $VFAdapter.InterfaceDescription 82 | } 83 | $VFrdmaCapabilities = Get-NetAdapterRdma -InterfaceDescription $VFAdapter.InterfaceDescription 84 | } 85 | else 86 | { 87 | $rdmaAdapterType = "pNIC" 88 | } 89 | if ($outputLevel -gt 0) 90 | { 91 | Write-Host "VERBOSE: The adapter " $rdmaAdapterName " is a " $rdmaAdapterType 92 | } 93 | 94 | $rdmaCapabilities = Get-NetAdapterRdma -InterfaceDescription $rdmaAdapter.InterfaceDescription 95 | 96 | if ($rdmaCapabilities -eq $null -or $rdmaCapabilities.Enabled -eq $false) 97 | { 98 | Write-Host "ERROR: The adapter " $rdmaAdapterName " is not enabled for RDMA" -ForegroundColor Red 99 | return 100 | } 101 | if ($rdmaAdapterType -eq "vmNIC" -and ( $VFrdmaCapabilities -eq $null -or $VFrdmaCapabilities.Enabled -eq $false )) 102 | { 103 | Write-Host "ERROR: The VF adapter " $VFAdapter.Name " is not enabled for RDMA" -ForegroundColor Red 104 | return 105 | } 106 | 107 | if ($rdmaCapabilities.MaxQueuePairCount -eq 0) 108 | { 109 | Write-Host "ERROR: RDMA capabilities for adapter $rdmaAdapterName are not valid : MaxQueuePairCount is 0" -ForegroundColor Red 110 | return 111 | } 112 | 113 | if ($rdmaCapabilities.MaxCompletionQueueCount -eq 0) 114 | { 115 | Write-Host "ERROR: RDMA capabilities for adapter $rdmaAdapterName are not valid : MaxCompletionQueueCount is 0" -ForegroundColor Red 116 | return 117 | } 118 | 119 | $smbClientNetworkInterfaces = Get-SmbClientNetworkInterface 120 | 121 | if ($smbClientNetworkInterfaces -eq $null) 122 | { 123 | Write-Host "ERROR: No network interfaces detected by SMB (Get-SmbClientNetworkInterface)" -ForegroundColor Red 124 | return 125 | } 126 | 127 | $rdmaAdapterSmbClientNetworkInterface = $null 128 | foreach ($smbClientNetworkInterface in $smbClientNetworkInterfaces) 129 | { 130 | if ($smbClientNetworkInterface.InterfaceIndex -eq $IfIndex) 131 | { 132 | $rdmaAdapterSmbClientNetworkInterface = $smbClientNetworkInterface 133 | } 134 | } 135 | 136 | if ($rdmaAdapterSmbClientNetworkInterface -eq $null) 137 | { 138 | Write-Host "ERROR: No network interfaces found by SMB for adapter $rdmaAdapterName (Get-SmbClientNetworkInterface)" -ForegroundColor Red 139 | return 140 | } 141 | 142 | if ($rdmaAdapterSmbClientNetworkInterface.RdmaCapable -eq $false) 143 | { 144 | Write-Host "ERROR: SMB did not detect adapter $rdmaAdapterName as RDMA capable. Make sure the adapter is bound to TCP/IP and not to other protocol like vmSwitch." -ForegroundColor Red 145 | return 146 | } 147 | 148 | $rdmaAdapters = $rdmaAdapter 149 | if ($RdmaAdapterType -eq "vNIC") 150 | { 151 | if ($OutputLevel -gt 0) 152 | { 153 | Write-Host "VERBOSE: Retrieving vSwitch bound to the virtual adapter" 154 | } 155 | $virtualAdapter = Get-VMNetworkAdapter -ManagementOS | where DeviceId -eq $rdmaAdapter.DeviceID 156 | $switchName = $virtualAdapter.switchName 157 | if ($OutputLevel -gt 0) 158 | { 159 | Write-Host "VERBOSE: Found vSwitch: $switchName" 160 | } 161 | $vSwitch = Get-VMSwitch -Name $switchName 162 | $rdmaAdapters = Get-NetAdapter -InterfaceDescription $vSwitch.NetAdapterInterfaceDescriptions 163 | if ($OutputLevel -gt 0) 164 | { 165 | $vSwitchAdapterMessage = "VERBOSE: Found the following physical adapter(s) bound to vSwitch: " 166 | $index = 1 167 | foreach ($qosAdapter in $rdmaAdapters) 168 | { 169 | $qosAdapterName = $qosAdapter.Name 170 | $vSwitchAdapterMessage = $vSwitchAdapterMessage + [string]$qosAdapterName 171 | if ($index -lt $rdmaAdapters.Length) 172 | { 173 | $vSwitchAdapterMessage = $vSwitchAdapterMessage + ", " 174 | } 175 | $index = $index + 1 176 | } 177 | Write-Host $vSwitchAdapterMessage 178 | } 179 | } 180 | 181 | 182 | if ($IsRoCE -eq $true -and $RdmaAdapterType -ne "vmNIC") 183 | { 184 | Write-Host "VERBOSE: Underlying adapter is RoCE. Checking if QoS/DCB/PFC is configured on each physical adapter(s)" 185 | foreach ($qosAdapter in $rdmaAdapters) 186 | { 187 | $qosAdapterName = $qosAdapter.Name 188 | $qos = Get-NetAdapterQos -Name $qosAdapterName 189 | if ($qos.Enabled -eq $false) 190 | { 191 | Write-Host "ERROR: QoS is not enabled for adapter $qosAdapterName" -ForegroundColor Red 192 | return 193 | } 194 | 195 | if ($qos.OperationalFlowControl -eq "All Priorities Disabled") 196 | { 197 | Write-Host "ERROR: Flow control is not enabled for adapter $qosAdapterName" -ForegroundColor Red 198 | return 199 | } 200 | } 201 | if ($OutputLevel -gt 0) 202 | { 203 | Write-Host "VERBOSE: QoS/DCB/PFC configuration is correct." 204 | } 205 | } 206 | 207 | if ($RdmaAdapterType -eq "vmNIC") 208 | { 209 | Write-Host "CAUTION: Guest Virtual NIC being tested, Guest can't check host adapter settings." -ForegroundColor Yellow 210 | } 211 | elseif ($OutputLevel -gt 0) 212 | { 213 | Write-Host "VERBOSE: RDMA configuration is correct." 214 | } 215 | 216 | if ($RemoteIpAddress -ne '') 217 | { 218 | if ($OutputLevel -eq 2) 219 | { 220 | Write-Host "DEBUG: Checking if remote IP address, $RemoteIpAddress, is reachable." 221 | } 222 | $canPing = Test-Connection $RemoteIpAddress -Quiet 223 | if ($canPing -eq $false) 224 | { 225 | Write-Host "ERROR: Cannot reach remote IP $RemoteIpAddress" -ForegroundColor Red 226 | return 227 | } 228 | elseif ($OutputLevel -gt 0) 229 | { 230 | Write-Host "VERBOSE: Remote IP $RemoteIpAddress is reachable." 231 | } 232 | } 233 | else 234 | { 235 | Write-Host "ERROR: Remote IP address was not provided." 236 | } 237 | 238 | if ($OutputLevel -gt 0) 239 | { 240 | Write-Host "VERBOSE: Disabling RDMA on adapters that are not part of this test. RDMA will be enabled on them later." 241 | } 242 | $adapters = Get-NetAdapterRdma 243 | $InstanceIds = $rdmaAdapters.InstanceID; 244 | 245 | $adaptersToEnableRdma = @() 246 | foreach ($adapter in $adapters) 247 | { 248 | if ($adapter.Enabled -eq $true) 249 | { 250 | if (($adapter.InstanceID -notin $InstanceIds) -and 251 | ($adapter.InstanceID -ne $rdmaAdapter.InstanceID) -and 252 | ($adapter.InstanceID -ne $VFAdapter.InstanceID)) 253 | { 254 | $adaptersToEnableRdma += $adapter 255 | Disable-NetAdapterRdma -Name $adapter.Name 256 | if ($OutputLevel -eq 2) 257 | { 258 | Write-Host "DEBUG: RDMA disabled on Adapter " $adapter.Name 259 | } 260 | } 261 | elseif ($OutputLevel -eq 2) 262 | { 263 | Write-Host "DEBUG: RDMA not disabled on Adapter " $adapter.Name 264 | } 265 | } 266 | } 267 | 268 | if ($OutputLevel -ne 0) 269 | { 270 | Write-Host "VERBOSE: Testing RDMA traffic. Traffic will be sent in a background job. Job details:" 271 | } 272 | 273 | # PseudoRandomize the target file name so two copies of this script can execute at the same time against the same destination 274 | $TargetFileName = "\\$RemoteIpAddress\C$\testfile$IfIndex.dat" 275 | if ($OutputLevel -eq 2) 276 | { 277 | Write-Host "DEBUG: TargetFileName is $TargetFileName" 278 | } 279 | 280 | $ScriptBlock = { 281 | param($RemoteIpAddress, $PathToDiskspd, $TargetFileName) 282 | cd $PathToDiskspd 283 | .\diskspd.exe -b4K -c10G -t4 -o16 -d100000 -L -Sr -d30 $TargetFileName 284 | } 285 | 286 | $thisJob = Start-Job $ScriptBlock -ArgumentList $RemoteIpAddress,$PathToDiskspd,$TargetFileName 287 | $RdmaTrafficDetected = $false 288 | # Check Perfmon counters while the job is running 289 | While ((Get-Job -id $($thisJob).Id).state -eq "Running") 290 | { 291 | $written = Get-Counter -Counter "\SMB Direct Connection(_Total)\Bytes RDMA Written/sec" -ErrorAction Ignore 292 | $sent = Get-Counter -Counter "\SMB Direct Connection(_Total)\Bytes Sent/sec" -ErrorAction Ignore 293 | if ($written -ne $null) 294 | { 295 | $RdmaWriteBytesPerSecond = [uint64]($written.Readings.split(":")[1]) 296 | if ($RdmaWriteBytesPerSecond -gt 0) 297 | { 298 | $RdmaTrafficDetected = $true 299 | } 300 | if ($OutputLevel -gt 0) 301 | { 302 | Write-Host "VERBOSE:" $RdmaWriteBytesPerSecond "RDMA bytes written per second" 303 | } 304 | } 305 | if ($sent -ne $null) 306 | { 307 | $RdmaWriteBytesPerSecond = [uint64]($sent.Readings.split(":")[1]) 308 | if ($RdmaWriteBytesPerSecond -gt 0) 309 | { 310 | $RdmaTrafficDetected = $true 311 | } 312 | if ($OutputLevel -gt 0) 313 | { 314 | Write-Host "VERBOSE:" $RdmaWriteBytesPerSecond "RDMA bytes sent per second" 315 | } 316 | } 317 | } 318 | 319 | del $TargetFileName 320 | 321 | if ($OutputLevel -gt 0) 322 | { 323 | Write-Host "VERBOSE: Enabling RDMA on adapters that are not part of this test. RDMA was disabled on them prior to sending RDMA traffic." 324 | } 325 | 326 | foreach ($adapter in $adaptersToEnableRdma) 327 | { 328 | Enable-NetAdapterRdma -Name $adapter.Name 329 | if ($OutputLevel -eq 2) 330 | { 331 | Write-Host "DEBUG: RDMA enabled on Adapter " $adapter.Name 332 | } 333 | } 334 | 335 | if ($RdmaTrafficDetected) 336 | { 337 | Write-Host "SUCCESS: RDMA traffic test SUCCESSFUL: RDMA traffic was sent to" $RemoteIpAddress -ForegroundColor Green 338 | } 339 | else 340 | { 341 | Write-Host "ERROR: RDMA traffic test FAILED: Please check " -ForegroundColor Yellow 342 | Write-Host "ERROR: a) physical switch port configuration for Priorty Flow Control." -ForegroundColor Yellow 343 | Write-Host "ERROR: b) job owner has write permission at " $RemoteIpAddress "\C$" -ForegroundColor Yellow 344 | } -------------------------------------------------------------------------------- /Setup/best practices/Test-RDMA/howto_test-rdma.md: -------------------------------------------------------------------------------- 1 | # How To Do Test-RDMA 2 | 3 | >Note: Before putting workload to your cluster make sure RDMA has been properly configured. Sometimes settings seem correct but no RDMA traffic is observed. This test can help. 4 | 5 | ## Why should I do this? "Rule of Thumb..." 6 | ...if you see a lot of packets in taskmanager on your SMB (RDMA enabled) adapters -> get suspicious - because when properly configured you should not see those. (You should observe them using RDMA counters in perfmon) 7 | 8 | ## How to do the test 9 | 1. Download [Test-Rdma.ps1](https://github.com/microsoft/SDN/blob/master/Diagnostics/Test-Rdma.ps1) 10 | 2. Download [diskspd](https://aka.ms/getdiskspd) onto your node e.g. with: 11 | ```PowerShell 12 | mkdir c:\temp 13 | start-bitstransfer "https://aka.ms/getdiskspd" "c:\temp\diskspd.zip" -Priority High -RetryInterval 60 -Verbose 14 | Expand-Archive c:\temp\diskspd.zip c:\temp\diskspd 15 | # Code for diskspd is here https://github.com/Microsoft/diskspd 16 | ``` 17 | 3. RDP into the node using a domain account with admin rights on all nodes. 18 | - open powershell 19 | - navigate to c:\temp 20 | - Do a `Get-NetAdapter` to get all the Nics ifIndex e.g. 21 | ``` 22 | PS C:\temp> get-netadapter 23 | 24 | Name InterfaceDescription ifIndex Status MacAddress LinkSpeed 25 | ---- -------------------- ------- ------ ---------- --------- 26 | pNIC02 Mellanox ConnectX-3 Ethernet Adapter #2 17 Up 00-02-C9-3B-9A-81 10 Gbps 27 | Ethernet Remote NDIS Compatible Device 14 Up 78-45-C4-F3-62-99 426.0 Mbps 28 | vEthernet (vMGMT) Hyper-V Virtual Ethernet Adapter 12 Up 00-15-5D-B1-A8-00 10 Gbps 29 | vEthernet (SMB1) Hyper-V Virtual Ethernet Adapter #2 10 Up 00-15-5D-B1-A8-01 10 Gbps 30 | NIC4 QLogic BCM57800 Gigabit Ethernet (ND... 9 Disconnected 90-B1-1C-29-59-FA 0 bps 31 | vEthernet (SMB2) Hyper-V Virtual Ethernet Adapter #3 8 Up 00-15-5D-B1-A8-02 10 Gbps 32 | pNIC01 Mellanox ConnectX-3 Ethernet Adapter 7 Up 00-02-C9-3B-9A-80 10 Gbps 33 | NIC3 QLogic BCM57800 Gigabit Ethernet (...#2 5 Disconnected 90-B1-1C-29-59-F8 0 bps 34 | NIC2 QLogic BCM57800 10 Gigabit Etherne...#2 4 Disconnected 90-B1-1C-29-59-F6 0 bps 35 | NIC1 QLogic BCM57800 10 Gigabit Ethernet ... 2 Disconnected 90-B1-1C-29-59-F4 0 bps 36 | ``` 37 | 4. Take the 1st storage adpater you want to test -> e.g. SMB1 -> interface index = 10 38 | - Launch test rdma like e.g.: 39 | ```PowerShell 40 | .\Test-RDMA.ps1 -IfIndex 10 -IsRoCE $true -RemoteIpAddress 192.168.10.184 -PathToDiskspd "c:\temp\diskspd\amd64" 41 | ``` 42 | Where remote IP address is the remote adapter's ip that can be reached via the local SMB1 adapter. 43 | (you can ping first if you are uncertain) 44 | 45 | The output should be something like: 46 | ``` 47 | VERBOSE: Diskspd.exe found at c:\temp\DiskSpd\amd64\diskspd.exe 48 | VERBOSE: The adapter vEthernet (SMB1) is a vNIC 49 | VERBOSE: Retrieving vSwitch bound to the virtual adapter 50 | VERBOSE: Found vSwitch: S2DSwitch 51 | VERBOSE: Found the following physical adapter(s) bound to vSwitch: pNIC02, pNIC01 52 | VERBOSE: Underlying adapter is RoCE. Checking if QoS/DCB/PFC is configured on each physical adapter(s) 53 | VERBOSE: QoS/DCB/PFC configuration is correct. 54 | VERBOSE: RDMA configuration is correct. 55 | VERBOSE: Remote IP 192.168.10.184 is reachable. 56 | VERBOSE: Disabling RDMA on adapters that are not part of this test. RDMA will be enabled on them later. 57 | VERBOSE: Testing RDMA traffic. Traffic will be sent in a background job. Job details: 58 | VERBOSE: 700599348 RDMA bytes written per second 59 | VERBOSE: 26466825 RDMA bytes sent per second 60 | VERBOSE: 711326594 RDMA bytes written per second 61 | VERBOSE: 26459854 RDMA bytes sent per second 62 | VERBOSE: 718014566 RDMA bytes written per second 63 | VERBOSE: 26408155 RDMA bytes sent per second 64 | VERBOSE: 713305774 RDMA bytes written per second 65 | ... 66 | VERBOSE: 26384721 RDMA bytes sent per second 67 | VERBOSE: 713992318 RDMA bytes written per second 68 | VERBOSE: 26796631 RDMA bytes sent per second 69 | VERBOSE: 428838206 RDMA bytes written per second 70 | VERBOSE: 0 RDMA bytes sent per second 71 | VERBOSE: Enabling RDMA on adapters that are not part of this test. RDMA was disabled on them prior to sending RDMA traffic. 72 | SUCCESS: RDMA traffic test SUCCESSFUL: RDMA traffic was sent to 192.168.10.184 73 | ``` 74 | Keyword is **SUCCESSFUL** ;-) 75 | 76 | **Whilst doing this you might want to look at the perfmon counters for RDMA of the node.**: 77 | On your admin box: start **perfmon** -> Server: *your node* -> add counter category 'RDMA Activity' -> choose e.g. 78 | "RDMA Inbound Bytes/sec", "RDMA Outbound Bytes/sec",... 79 | ![perfmon RDMA](RDMAcounter.png) 80 | 81 | 5. Repeat the tests for other RDMA adapters -------------------------------------------------------------------------------- /Setup/best practices/readme.md: -------------------------------------------------------------------------------- 1 | # AzStack HCI - bfrank's compilation of best practices found - Checklist Draft 0.1 2 | 3 | > Warning: This is a compilation of stuff found on the internet + personal habits. It is not complete and might contain errors or superfluous information. No warranties. Use for brainstorming and feel free to contribute. 4 | 5 | ## Example Vendor Deployment Guides 6 | The contain a lot HW specific but also generic settings and best practices. Worth checking - even if your HW is different. 7 | - [HPE: Implementing Azure Stack HCI OS using HPE ProLiant servers technical white paper](https://www.hpe.com/psnow/doc/a50004375enw) 8 | - [Lenovo: S2D Deployment Guide](https://lenovopress.lenovo.com/lp0064-microsoft-storage-spaces-direct-s2d-deployment-guide) 9 | - [Dell: S2D Deployment Guide](https://downloads.dell.com/solutions/general-solution-resources/White%20Papers/DellEMCMicrosoft_StorageSpacesDirect_ReadyNode_PowerEdgeR740xdR640-Scalable-DG.pdf) 10 | 11 | ## Physical Switch: 12 | With AzStack HCI we have 3 traffic classes: **Management** (e.g. Cluster internal or to DC), **Compute** (VMs network talk), **Storage** (SMB direct (RDMA) | S2D traffic) 13 | - Make sure the switch you choose does work for connected traffic class! [Network switches for Azure Stack HCI](https://learn.microsoft.com/en-us/azure-stack/hci/concepts/physical-network-requirements?tabs=22H2%2C20-21H2reqs) 14 | - (**Storage**) RDMA switchports set higher MTU size 15 | - (**Storage**) Do PFC / DCB settings as per vendor deployment guide see e.g. [HPE], [Lenovo] 16 | 17 | [HPE]: https://www.hpe.com/psnow/doc/a50004375enw 18 | [Lenovo]: https://lenovopress.lenovo.com/lp0064-microsoft-storage-spaces-direct-s2d-deployment-guide 19 | 20 | ## Hardware 21 | - **Rule of thumb**: **Buy an integrated system or at least validated nodes (it's tested, certified)** [Azure Stack HCI Solutions] 22 | (This does not mean that you could not build a working HCI - you may save some bucks on HW but you will invest (substancial) time(==money) learning ;-) ) 23 | - You need a Host Bus Adapter (HBA) - not a RAID controller for your SSDs, HDDs (HDDs? Can do - but I wouldn't). (nowadays seen controllers that can do both: RAID for OS & HBA for S2D) -> make sure your's is supported. -> ask vendor and check [Storage Spaces Direct hardware requirements](https://learn.microsoft.com/en-us/windows-server/storage/storage-spaces/storage-spaces-direct-hardware-requirements) 24 | - Never use consumer grade SSDs - just [Don't do it] - performance will '*su.k*' (SATA is ok but it has to be DC ready i.e. you **require 'Power-Loss Protection'** ) 25 | - How many disks you need to buy? (or *perf + resiliency impacts storage efficiency*) **Rule of thumb: Have your savvy vendor help you with right sizing** + Do the plausibility check: [Storage Spaces Direct Calculator (preview)](https://aka.ms/s2dcalc) 26 | - >Beware: That vendors use Terrabyte to express a device capacity - however many OSes show e.g. Tebibyte (1TB = 0.91TiB) - beware with what your are calculating - not to run short! 27 | - Choose only [NICs that have the required certifications](https://learn.microsoft.com/en-us/azure-stack/hci/concepts/host-network-requirements) [Windows Server Catalog] 28 | 29 | [Azure Stack HCI Solutions]: https://hcicatalog.azurewebsites.net/#/catalog 30 | [Don't do it]: https://techcommunity.microsoft.com/t5/storage-at-microsoft/don-t-do-it-consumer-grade-solid-state-drives-ssd-in-storage/ba-p/425914 31 | [Windows Server Catalog]: https://www.windowsservercatalog.com/ 32 | 33 | ## BIOS: 34 | **Rule of thumb: follow your vendors 'S2D | HCI deployment guide'.** 35 | These probably include settings similar to: 36 | - Boot Mode - UEFI 37 | - Enable Virtualization Mode 38 | - Enable SR-IOV 39 | - Due to EU regulations your system might be set to run power- and perf- capped: Check and change if required: e.g. System profile | CPU Mode to High or "Virtualization Max Performance" -> refer to your vendor's deployment guide + [Perf tuning for low latency] 40 | - Enable secure boot. 41 | - When having multiple physical NICs + multiple CPUs: You might want to use (fast) PCIe lanes that are served by different CPUs (to split load) 42 | 43 | [Perf tuning for low latency]: https://learn.microsoft.com/en-us/windows-server/networking/technologies/network-subsystem/net-sub-performance-tuning-nics?source=recommendations#bkmk_low 44 | 45 | 46 | ## OS: 47 | Do **not** update! This will be part of the installation. 48 | ### Network 49 | In 23H2 most of the networking settings will be done for you (some based on your input). We have 3 networks to care about: 50 | 1. **Management** - e.g. cluster communication, host to domain controller traffic, internet access, DNS,... 51 | 2. **Compute** - for the VMs to talk to the outside world. 52 | 3. **Storage** - for S2D i.e. the network traffic to provide storage redundancy (iwarp, RoCE, RoCEv2) 53 | 54 | For these networks and the adapters there are some best practices to consider: 55 | - Before configure host networking make sure the **firmware** & **drivers** for all NICs are update. Use your vendors supported way. 56 | - (**Management, Compute, Storage**) Jumbo frames are the default. Make sure these are working with the switches that adapters are attached to. 57 | - Configure just one network adapter to talk to the internet. With DNS, DG, single IP. 58 | - Remove IPv6 from all network adapters when not configured. 59 | - Rename network adapters consistently accross all cluster nodes for their purpose: e.g. 60 | ```PowerShell 61 | Rename-NetAdapter -InterfaceDescription 'Intel(R) Ethernet Network Adapter E810-XXV-2' -NewName "Storage1" 62 | ``` 63 | - (**Storage**) When switchless Make sure adapters are labeled and cross cabled correctly. You may consider looking at the nics MAC address to map interface description with physical port at the back of the server. e.g. 64 | ```cmd 65 | PS C:\Users\Administrator.MYAVD\Documents> get-netadapter 66 | 67 | Name InterfaceDescription ifIndex Status MacAddress LinkSpeed 68 | ---- -------------------- ------- ------ ---------- --------- 69 | COMP2 Intel(R) Ethernet Connection X722 fo... 15 Up 04-7B-CB-8A-45-FD 10 Gbps 70 | SMB1 Intel(R) Ethernet Network Adapter E8... 14 Up B4-96-91-BA-22-54 25 Gbps 71 | NIC1_HCIMX1 Intel(R) I350 Gigabit Network Connec... 13 Not Present 08-3A-88-FA-C5-CE 0 bps 72 | SMB2 Intel(R) Ethernet Network Adapter ...#2 11 Up B4-96-91-BA-22-55 25 Gbps 73 | COMP1 Intel(R) Ethernet Connection X722 ...#2 9 Up 04-7B-CB-8A-45-FC 10 Gbps 74 | Ethernet IBM USB Remote NDIS Network Device 6 Not Present 06-7B-CB-8A-46-02 0 bps 75 | NIC2_HCIMX1 Intel(R) I350 Gigabit Network Conn...#2 4 Not Present 08-3A-88-FA-C5-CF 0 bps 76 | ``` 77 | - Disable DHCP, DNS registration, and IPv6 on all other adapters e.g. 78 | ```PowerShell 79 | $SMB2NetAdapterName = "Storage2" 80 | Set-NetIPInterface -InterfaceAlias $SMB2NetAdapterName -Dhcp Enabled -Verbose 81 | Start-Sleep 3 82 | Set-NetIPInterface -InterfaceAlias $SMB2NetAdapterName -Dhcp Disabled -Verbose 83 | Set-DnsClient -InterfaceAlias $SMB2NetAdapterName -RegisterThisConnectionsAddress $false 84 | Disable-NetAdapterBinding -InterfaceAlias $SMB2NetAdapterName -ComponentID ms_tcpip6 #disable IPv6 85 | ``` 86 | - Rename adapters that should not be used with a unique value - e.g. the host's name and disable them: 87 | ```PowerShell 88 | $nic = get-netadapter -InterfaceDescription 'Intel(R) I350 Gigabit Network Connection' 89 | Rename-NetAdapter -InputObject $nic -NewName $("$($nic.Name)" + "_" + $env:COMPUTERNAME) 90 | Disable-NetAdapter -InputObject $nic -Verbose 91 | ``` 92 | ``` 93 | NIC1_HCIMX1 Intel(R) I350 Gigabit Network Connec... 13 Not Present 08-3A-88-FA-C5-CE 0 bps 94 | ``` 95 | 96 | - (**Storage**) When this network is switched be aware that Qos Policies and RDMA will be used (required for [RoCE](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn583822(v=ws.11)) - recommended for [iWARP](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn583825(v=ws.11)) ) 97 | --->make sure switches are configured to support it! Consult your vendor's deployment guide. 98 | - (**Storage**) Do a [Test-RDMA](./Test-RDMA/howto_test-rdma.md) + [Test-RDMA.ps1](https://github.com/microsoft/SDN/blob/master/Diagnostics/Test-Rdma.ps1) before going into production. 99 | 100 | 101 | ### Storage 102 | - Consider [Reduced networking performance after you enable SMB Encryption or SMB Signing](https://learn.microsoft.com/en-us/troubleshoot/windows-server/networking/reduced-performance-after-smb-encryption-signing?source=recommendations) 103 | - Run a [VMFleet 2.0](https://techcommunity.microsoft.com/t5/azure-stack-blog/vmfleet-2-0-quick-start-guide/ba-p/2824778) test to do a performance baseline of your storage before putting workload on. 104 | 105 | 106 | ## Cluster: 107 | - VMs should not talk on the management network. 108 | - Have additional networks for cluster to cluster communication (i.e. Heartbeat. Not just one) 109 | - Configure cluster witness (will be done for you) 110 | - Create at least one CSV per node. ( CSVFS_ReFS as filesystem) 111 | - Make sure your CSVs uses the proper resiliency & performance option for your workload. [Plan volumes] 112 | - Remove the host management network from live migration - or de-priotize the host management network for live migration - e.g.: 113 | ```powershell 114 | $clusterResourceType = Get-ClusterResourceType -Name 'Virtual Machine' 115 | $hostNetworkID = Get-ClusterNetwork | Where-Object { $_.Address -eq '172.16.102.0' } | Select-Object -ExpandProperty ID 116 | $otherNetworkID = (Get-ClusterNetwork).Where({$_.ID -ne $hostnetworkID}).ID 117 | $newMigrationOrder = ($otherNetworkID + $hostNetworkID) -join ';' 118 | Set-ClusterParameter -InputObject $clusterResourceType -Name MigrationNetworkOrder -Value $newMigrationOrder 119 | ``` 120 | [Plan volumes]: https://learn.microsoft.com/en-us/azure-stack/hci/concepts/plan-volumes#with-four-or-more-servers 121 | 122 | ## Advanced & Experimental 123 | 124 | ### Networking 125 | - Have a look at [Performance tuning for low-latency packet processing](https://learn.microsoft.com/en-us/windows-server/networking/technologies/network-subsystem/net-sub-performance-tuning-nics?source=recommendations#bkmk_low) and the remainder of the article and consider tuning. 126 | - (**Storage** network) Test if reducing | disabling interrupt moderation reduces latencies: [Interrupt Moderation (IM)](https://learn.microsoft.com/en-us/windows-server/networking/technologies/hpn/hpn-hardware-only-features#interrupt-moderation-im) 127 | - Check your RSS and VMQ settings and consider tuning of those: 128 | - basically check that RSS is enabled, your Nics are NUMA node aligned (i.e. use the CPU that serving the NICs PCIe bus), use jumbo frames, VMMQ is enabled, and your vNics (especially host vnics e.g. vSMB1, vREPL,...) are affinitized to the right pNIC. 129 | https://learn.microsoft.com/en-us/windows-hardware/drivers/network/introduction-to-receive-side-scaling 130 | https://www.darrylvanderpeijl.com/windows-server-2016-networking-optimizing-network-settings/ 131 | https://www.broadcom.com/support/knowledgebase/1211161326328/rss-and-vmq-tuning-on-windows-servers 132 | https://learn.microsoft.com/en-us/windows-hardware/drivers/network/vmmq-send-and-receive-processing 133 | 134 | 135 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /Setup/proxy/23H2ProxyDeployment_12-07-2024.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/Setup/proxy/23H2ProxyDeployment_12-07-2024.xlsx -------------------------------------------------------------------------------- /Setup/proxy/ArcRegisterWithProxy.ps1: -------------------------------------------------------------------------------- 1 | Set-Variable -Name 'ConfirmPreference' -Value 'None' -Scope Global 2 | Write-Output "Installing PackageManagement" 3 | Install-Package -Name PackageManagement -MinimumVersion 1.4.8 -Force -Confirm:$false -Source PSGallery 4 | Write-Output "Installing PowershellGet" 5 | Install-Package -Name PowershellGet -Force -Verbose 6 | 7 | Register-PSRepository -Default -InstallationPolicy Trusted 8 | 9 | #Install required PowerShell modules in your node for registration 10 | Install-Module Az.Accounts -RequiredVersion 2.13.2 11 | Install-Module Az.Resources -RequiredVersion 6.12.0 12 | Install-Module Az.ConnectedMachine -RequiredVersion 0.5.2 13 | 14 | #Install Arc registration script from PSGallery 15 | Install-Module AzSHCI.ARCInstaller # -RequiredVersion 0.2.2616.70 ## only when using the nested deployment 16 | 17 | $verbosePreference = "Continue" 18 | $subscription = "a2ba.........7e6f" 19 | $tenantID = "47f4........5aab0" 20 | $rg = "rg-mynested" # an existing an configured RG. 21 | $region = "westeurope" #or eastus??? 22 | 23 | Connect-AzAccount -TenantId $tenantID -Subscription $subscription -UseDeviceAuthentication 24 | $armAccessToken = (Get-AzAccessToken).Token 25 | $id = (Get-AzContext).Account.Id 26 | Start-Sleep -Seconds 3 27 | Invoke-AzStackHciArcInitialization -subscription $subscription -ResourceGroup $rg -TenantID $tenantID -Region $region -Cloud 'AzureCloud' -ArmAccessToken $armAccessToken -AccountID $id -Verbose -Proxy 'http://192.168.1.254:3128' 28 | -------------------------------------------------------------------------------- /Setup/proxy/Screenshot 2024-07-30 152348.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/Setup/proxy/Screenshot 2024-07-30 152348.png -------------------------------------------------------------------------------- /Setup/proxy/Screenshot 2024-07-30 152444.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/Setup/proxy/Screenshot 2024-07-30 152444.png -------------------------------------------------------------------------------- /Setup/proxy/Screenshot 2024-07-30 152522.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/Setup/proxy/Screenshot 2024-07-30 152522.png -------------------------------------------------------------------------------- /Setup/proxy/SetProxy.ps1: -------------------------------------------------------------------------------- 1 | # https://learn.microsoft.com/en-us/azure-stack/hci/manage/configure-proxy-settings-23h2 2 | $proxyServer = "192.168.1.254:3128" #e.g. proxy.contoso.com:8080 3 | 4 | $BypassList = "localhost,127.0.0.1,*.svc,01-HCI-1,01-HCI-2,hci01nested,192.168.1.2,192.168.1.3,*.HCI01.org,192.168.1.11" # 192.168.1.* can use * for IP range of the HCI cluster. 5 | # ip each node, netbios node & cluster, ARB IP 6 | # IP address of each cluster member server. 7 | # Netbios name of each server. 8 | # Netbios cluster name. 9 | # *.contoso.com. 10 | # Second IP address of the infrastructure pool. (ARB IP) -> 192.168.1.11 (e.g. when specifying 192.168.1.10 - 192.168.1.30) -> .10 (= cluster), .11 (= ARB IP) 11 | 12 | #region Proxy settings 13 | #WinInet 14 | Set-WinInetProxy -ProxySettingsPerUser 0 -ProxyServer $proxyServer -ProxyBypass $BypassList # use '*' for domains and whole subnets 15 | 16 | #Environment variables 17 | [Environment]::SetEnvironmentVariable("HTTPS_PROXY", "http://$proxyServer", "Machine") #must be http! (no 's' !!!) 18 | $env:HTTPS_PROXY = [System.Environment]::GetEnvironmentVariable("HTTPS_PROXY", "Machine") 19 | 20 | [Environment]::SetEnvironmentVariable("HTTP_PROXY", "http://$proxyServer", "Machine") 21 | $env:HTTP_PROXY = [System.Environment]::GetEnvironmentVariable("HTTP_PROXY", "Machine") 22 | 23 | $no_proxy_bypassList = "localhost,127.0.0.1,.svc,192.168.1.0/24,.HCI01.org,01-HCI-1,01-HCI-2,hci01nested" # no * for domains and use CIDR for subnets 24 | [Environment]::SetEnvironmentVariable("NO_PROXY", $no_proxy_bypassList, "Machine") 25 | $env:NO_PROXY = [System.Environment]::GetEnvironmentVariable("NO_PROXY", "Machine") 26 | 27 | #WinHTTP 28 | netsh winhttp set proxy $proxyServer bypass-list=$BypassList # use '*' for domains and whole subnets 29 | #endregion 30 | 31 | #Test WebRequest 32 | Invoke-WebRequest -Uri www.microsoft.com -UseBasicParsing 33 | 34 | #Make sure you have a time server. 35 | # Set Time Server in your geographical region 36 | #w32tm /config /manualpeerlist:de.pool.ntp.org /syncfromflags:manual /reliable:yes /update 37 | #w32tm /resync /force 38 | #w32tm /query /status 39 | 40 | #test if time server responds. 41 | #w32tm /stripchart /computer:de.pool.ntp.org /samples:5 /dataonly 42 | 43 | -------------------------------------------------------------------------------- /Setup/proxy/proxy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfrankMS/AzureLocal_AzStackHCI/e5c1a52b121b5d70b5a0ce081997e073cfbd7042/Setup/proxy/proxy.png -------------------------------------------------------------------------------- /Setup/proxy/readme.md: -------------------------------------------------------------------------------- 1 | # Installing Azure Stack HCI 23h2 with a Proxy 2 | 3 | ## This is how the environment looks like 4 | ![My HCI Demo Sandbox with a Squid Proxy](proxy.png) 5 | 6 | ## Watch it done on YT 7 | [![Installing Azure Stack HCI 23h2 with a Proxy](https://img.youtube.com/vi/qRnh5-jelXI/0.jpg)](https://youtu.be/qRnh5-jelXI) 8 | 9 | ## Artefacts in this code repo 10 | - [my Squid proxy log, filtered URL list + deployment steps and timestamps for reference with URLs, DNS, FQDNs being used.](23H2ProxyDeployment_12-07-2024.xlsx) 11 | 12 | | deployment steps | squid log | filtered FQDNs | 13 | |--|--|--| 14 | |![deployment steps](Screenshot%202024-07-30%20152348.png) | ![squid log](Screenshot%202024-07-30%20152444.png)| ![filtered FQDNs, DNS](Screenshot%202024-07-30%20152522.png)| 15 | 16 | 17 | - [SetProxy.ps1 - PScript sample to do proxy settings on each HCI node prio deployment](SetProxy.ps1) 18 | - [ArcRegisterWithProxy.ps1 - PScript sample to register each HCI node in Azure via a proxy](ArcRegisterWithProxy.ps1) 19 | 20 | ## Additional Resources 21 | - [Configure proxy settings for Azure Stack HCI, version 23H2](https://learn.microsoft.com/en-us/azure-stack/hci/manage/configure-proxy-settings-23h2) 22 | - [Required firewall URLs for Azure Stack HCI 23H2 deployments](https://learn.microsoft.com/en-us/azure-stack/hci/concepts/firewall-requirements#required-firewall-urls-for-azure-stack-hci-23h2-deployments) 23 | - [About Azure Stack HCI, version 23H2 deployment](https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-introduction) 24 | - [Used this (modified) for creating my demo environment](https://github.com/bfrankMS/CreateHypervVms/tree/master/Scenario-AzStackHCI) 25 | -------------------------------------------------------------------------------- /TS/DumpHCIInfo.ps1: -------------------------------------------------------------------------------- 1 | if (!(Test-Path c:\temp)){mkdir c:\temp} 2 | Start-Transcript c:\temp\DumpHCIInfo.log 3 | 4 | "======Nics========" 5 | Get-NetAdapter 6 | "------------------" 7 | Get-NetAdapterHardwareInfo 8 | "------------------" 9 | Get-VMNetworkAdapter -ManagementOS | ft Name,IsManagementOs,SwitchName, MacAddress,Status,IPAddresses,*band* 10 | "------------------" 11 | Get-VMNetworkAdapter * | ft name,vmname,*band* 12 | 13 | "======Switch========" 14 | Get-VMSwitch 15 | Get-VMSwitch | fl * 16 | "------------------" 17 | Get-VMSwitchTeam 18 | "------------------" 19 | Get-VMNetworkAdapterTeamMapping -Name "*" -ManagementOS 20 | 21 | "======RDMA & QoS========" 22 | Get-NetAdapterRdma 23 | Get-NetAdapterRdma -Name "*" | Where-Object -FilterScript { $_.Enabled } | fl * 24 | "------------------" 25 | Get-NetAdapterQos 26 | "------------------" 27 | Get-NetQosTrafficClass -Cimsession (Get-ClusterNode).Name | Select PSComputerName, Name, Priority, Bandwidth 28 | Get-NetQosFlowControl 29 | Get-NetQosPolicy 30 | 31 | "======Hyper-V Host========" 32 | Get-VMHost | fl * 33 | 34 | "======Network ATC========" 35 | Get-NetIntent | Format-Table IntentName, Scope,IntentType,NetAdapterNamesAsList, StorageVLANs,ManagementVLAN 36 | Get-NetIntentStatus | Format-Table IntentName, Host, ProvisioningStatus, ConfigurationStatus 37 | 38 | Get-NetIntentStatus -Globaloverrides 39 | 40 | $intents = Get-NetIntent 41 | $intents | %{$_ | select-object -Property IntentName, AdapterAdvancedParametersOverride,RssConfigOverride,QosPolicyOverride,SwitchConfigOverride,IPOverride |convertto-json} 42 | 43 | "======Nics advanced========" 44 | Get-SmbBandwidthLimit 45 | "------------------" 46 | Get-NetAdapterSriov -Name "*" | ft Name,enabled, SwitchName,SriovSupport,NumVFs -AutoSize 47 | "------------------" 48 | Get-NetAdapterAdvancedProperty 49 | "------------------" 50 | Get-NetAdapterRss 51 | "------------------" 52 | Get-NetAdapterStatistics -Name "*" 53 | Get-NetAdapterStatistics -Name "*" | Format-List -Property "*" 54 | "------------------" 55 | 56 | "======Get-ActionPlanInstances========" 57 | Get-ActionPlanInstances | ft ActionPlanName,status, InstanceID 58 | 59 | Get-ActionPlanInstances | where status -ne "Completed" | select -First 5 60 | 61 | "==============" 62 | 63 | "======Eventlogs========" 64 | $HCILogs = @("system","application","AKSHCI","azshciarc","AzStackHciEnvironmentChecker","Microsoft-Windows-Health-Hci/Operational","Microsoft-AzureStack-HCI/Admin","Microsoft-AzureStack-HCI-AttestationService/Admin","Microsoft-Windows-Networking-NetworkAtc/Operational", "Microsoft-Windows-Networking-NetworkAtc/Admin") 65 | 66 | foreach ($log in $HCILogs) 67 | { 68 | Get-WinEvent -LogName "$log" -MaxEvents 10 69 | Get-WinEvent -LogName "$log" -MaxEvents 10 | Select-Object -property @{name='TimeCreated'; expression={$_.TimeCreated.ToString("yyyy-MM-dd_HH:mm:ss")}},MachineName,LevelDisplayName,Message | Sort-Object TimeWritten -Descending | convertto-json 70 | } 71 | 72 | "==============" 73 | 74 | "======ClusterLog========" 75 | Get-ClusterLog -TimeSpan 5 -Destination 'c:\temp\' 76 | "==============" 77 | 78 | 79 | 80 | Stop-Transcript 81 | 82 | 83 | -------------------------------------------------------------------------------- /TS/DumpHCIInfo2.ps1: -------------------------------------------------------------------------------- 1 | if (!(Test-Path c:\temp)) { mkdir c:\temp } 2 | 3 | $header = @" 4 | host = $env:COMPUTERNAME 5 | Time = $(Get-Date -Format "yyyy-MM-dd_HH:mm:ss") 6 | whoami = $(whoami) 7 | "@ 8 | 9 | $header | Out-File -FilePath "c:\temp\DumpHCIInfo.log" 10 | 11 | $commands = @( 12 | @{ 13 | Name = "Get-NetAdapter"; Command = { Get-NetAdapter } 14 | }, 15 | @{ 16 | Name = "Get-NetAdapterHardwareInfo"; Command = { Get-NetAdapterHardwareInfo } 17 | }, 18 | @{ 19 | Name = "Get-VMNetworkAdapter -ManagementOS"; Command = { Get-VMNetworkAdapter -ManagementOS | Format-Table Name, IsManagementOs, SwitchName, MacAddress, Status, IPAddresses, *band* } 20 | }, 21 | @{ 22 | Name = "Get-VMNetworkAdapter *"; Command = { Get-VMNetworkAdapter * | Format-Table name, vmname, *band* } 23 | }, 24 | @{ 25 | Name = "Get-VMSwitch"; Command = { Get-VMSwitch } 26 | }, 27 | @{ 28 | Name = "Get-VMSwitch | Format-List *"; Command = { Get-VMSwitch | Format-List * } 29 | }, 30 | @{ 31 | Name = "Get-VMSwitchTeam"; Command = { Get-VMSwitchTeam } 32 | }, 33 | @{ 34 | Name = "Get-VMNetworkAdapterTeamMapping -Name -ManagementOS"; Command = { Get-VMNetworkAdapterTeamMapping -Name "*" -ManagementOS } 35 | }, 36 | @{ 37 | Name = "Get-NetAdapterRdma"; Command = { Get-NetAdapterRdma } 38 | }, 39 | @{ 40 | Name = "Get-NetAdapterRdma -Name | Where-Object -FilterScript { $_.Enabled } | Format-List *"; Command = { Get-NetAdapterRdma -Name "*" | Where-Object -FilterScript { $_.Enabled } | Format-List * } 41 | }, 42 | @{ 43 | Name = "Get-NetAdapterQos"; Command = { Get-NetAdapterQos } 44 | }, 45 | @{ 46 | Name = "Get-NetQosTrafficClass -Cimsession (Get-ClusterNode).Name | Select-Object PSComputerName, Name, Priority, Bandwidth"; Command = { Get-NetQosTrafficClass -Cimsession (Get-ClusterNode).Name | Select-Object PSComputerName, Name, Priority, Bandwidth } 47 | }, 48 | @{ 49 | Name = "Get-NetQosFlowControl"; Command = { Get-NetQosFlowControl } 50 | }, 51 | @{ 52 | Name = "Get-NetQosPolicy"; Command = { Get-NetQosPolicy } 53 | }, 54 | @{ 55 | Name = "Get-VMHost | Format-List *"; Command = { Get-VMHost | Format-List * } 56 | }, 57 | @{ 58 | Name = "Get-NetIntent | Format-Table IntentName, Scope, IntentType, NetAdapterNamesAsList, StorageVLANs, ManagementVLAN"; Command = { Get-NetIntent | Format-Table IntentName, Scope, IntentType, NetAdapterNamesAsList, StorageVLANs, ManagementVLAN } 59 | }, 60 | @{ 61 | Name = "Get-NetIntentStatus | Format-Table IntentName, Host, ProvisioningStatus, ConfigurationStatus"; Command = { Get-NetIntentStatus | Format-Table IntentName, Host, ProvisioningStatus, ConfigurationStatus } 62 | }, 63 | @{ 64 | Name = "Get-NetIntentStatus -Globaloverrides"; Command = { Get-NetIntentStatus -Globaloverrides } 65 | }, 66 | @{ 67 | Name = "Get-SmbBandwidthLimit"; Command = { Get-SmbBandwidthLimit } 68 | }, 69 | @{ 70 | Name = "Get-NetAdapterSriov -Name | Format-Table Name, enabled, SwitchName, SriovSupport, NumVFs -AutoSize"; Command = { Get-NetAdapterSriov -Name "*" | Format-Table Name, enabled, SwitchName, SriovSupport, NumVFs -AutoSize } 71 | }, 72 | @{ 73 | Name = "Get-NetAdapterAdvancedProperty"; Command = { Get-NetAdapterAdvancedProperty } 74 | }, 75 | @{ 76 | Name = "Get-NetAdapterRss"; Command = { Get-NetAdapterRss } 77 | }, 78 | @{ 79 | Name = "Get-NetAdapterStatistics -Name"; Command = { Get-NetAdapterStatistics -Name "*" } 80 | }, 81 | @{ 82 | Name = "Get-NetAdapterStatistics -Name | Format-List -Property "; Command = { Get-NetAdapterStatistics -Name "*" | Format-List -Property "*" } 83 | }, 84 | @{ 85 | Name = "Get-EventLog -list"; Command = { $HCILogs = @("system", "application", "AKSHCI", "azshciarc", "AzStackHciEnvironmentChecker", "Microsoft-Windows-Health-Hci/Operational", "Microsoft-AzureStack-HCI/Admin", "Microsoft-AzureStack-HCI-AttestationService/Admin", "Microsoft-Windows-Networking-NetworkAtc/Operational", "Microsoft-Windows-Networking-NetworkAtc/Admin") 86 | 87 | foreach ($log in $HCILogs) { 88 | Get-WinEvent -LogName "$log" -MaxEvents 10 89 | Get-WinEvent -LogName "$log" -MaxEvents 10 | Select-Object -Property @{name = 'TimeCreated'; expression = { $_.TimeCreated.ToString("yyyy-MM-dd_HH:mm:ss") } }, MachineName, LevelDisplayName, Message | Sort-Object TimeWritten -Descending | ConvertTo-Json 90 | } 91 | } 92 | }, 93 | @{ 94 | Name = "Get-ClusterLog "; Command = { Get-ClusterLog -TimeSpan 5 -Destination 'c:\temp\' } 95 | } 96 | ) 97 | 98 | foreach ($command in $commands) { 99 | <# $currentItemName is the current item #> 100 | "---> {0}" -f $command.Name 101 | "---> {0}" -f $command.Name | Out-File -FilePath "c:\temp\DumpHCIInfo.log" -Append 102 | Invoke-Command -ScriptBlock $($command.Command) | Out-File -FilePath "c:\temp\DumpHCIInfo.log" -Append 103 | } 104 | 105 | 106 | -------------------------------------------------------------------------------- /TS/GetNetworkATCEvents.ps1: -------------------------------------------------------------------------------- 1 | if (!(Test-Path c:\temp)){mkdir c:\temp} 2 | Start-Transcript c:\temp\NetworkATCEvents.log 3 | 4 | Get-WinEvent -LogName "Microsoft-Windows-Networking-NetworkAtc/Operational" -MaxEvents 10 5 | 6 | Get-WinEvent -LogName "Microsoft-Windows-Networking-NetworkAtc/Operational" -MaxEvents 10 | where message -like "*Error*" | Select-Object -property @{name='TimeCreated'; expression={$_.TimeCreated.ToString("yyyy-MM-dd_HH:mm:ss")}},MachineName,LevelDisplayName,Message | Sort-Object TimeWritten -Descending | convertto-json 7 | 8 | Get-WinEvent -LogName "Microsoft-Windows-Networking-NetworkAtc/Admin" -MaxEvents 10 9 | 10 | Get-WinEvent -LogName "Microsoft-Windows-Networking-NetworkAtc/Admin" -MaxEvents 10 | Select-Object -property @{name='TimeCreated'; expression={$_.TimeCreated.ToString("yyyy-MM-dd_HH:mm:ss")}},MachineName,LevelDisplayName,Message | Sort-Object TimeWritten -Descending | convertto-json 11 | 12 | Stop-Transcript -------------------------------------------------------------------------------- /TS/readme.md: -------------------------------------------------------------------------------- 1 | ## Enable RDP after deployment 2 | RDP is disabled for security reasons - PS remoting should work. Remote into a node using e.g. *etsn node1* and do a: 3 | ```PowerShell 4 | Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Control\Terminal Server' -Name "fDenyTSConnections" -Value 0 5 | Enable-NetFirewallRule -DisplayGroup "Remote Desktop" 6 | ``` --------------------------------------------------------------------------------