diff --git a/readme.md b/readme.md index e394201b9..7359e0136 100644 --- a/readme.md +++ b/readme.md @@ -31,7 +31,7 @@ As of today, we have a first reference implementation scenario that is one of th | Deployment Type | Link | |:--|:--| -| Azure portal UI |[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Farm%2Fdeploy-baseline.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Fportal-ui%2Fportal-ui-baseline.json) [![Deploy to Azure Gov](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/?feature.deployapiver=2022-12-01#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Farm%2Fdeploy-baseline.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Fportal-ui%2Fportal-ui-baseline.json) [![Deploy to Azure China](https://aka.ms/deploytoazurechinabutton)](https://portal.azure.cn/?feature.deployapiver=2022-12-01#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Farm%2Fdeploy-baseline.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Fportal-ui%2Fportal-ui-baseline.json)| +| Azure portal UI |[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Farm%2Fdeploy-baseline.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Fportal-ui%2Fportal-ui-baseline.json) [![Deploy to Azure Gov](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/?feature.deployapiver=2022-12-01#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Farm%2Fdeploy-baseline.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Fportal-ui%2Fportal-ui-baseline.json) [![Deploy to Azure China](https://aka.ms/deploytoazurechinabutton)](https://portal.azure.cn/?feature.deployapiver=2022-12-01#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Farm%2Fdeploy-baseline.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Fportal-ui%2Fportal-ui-baseline.json)| | Command line (Bicep/ARM) | [![Powershell/Azure CLI](./workload/docs/icons/powershell.png)](./workload/bicep/readme.md#avd-accelerator-baseline) | | Terraform | [![Terraform](./workload/docs/icons/terraform.png)](./workload/terraform/greenfield/readme.md) | @@ -78,7 +78,7 @@ Custom image is patched with the latest Windows updates. | Deployment Type | Link | |:--|:--| -| Azure portal UI | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Farm%2Fdeploy-custom-image.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Fportal-ui%2Fportal-ui-custom-image.json) [![Deploy to Azure Gov](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/?feature.deployapiver=2022-12-01#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Farm%2Fdeploy-custom-image.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fmain%2Fworkload%2Fportal-ui%2Fportal-ui-custom-image.json) | +| Azure portal UI | [![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Farm%2Fdeploy-custom-image.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Fportal-ui%2Fportal-ui-custom-image.json) [![Deploy to Azure Gov](https://aka.ms/deploytoazuregovbutton)](https://portal.azure.us/?feature.deployapiver=2022-12-01#blade/Microsoft_Azure_CreateUIDef/CustomDeploymentBlade/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Farm%2Fdeploy-custom-image.json/uiFormDefinitionUri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure%2Favdaccelerator%2Fanf-fslogix%2Fworkload%2Fportal-ui%2Fportal-ui-custom-image.json) | | Command line (Bicep/ARM) | [![Powershell/Azure CLI](./workload/docs/icons/powershell.png)](./workload/bicep/readme.md#optional-custom-image-build-deployment) | | Terraform | [![Terraform](./workload/docs/icons/terraform.png)](./workload/terraform/customimage) | diff --git a/workload/bicep/deploy-baseline.bicep b/workload/bicep/deploy-baseline.bicep index 919e0c4e3..08acb585a 100644 --- a/workload/bicep/deploy-baseline.bicep +++ b/workload/bicep/deploy-baseline.bicep @@ -144,7 +144,10 @@ param existingVnetAvdSubnetResourceId string = '' @sys.description('Existing virtual network subnet for private endpoints. (Default: "")') param existingVnetPrivateEndpointSubnetResourceId string = '' -@sys.description('Existing hub virtual network for perring. (Default: "")') +@sys.description('Existing virtual network subnet for ANF. (Default: "")') +param existingVnetAnfSubnetResourceId string = '' + +@sys.description('Existing hub virtual network for peering. (Default: "")') param existingHubVnetResourceId string = '' @sys.description('AVD virtual network address prefixes. (Default: 10.10.0.0/16)') @@ -156,6 +159,9 @@ param vNetworkAvdSubnetAddressPrefix string = '10.10.1.0/24' @sys.description('private endpoints virtual network subnet address prefix. (Default: 10.10.2.0/27)') param vNetworkPrivateEndpointSubnetAddressPrefix string = '10.10.2.0/27' +@sys.description('ANF virtual network subnet address prefix. (Default: 10.10.3.0/26)') +param vNetworkAnfSubnetAddressPrefix string = '10.10.3.0/26' + @sys.description('custom DNS servers IPs. (Default: "")') param customDnsIps string = '' @@ -187,16 +193,23 @@ param avdVnetPrivateDnsZoneKeyvaultId string = '' param vNetworkGatewayOnHub bool = false @sys.description('Deploy Fslogix setup. (Default: true)') -param createAvdFslogixDeployment bool = true +param createFslogixDeployment bool = true + +@allowed([ + 'ANF' // Azure NetApp Files + 'AzureFiles' // Storage account +]) +@sys.description('Type of storage service to deploy for FSLogix. (Default: AzureFiles)') +param storageService string = 'AzureFiles' @sys.description('Deploy App Attach setup. (Default: false)') param createAppAttachDeployment bool = false -@sys.description('Fslogix file share size. (Default: 1)') -param fslogixFileShareQuotaSize int = 1 +@sys.description('Fslogix file share size in GB. (Default: 100)') +param fslogixFileShareQuotaSize int = 100 -@sys.description('App Attach file share size. (Default: 1)') -param appAttachFileShareQuotaSize int = 1 +@sys.description('App Attach file share size in GB. (Default: 100)') +param appAttachFileShareQuotaSize int = 100 @sys.description('Deploy new session hosts. (Default: true)') param avdDeploySessionHosts bool = true @@ -227,13 +240,16 @@ param avdDeploySessionHostsCount int = 1 @sys.description('The session host number to begin with for the deployment. This is important when adding virtual machines to ensure the names do not conflict. (Default: 1)') param avdSessionHostCountIndex int = 1 -@sys.description('When true VMs are distributed across availability zones, when set to false, VMs will be deployed at regional level.') +@sys.description('When set to AvailabilityZones, VMs are distributed across availability zones, when set to None, VMs are deployed at regional level.') @allowed([ 'None' 'AvailabilityZones' ]) param availability string = 'None' +@sys.description('When true, Zone Redundant Storage (ZRS) is used, when set to false, Locally Redundant Storage (LRS) is used. (Default: false)') +param zoneRedundantStorage bool = false + @sys.description('The Availability Zones to use for the session hosts.') @allowed([ '1' @@ -242,9 +258,6 @@ param availability string = 'None' ]) param availabilityZones array = ['1', '2', '3'] -@sys.description('When true, Zone Redundant Storage (ZRS) is used, when set to false, Locally Redundant Storage (LRS) is used. (Default: false)') -param zoneRedundantStorage bool = false - // @sys.description('Deploys a VMSS Flex group and associates session hosts with it for availability purposes. (Default: true)') // param deployVmssFlex bool = true @@ -254,15 +267,17 @@ param zoneRedundantStorage bool = false @allowed([ 'Standard' 'Premium' + 'Ultra' ]) -@sys.description('Storage account SKU for FSLogix storage. Recommended tier is Premium (Default: Premium)') +@sys.description('SKU for FSLogix storage. Recommended tier is Premium for storage account and Standard for ANF. (Default: Premium)') param fslogixStoragePerformance string = 'Premium' @allowed([ 'Standard' 'Premium' + 'Ultra' ]) -@sys.description('Storage account SKU for App Attach storage. Recommended tier is Premium. (Default: Premium)') +@sys.description('SKU for App Attach storage. RRecommended tier is Premium for storage account and Standard for ANF. (Default: Premium)') param appAttachStoragePerformance string = 'Premium' @sys.description('Enables a zero trust configuration on the session host disks. (Default: false)') @@ -311,7 +326,7 @@ param avdCustomImageDefinitionId string = '' @sys.description('Management VM image SKU (Default: winServer_2022_Datacenter_smalldisk_g2)') param managementVmOsImage string = 'winServer_2022_Datacenter_smalldisk_g2' -@sys.description('OU name for Azure Storage Account. It is recommended to create a new AD Organizational Unit (OU) in AD and disable password expiration policy on computer accounts or service logon accounts accordingly. (Default: "")') +@sys.description('OU name for Azure Storage Account or Azure Netapp Files. It is recommended to create a new AD Organizational Unit (OU) in AD and disable password expiration policy on computer accounts or service logon accounts accordingly. (Default: "")') param storageOuPath string = '' // Custom Naming @@ -355,6 +370,10 @@ param avdVnetworkSubnetCustomName string = 'snet-avd-app1-dev-use2-001' @sys.description('private endpoints virtual network subnet custom name. (Default: snet-pe-app1-dev-use2-001)') param privateEndpointVnetworkSubnetCustomName string = 'snet-pe-app1-dev-use2-001' +@maxLength(80) +@sys.description('ANF virtual network subnet custom name. (Default: snet-anf-app1-dev-use2-001)') +param anfVnetworkSubnetCustomName string = 'snet-anf-app1-dev-use2-001' + @maxLength(80) @sys.description('AVD network security group custom name. (Default: nsg-avd-app1-dev-use2-001)') param avdNetworksecurityGroupCustomName string = 'nsg-avd-app1-dev-use2-001' @@ -363,6 +382,10 @@ param avdNetworksecurityGroupCustomName string = 'nsg-avd-app1-dev-use2-001' @sys.description('Private endpoint network security group custom name. (Default: nsg-pe-app1-dev-use2-001)') param privateEndpointNetworksecurityGroupCustomName string = 'nsg-pe-app1-dev-use2-001' +@maxLength(80) +@sys.description('ANF network security group custom name. (Default: nsg-anf-app1-dev-use2-001)') +param anfNetworksecurityGroupCustomName string = 'nsg-anf-app1-dev-use2-001' + @maxLength(80) @sys.description('AVD route table custom name. (Default: route-avd-app1-dev-use2-001)') param avdRouteTableCustomName string = 'route-avd-app1-dev-use2-001' @@ -415,6 +438,10 @@ param avdSessionHostCustomNamePrefix string = 'vmapp1duse2' @sys.description('AVD FSLogix and App Attach storage account prefix custom name. (Default: st)') param storageAccountPrefixCustomName string = 'st' +//@maxLength(10) +@sys.description('AVD FSLogix and App Attach storage account prefix custom name. (Default: anf-acc-app1-dev-use2-001)') +param anfAccountCustomName string = 'anf-acc-app1-dev-use2-001' + @sys.description('FSLogix file share name. (Default: fslogix-pc-app1-dev-001)') param fslogixFileShareCustomName string = 'fslogix-pc-app1-dev-use2-001' @@ -543,10 +570,9 @@ param enableDefForArm bool = true var varDeploymentPrefixLowercase = toLower(deploymentPrefix) var varAzureCloudName = environment().name var varDeploymentEnvironmentLowercase = toLower(deploymentEnvironment) -var varDeploymentEnvironmentComputeStorage = (deploymentEnvironment == 'Dev') +var varDeploymentEnvironmentOneCharacter = (deploymentEnvironment == 'Dev') ? 'd' : ((deploymentEnvironment == 'Test') ? 't' : ((deploymentEnvironment == 'Prod') ? 'p' : '')) -var varNamingUniqueStringThreeChar = take('${uniqueString(avdWorkloadSubsId, varDeploymentPrefixLowercase, time)}', 3) var varNamingUniqueStringTwoChar = take('${uniqueString(avdWorkloadSubsId, varDeploymentPrefixLowercase, time)}', 2) var varSessionHostLocationAcronym = varLocations[varSessionHostLocationLowercase].acronym var varManagementPlaneLocationAcronym = varLocations[varManagementPlaneLocationLowercase].acronym @@ -586,12 +612,18 @@ var varVnetAvdSubnetName = avdUseCustomNaming var varVnetPrivateEndpointSubnetName = avdUseCustomNaming ? privateEndpointVnetworkSubnetCustomName : 'snet-pe-${varComputeStorageResourcesNamingStandard}-001' +var varVnetAnfSubnetName = avdUseCustomNaming + ? anfVnetworkSubnetCustomName + : 'snet-anf-${varComputeStorageResourcesNamingStandard}-001' var varAvdNetworksecurityGroupName = avdUseCustomNaming ? avdNetworksecurityGroupCustomName : 'nsg-avd-${varComputeStorageResourcesNamingStandard}-001' var varPrivateEndpointNetworksecurityGroupName = avdUseCustomNaming ? privateEndpointNetworksecurityGroupCustomName : 'nsg-pe-${varComputeStorageResourcesNamingStandard}-001' +var varAnfNetworksecurityGroupName = avdUseCustomNaming + ? anfNetworksecurityGroupCustomName + : 'nsg-anf-${varComputeStorageResourcesNamingStandard}-001' var varAvdRouteTableName = avdUseCustomNaming ? avdRouteTableCustomName : 'route-avd-${varComputeStorageResourcesNamingStandard}-001' @@ -637,26 +669,9 @@ var varWrklKeyVaultSku = (varAzureCloudName == 'AzureCloud' || varAzureCloudName : (varAzureCloudName == 'AzureChinaCloud' ? 'standard' : null) var varSessionHostNamePrefix = avdUseCustomNaming ? avdSessionHostCustomNamePrefix - : 'vm${varDeploymentPrefixLowercase}${varDeploymentEnvironmentComputeStorage}${varSessionHostLocationAcronym}' + : 'vm${varDeploymentPrefixLowercase}${varDeploymentEnvironmentOneCharacter}${varSessionHostLocationAcronym}' //var varVmssFlexNamePrefix = avdUseCustomNaming ? '${vmssFlexCustomNamePrefix}-${varComputeStorageResourcesNamingStandard}' : 'vmss-${varComputeStorageResourcesNamingStandard}' var varStorageManagedIdentityName = 'id-storage-${varComputeStorageResourcesNamingStandard}-001' -var varFslogixFileShareName = avdUseCustomNaming - ? fslogixFileShareCustomName - : 'fslogix-pc-${varDeploymentPrefixLowercase}-${varDeploymentEnvironmentLowercase}-${varSessionHostLocationAcronym}-001' -var varAppAttachFileShareName = avdUseCustomNaming - ? appAttachFileShareCustomName - : 'appa-${varDeploymentPrefixLowercase}-${varDeploymentEnvironmentLowercase}-${varSessionHostLocationAcronym}-001' -var varFslogixStorageName = avdUseCustomNaming - ? '${storageAccountPrefixCustomName}fsl${varDeploymentPrefixLowercase}${varDeploymentEnvironmentComputeStorage}${varNamingUniqueStringThreeChar}' - : 'stfsl${varDeploymentPrefixLowercase}${varDeploymentEnvironmentComputeStorage}${varNamingUniqueStringThreeChar}' -var varFslogixStorageFqdn = createAvdFslogixDeployment - ? '${varFslogixStorageName}.file.${environment().suffixes.storage}' - : '' -var varAppAttachStorageFqdn = '${varAppAttachStorageName}.file.${environment().suffixes.storage}' -var varAppAttachStorageName = avdUseCustomNaming - ? '${storageAccountPrefixCustomName}appa${varDeploymentPrefixLowercase}${varDeploymentEnvironmentComputeStorage}${varNamingUniqueStringThreeChar}' - : 'stappa${varDeploymentPrefixLowercase}${varDeploymentEnvironmentComputeStorage}${varNamingUniqueStringThreeChar}' -var varManagementVmName = 'vmmgmt${varDeploymentPrefixLowercase}${varDeploymentEnvironmentComputeStorage}${varSessionHostLocationAcronym}' var varAlaWorkspaceName = avdUseCustomNaming ? avdAlaWorkspaceCustomName : 'log-avd-${varDeploymentEnvironmentLowercase}-${varManagementPlaneLocationAcronym}' @@ -665,30 +680,9 @@ var varZtKvName = avdUseCustomNaming ? '${ztKvPrefixCustomName}-${varComputeStorageResourcesNamingStandard}-${varNamingUniqueStringTwoChar}' : 'kv-key-${varComputeStorageResourcesNamingStandard}-${varNamingUniqueStringTwoChar}' // max length limit 24 characters var varZtKvPrivateEndpointName = 'pe-${varZtKvName}-vault' -// -var varFslogixSharePath = createAvdFslogixDeployment - ? '\\\\${varFslogixStorageName}.file.${environment().suffixes.storage}\\${varFslogixFileShareName}' - : '' -var varBaseScriptUri = 'https://raw.githubusercontent.com/azure/avdaccelerator/main/workload/' +var varBaseScriptUri = 'https://raw.githubusercontent.com/azure/avdaccelerator/anf-fslogix/workload/' var varSessionHostConfigurationScriptUri = '${varBaseScriptUri}scripts/Set-SessionHostConfiguration.ps1' var varSessionHostConfigurationScript = 'Set-SessionHostConfiguration.ps1' -var varCreateStorageDeployment = (createAvdFslogixDeployment || varCreateAppAttachDeployment == true) ? true : false -var varFslogixStorageSku = zoneRedundantStorage - ? '${fslogixStoragePerformance}_ZRS' - : '${fslogixStoragePerformance}_LRS' -var varAppAttachStorageSku = zoneRedundantStorage - ? '${appAttachStoragePerformance}_ZRS' - : '${appAttachStoragePerformance}_LRS' -var varMgmtVmSpecs = { - osImage: varMarketPlaceGalleryWindows[managementVmOsImage] - osDiskType: 'Standard_LRS' - mgmtVmSize: avdSessionHostsSize //'Standard_D2ads_v5' - enableAcceleratedNetworking: false - ouPath: avdOuPath - subnetId: createAvdVnet - ? '${networking.outputs.virtualNetworkResourceId}/subnets/${varVnetAvdSubnetName}' - : existingVnetAvdSubnetResourceId -} var varMaxSessionHostsPerTemplate = 10 var varMaxSessionHostsDivisionValue = avdDeploySessionHostsCount / varMaxSessionHostsPerTemplate var varMaxSessionHostsDivisionRemainderValue = avdDeploySessionHostsCount % varMaxSessionHostsPerTemplate @@ -952,19 +946,11 @@ var varPooledScalingPlanSchedules = [ } } ] -var varMarketPlaceGalleryWindows = loadJsonContent('../variables/osMarketPlaceImages.json') -var varStorageAzureFilesDscAgentPackageLocation = 'https://github.com/Azure/avdaccelerator/raw/main/workload/scripts/DSCStorageScripts/1.0.3/DSCStorageScripts.zip' -var varStorageToDomainScriptUri = '${varBaseScriptUri}scripts/Manual-DSC-Storage-Scripts.ps1' -var varStorageToDomainScript = './Manual-DSC-Storage-Scripts.ps1' -var varOuStgPath = !empty(storageOuPath) ? '"${storageOuPath}"' : '"${varDefaultStorageOuPath}"' -var varDefaultStorageOuPath = (avdIdentityServiceProvider == 'EntraDS') ? 'AADDC Computers' : 'Computers' -var varStorageCustomOuPath = !empty(storageOuPath) ? 'true' : 'false' var varAllDnsServers = '${customDnsIps},168.63.129.16' var varDnsServers = empty(customDnsIps) ? [] : (split(varAllDnsServers, ',')) var varCreateVnetPeering = !empty(existingHubVnetResourceId) ? true : false // Resource tagging // Tag Exclude-${varAvdScalingPlanName} is used by scaling plans to exclude session hosts from scaling. Exmaple: Exclude-vdscal-eus2-app1-dev-001 - var varTagsWithValues = union( empty(workloadNameTag) ? {} : { WorkloadName: workloadNameTag }, empty(workloadTypeTag) ? {} : { WorkloadType: workloadTypeTag }, @@ -1021,10 +1007,10 @@ var varResourceGroups = [ : union(varAvdDefaultTags, varAllComputeStorageTags) } ] - // security Principals (you can add support for more than one because it is an array. Future) var varSecurityPrincipalId = !empty(avdSecurityGroups) ? avdSecurityGroups[0].objectId : '' var varSecurityPrincipalName = !empty(avdSecurityGroups) ? avdSecurityGroups[0].displayName : '' +var varCreateStorageDeployment = (createFslogixDeployment || createAppAttachDeployment) ? true : false // =========== // // Deployments // @@ -1095,8 +1081,12 @@ module monitoringDiagnosticSettings './modules/avdInsightsMonitoring/deploy.bice computeObjectsRgName: varComputeObjectsRgName serviceObjectsRgName: varServiceObjectsRgName dataCollectionRulesName: varDataCollectionRulesName - storageObjectsRgName: (createAvdFslogixDeployment || createAppAttachDeployment) ? varStorageObjectsRgName : '' - networkObjectsRgName: (createAvdVnet) ? varNetworkObjectsRgName : '' + storageObjectsRgName: varCreateStorageDeployment + ? varStorageObjectsRgName + : '' + networkObjectsRgName: createAvdVnet + ? varNetworkObjectsRgName + : '' monitoringRgName: varMonitoringRgName deployCustomPolicyMonitoring: deployCustomPolicyMonitoring alaWorkspaceId: deployAlaWorkspace ? '' : alaExistingWorkspaceResourceId @@ -1113,11 +1103,11 @@ module monitoringDiagnosticSettings './modules/avdInsightsMonitoring/deploy.bice } // Networking -module networking './modules/networking/deploy.bicep' = if (createAvdVnet || createPrivateDnsZones || avdDeploySessionHosts || createAvdFslogixDeployment || varCreateAppAttachDeployment) { +module networking './modules/networking/deploy.bicep' = if (createAvdVnet || createPrivateDnsZones || avdDeploySessionHosts || createFslogixDeployment || varCreateAppAttachDeployment) { name: 'Networking-${time}' params: { createVnet: createAvdVnet - deployAsg: (avdDeploySessionHosts || createAvdFslogixDeployment || varCreateAppAttachDeployment) ? true : false + deployAsg: (avdDeploySessionHosts || createFslogixDeployment || varCreateAppAttachDeployment) ? true : false existingAvdSubnetResourceId: existingVnetAvdSubnetResourceId createPrivateDnsZones: (deployPrivateEndpointKeyvaultStorage || deployAvdPrivateLinkService) ? createPrivateDnsZones @@ -1126,6 +1116,7 @@ module networking './modules/networking/deploy.bicep' = if (createAvdVnet || cre computeObjectsRgName: varComputeObjectsRgName networkObjectsRgName: varNetworkObjectsRgName avdNetworksecurityGroupName: varAvdNetworksecurityGroupName + anfNetworksecurityGroupName: varAnfNetworksecurityGroupName privateEndpointNetworksecurityGroupName: varPrivateEndpointNetworksecurityGroupName avdRouteTableName: varAvdRouteTableName privateEndpointRouteTableName: varPrivateEndpointRouteTableName @@ -1135,16 +1126,19 @@ module networking './modules/networking/deploy.bicep' = if (createAvdVnet || cre remoteVnetPeeringName: varRemoteVnetPeeringName vnetAvdSubnetName: varVnetAvdSubnetName vnetPrivateEndpointSubnetName: varVnetPrivateEndpointSubnetName + vnetAnfSubnetName: varVnetAnfSubnetName createVnetPeering: varCreateVnetPeering deployDDoSNetworkProtection: deployDDoSNetworkProtection ddosProtectionPlanName: varDDosProtectionPlanName deployPrivateEndpointSubnet: (deployPrivateEndpointKeyvaultStorage || deployAvdPrivateLinkService) ? true : false //adding logic that will be used when also including AVD control plane PEs + deployAnfSubnet: storageService == 'ANF' ? true : false deployAvdPrivateLinkService: deployAvdPrivateLinkService vNetworkGatewayOnHub: vNetworkGatewayOnHub existingHubVnetResourceId: existingHubVnetResourceId location: avdDeploySessionHosts ? avdSessionHostLocation : avdManagementPlaneLocation vnetAvdSubnetAddressPrefix: vNetworkAvdSubnetAddressPrefix vnetPrivateEndpointSubnetAddressPrefix: vNetworkPrivateEndpointSubnetAddressPrefix + vnetAnfSubnetAddressPrefix: vNetworkAnfSubnetAddressPrefix workloadSubsId: avdWorkloadSubsId dnsServers: varDnsServers tags: createResourceTags ? union(varCustomResourceTags, varAvdDefaultTags) : varAvdDefaultTags @@ -1389,153 +1383,86 @@ module wrklKeyVault '../../avm/1.0.0/res/key-vault/vault/main.bicep' = { ] } -// Management VM deployment -module managementVm './modules/storageAzureFiles/.bicep/managementVm.bicep' = if (avdIdentityServiceProvider != 'EntraID' && (createAvdFslogixDeployment || varCreateAppAttachDeployment)) { - name: 'Storage-MGMT-VM-${time}' +// FSLogix and/or App Attach deployment +module storage './modules/sharedModules/storage.bicep' = if (varCreateStorageDeployment) { + name: 'SMB-Storage-${time}' + scope: subscription('${avdWorkloadSubsId}') params: { - diskEncryptionSetResourceId: diskZeroTrust ? zeroTrust.outputs.ztDiskEncryptionSetResourceId : '' - identityServiceProvider: avdIdentityServiceProvider - managementVmName: varManagementVmName - computeTimeZone: varTimeZoneSessionHosts - applicationSecurityGroupResourceId: (avdDeploySessionHosts || createAvdFslogixDeployment || varCreateAppAttachDeployment) - ? '${networking.outputs.applicationSecurityGroupResourceId}' - : '' + deploymentPrefix: varDeploymentPrefixLowercase + deploymentEnvironment: varDeploymentEnvironmentLowercase + storageService: storageService + useCustomNaming: avdUseCustomNaming + storageAvailabilityZones: zoneRedundantStorage domainJoinUserName: avdDomainJoinUserName - wrklKvName: varWrklKvName - serviceObjectsRgName: varServiceObjectsRgName + vmLocalUserName: avdVmLocalUserName + identityServiceProvider: avdIdentityServiceProvider + avdSessionHostsOuPath: avdOuPath + storageOuPath: storageOuPath + managementVmSize: avdDeploySessionHosts + ? avdSessionHostsSize + : 'Standard_D2ads_v5' + createResourceTags: createResourceTags + deployPrivateEndpointKeyvaultStorage: deployPrivateEndpointKeyvaultStorage + dnsServers: customDnsIps identityDomainName: identityDomainName - ouPath: varMgmtVmSpecs.ouPath - osDiskType: varMgmtVmSpecs.osDiskType - location: avdDeploySessionHosts ? avdSessionHostLocation : avdManagementPlaneLocation - mgmtVmSize: varMgmtVmSpecs.mgmtVmSize - subnetId: varMgmtVmSpecs.subnetId - enableAcceleratedNetworking: varMgmtVmSpecs.enableAcceleratedNetworking + storageObjectsRgName: varStorageObjectsRgName + baseScriptUri: varBaseScriptUri + securityPrincipalName: varSecurityPrincipalName + diskEncryptionSetResourceId: diskZeroTrust ? zeroTrust.outputs.ztDiskEncryptionSetResourceId : '' + sessionHostTimeZone: varTimeZoneSessionHosts + createFslogixDeployment: createFslogixDeployment securityType: securityType == 'Standard' ? '' : securityType secureBootEnabled: secureBootEnabled vTpmEnabled: vTpmEnabled - vmLocalUserName: avdVmLocalUserName - workloadSubsId: avdWorkloadSubsId encryptionAtHost: diskZeroTrust - storageManagedIdentityResourceId: varCreateStorageDeployment && avdIdentityServiceProvider != 'EntraID' - ? identity.outputs.managedIdentityStorageResourceId - : '' - osImage: varMgmtVmSpecs.osImage - tags: createResourceTags ? union(varCustomResourceTags, varAvdDefaultTags) : varAvdDefaultTags - } - dependsOn: [ - baselineStorageResourceGroup - wrklKeyVault - ] -} - -// FSLogix storage -module fslogixAzureFilesStorage './modules/storageAzureFiles/deploy.bicep' = if (createAvdFslogixDeployment) { - name: 'Storage-FSLogix-${time}' - params: { - storagePurpose: 'fslogix' - vmLocalUserName: avdVmLocalUserName - fileShareName: varFslogixFileShareName - fileShareMultichannel: (fslogixStoragePerformance == 'Premium') ? true : false - storageSku: varFslogixStorageSku - fileShareQuotaSize: fslogixFileShareQuotaSize - storageAccountFqdn: varFslogixStorageFqdn - storageAccountName: varFslogixStorageName - storageToDomainScript: varStorageToDomainScript - storageToDomainScriptUri: varStorageToDomainScriptUri - identityServiceProvider: avdIdentityServiceProvider - dscAgentPackageLocation: varStorageAzureFilesDscAgentPackageLocation - storageCustomOuPath: varStorageCustomOuPath - managementVmName: varManagementVmName - deployPrivateEndpoint: deployPrivateEndpointKeyvaultStorage - ouStgPath: varOuStgPath - managedIdentityClientId: varCreateStorageDeployment && avdIdentityServiceProvider != 'EntraID' - ? identity.outputs.managedIdentityStorageClientId - : '' - securityPrincipalName: varSecurityPrincipalName - domainJoinUserName: avdDomainJoinUserName - wrklKvName: varWrklKvName + storageManagedIdentityResourceId: ((varCreateStorageDeployment) && avdIdentityServiceProvider != 'EntraID') + ? identity.outputs.managedIdentityStorageResourceId + : '' + applicationSecurityGroupResourceId: (avdDeploySessionHosts || createFslogixDeployment || varCreateAppAttachDeployment) + ? '${networking.outputs.applicationSecurityGroupResourceId}' + : '' + createAppAttachDeployment: createAppAttachDeployment + fslogixFileShareCustomName: fslogixFileShareCustomName + appAttachFileShareCustomName: appAttachFileShareCustomName + storageAccountPrefixCustomName: storageAccountPrefixCustomName + anfAccountCustomName: anfAccountCustomName + managedIdentityClientId: (varCreateStorageDeployment && avdIdentityServiceProvider != 'EntraID') + ? identity.outputs.managedIdentityStorageClientId + : '' + privateDnsZoneFilesResourceId: createPrivateDnsZones + ? networking.outputs.azureFilesDnsZoneResourceId + : avdVnetPrivateDnsZoneFilesId serviceObjectsRgName: varServiceObjectsRgName - identityDomainName: identityDomainName - identityDomainGuid: identityDomainGuid - location: avdDeploySessionHosts ? avdSessionHostLocation : avdManagementPlaneLocation - storageObjectsRgName: varStorageObjectsRgName - privateEndpointSubnetId: createAvdVnet - ? '${networking.outputs.virtualNetworkResourceId}/subnets/${varVnetPrivateEndpointSubnetName}' - : existingVnetPrivateEndpointSubnetResourceId - vmsSubnetId: createAvdVnet - ? '${networking.outputs.virtualNetworkResourceId}/subnets/${varVnetAvdSubnetName}' - : existingVnetAvdSubnetResourceId - vnetPrivateDnsZoneFilesId: createPrivateDnsZones - ? networking.outputs.azureFilesDnsZoneResourceId - : avdVnetPrivateDnsZoneFilesId - workloadSubsId: avdWorkloadSubsId - tags: createResourceTags ? union(varCustomResourceTags, varAvdDefaultTags) : varAvdDefaultTags alaWorkspaceResourceId: avdDeployMonitoring - ? (deployAlaWorkspace - ? monitoringDiagnosticSettings.outputs.avdAlaWorkspaceResourceId - : alaExistingWorkspaceResourceId) - : '' - } - dependsOn: [ - baselineStorageResourceGroup - wrklKeyVault - managementVm - ] -} - -// App Attach storage -module appAttachAzureFilesStorage './modules/storageAzureFiles/deploy.bicep' = if (varCreateAppAttachDeployment) { - name: 'Storage-AppA-${time}' - params: { - storagePurpose: 'AppAttach' - vmLocalUserName: avdVmLocalUserName - fileShareName: varAppAttachFileShareName - fileShareMultichannel: (appAttachStoragePerformance == 'Premium') ? true : false - storageSku: varAppAttachStorageSku - fileShareQuotaSize: appAttachFileShareQuotaSize - storageAccountFqdn: varAppAttachStorageFqdn - storageAccountName: varAppAttachStorageName - storageToDomainScript: varStorageToDomainScript - storageToDomainScriptUri: varStorageToDomainScriptUri - identityServiceProvider: avdIdentityServiceProvider - dscAgentPackageLocation: varStorageAzureFilesDscAgentPackageLocation - storageCustomOuPath: varStorageCustomOuPath - managementVmName: varManagementVmName - deployPrivateEndpoint: deployPrivateEndpointKeyvaultStorage - ouStgPath: varOuStgPath - managedIdentityClientId: varCreateStorageDeployment && avdIdentityServiceProvider != 'EntraID' - ? identity.outputs.managedIdentityStorageClientId - : '' - securityPrincipalName: varSecurityPrincipalName - domainJoinUserName: avdDomainJoinUserName - wrklKvName: varWrklKvName - serviceObjectsRgName: varServiceObjectsRgName - identityDomainName: identityDomainName - identityDomainGuid: identityDomainGuid - location: avdDeploySessionHosts ? avdSessionHostLocation : avdManagementPlaneLocation - storageObjectsRgName: varStorageObjectsRgName - privateEndpointSubnetId: createAvdVnet - ? '${networking.outputs.virtualNetworkResourceId}/subnets/${varVnetPrivateEndpointSubnetName}' - : existingVnetPrivateEndpointSubnetResourceId - vmsSubnetId: createAvdVnet + ? (deployAlaWorkspace + ? monitoringDiagnosticSettings.outputs.avdAlaWorkspaceResourceId + : alaExistingWorkspaceResourceId) + : '' + deploymentEnvironmentOneCharacter: varDeploymentEnvironmentOneCharacter + computeStorageResourcesNamingStandard: varComputeStorageResourcesNamingStandard + fslogixFileShareQuotaSize: fslogixFileShareQuotaSize + appAttachFileShareQuotaSize: appAttachFileShareQuotaSize + fslogixStoragePerformance: fslogixStoragePerformance + appAttachStoragePerformance: appAttachStoragePerformance + anfSubnetResourceId: createAvdVnet + ? '${networking.outputs.virtualNetworkResourceId}/subnets/${varVnetAnfSubnetName}' + : existingVnetAnfSubnetResourceId + vmsSubnetResourceId: createAvdVnet ? '${networking.outputs.virtualNetworkResourceId}/subnets/${varVnetAvdSubnetName}' : existingVnetAvdSubnetResourceId - vnetPrivateDnsZoneFilesId: createPrivateDnsZones - ? networking.outputs.azureFilesDnsZoneResourceId - : avdVnetPrivateDnsZoneFilesId - workloadSubsId: avdWorkloadSubsId - tags: createResourceTags ? union(varCustomResourceTags, varAvdDefaultTags) : varAvdDefaultTags - alaWorkspaceResourceId: avdDeployMonitoring - ? (deployAlaWorkspace - ? monitoringDiagnosticSettings.outputs.avdAlaWorkspaceResourceId - : alaExistingWorkspaceResourceId) - : '' + privateEndpointSubnetResourceId: createAvdVnet + ? '${networking.outputs.virtualNetworkResourceId}/subnets/${varVnetPrivateEndpointSubnetName}' + : existingVnetAvdSubnetResourceId + location: avdDeploySessionHosts ? avdSessionHostLocation : avdManagementPlaneLocation + locationAcronym: avdDeploySessionHosts ? varSessionHostLocationAcronym : avdManagementPlaneLocation + managementVmOsImage: managementVmOsImage + keyVaultResourceId: wrklKeyVault.outputs.resourceId + customResourceTags: varCustomResourceTags + defaultTags: varAvdDefaultTags + identityDomainGuid: identityDomainGuid } dependsOn: [ - fslogixAzureFilesStorage baselineStorageResourceGroup - wrklKeyVault - managementVm ] } @@ -1563,14 +1490,14 @@ module sessionHosts './modules/avdSessionHosts/deploy.bicep' = [ for i in range(1, varSessionHostBatchCount): if (avdDeploySessionHosts) { name: 'SH-Batch-${i}-${time}' params: { - asgResourceId: (avdDeploySessionHosts || createAvdFslogixDeployment || varCreateAppAttachDeployment) + asgResourceId: (avdDeploySessionHosts || createFslogixDeployment || varCreateAppAttachDeployment) ? '${networking.outputs.applicationSecurityGroupResourceId}' : '' availability: availability availabilityZones: availabilityZones batchId: i - 1 computeObjectsRgName: varComputeObjectsRgName - configureFslogix: createAvdFslogixDeployment + configureFslogix: createFslogixDeployment count: i == varSessionHostBatchCount && varMaxSessionHostsDivisionRemainderValue > 0 ? varMaxSessionHostsDivisionRemainderValue : varMaxSessionHostsPerTemplate @@ -1588,9 +1515,11 @@ module sessionHosts './modules/avdSessionHosts/deploy.bicep' = [ domainJoinUserPrincipalName: avdDomainJoinUserName enableAcceleratedNetworking: enableAcceleratedNetworking encryptionAtHost: diskZeroTrust - fslogixSharePath: varFslogixSharePath - fslogixStorageAccountResourceId: avdIdentityServiceProvider == 'EntraID' - ? fslogixAzureFilesStorage.outputs.storageAccountResourceId + fslogixSharePath: createFslogixDeployment + ? storage.outputs.fslogixFileSharePath + : '' + fslogixStorageAccountResourceId: (avdIdentityServiceProvider == 'EntraID' && createFslogixDeployment) + ? storage.outputs.fslogixStorageAccountResourceId : '' hostPoolResourceId: managementPLane.outputs.hostPoolResourceId identityDomainName: identityDomainName diff --git a/workload/bicep/modules/azureNetappFiles/.bicep/getNetAppVolumeSmbServerFqdn.bicep b/workload/bicep/modules/azureNetappFiles/.bicep/getNetAppVolumeSmbServerFqdn.bicep new file mode 100644 index 000000000..8005fe344 --- /dev/null +++ b/workload/bicep/modules/azureNetappFiles/.bicep/getNetAppVolumeSmbServerFqdn.bicep @@ -0,0 +1,36 @@ +metadata name = 'AVD LZA storage ANF volume SMB server FQDN' +metadata description = 'This module returns the SMB server FQDN of an ANF volume.' +metadata owner = 'Azure/avdaccelerator' + +targetScope = 'subscription' + +// ========== // +// Parameters // +// ========== // + +param netAppVolumeResourceId string + +// =========== // +// Variable declaration // +// =========== // + +// =========== // +// Deployments // +// =========== // + +// Call on ANF account and volume to get the SMB server FQDN. +resource netAppAccount 'Microsoft.NetApp/netAppAccounts@2024-09-01' existing = { + name: split(netAppVolumeResourceId, '/')[8] + scope: resourceGroup(split(netAppVolumeResourceId, '/')[2], split(netAppVolumeResourceId, '/')[4]) + resource capacityPool 'capacityPools' existing = { + name: split(netAppVolumeResourceId, '/')[10] + resource volume 'volumes' existing = { + name: last(split(netAppVolumeResourceId, '/')) + } + } + } + +// =========== // +// Outputs // +// =========== // +output anfSmbServerFqdn string = netAppAccount::capacityPool::volume.properties.mountTargets[0].smbServerFqdn diff --git a/workload/bicep/modules/azureNetappFiles/deploy.bicep b/workload/bicep/modules/azureNetappFiles/deploy.bicep new file mode 100644 index 000000000..ad03f49d1 --- /dev/null +++ b/workload/bicep/modules/azureNetappFiles/deploy.bicep @@ -0,0 +1,122 @@ +metadata name = 'AVD LZA storage' +metadata description = 'This module deploys ANF account, capacity pool and volumes' +metadata owner = 'Azure/avdaccelerator' + +targetScope = 'subscription' + +// ========== // +// Parameters // +// ========== // + +@sys.description('Workload subscription ID') +param subId string + +@sys.description('Resource Group Name where to deploy Azure NetApp Files.') +param storageObjectsRgName string + +@sys.description('ANF account name.') +param accountName string + +@sys.description('Capacity pool volume name.') +param capacityPoolName string + +@sys.description('Capacity pool volume name.') +param createFslogixStorage bool + +@sys.description('Capacity pool volume name.') +param createAppAttachStorage bool + +@sys.description('ANF volumes.') +param volumes array + +@sys.description('ANF SMB prefix.') +param smbServerNamePrefix string + +@sys.description('DNS servers IPs.') +param dnsServers string + +@sys.description('Location where to deploy resources.') +param location string + +@sys.description('Identity domain name.') +param identityDomainName string + +@sys.description('Organizational Unit (OU) storage path for domain join.') +param storageOuPath string + +@sys.description('Keyvault resource ID to get credentials from.') +param keyVaultResourceId string + +@sys.description('AVD session host domain join credentials.') +param domainJoinUserName string + +@sys.description('ANF performance tier.') +param performance string + +@sys.description('ANF capacity pool size in TiBs.') +param capacityPoolSize int = 4 + +@sys.description('Tags to be applied to resources') +param tags object = {} + +@sys.description('Do not modify, used to set unique value for resource deployment.') +param time string = utcNow() + +// =========== // +// Variable declaration // +// =========== // +var varKeyVaultSubId = split(keyVaultResourceId, '/')[2] +var varKeyVaultRgName = split(keyVaultResourceId, '/')[4] +var varKeyVaultName = split(keyVaultResourceId, '/')[8] + +// =========== // +// Deployments // +// =========== // + +// Call on the KV. +resource keyVaultGet 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = { + name: varKeyVaultName + scope: resourceGroup('${varKeyVaultSubId}', '${varKeyVaultRgName}') +} + +// Provision the Azure NetApp Files. +module azureNetAppFiles '../../../../avm/1.1.0/res/net-app/net-app-account/main.bicep' = { + scope: resourceGroup('${subId}', '${storageObjectsRgName}') + name: 'Storage-ANF-${time}' + params: { + name: accountName + adName: accountName + domainName: identityDomainName + domainJoinUser: domainJoinUserName + domainJoinPassword: keyVaultGet.getSecret('domainJoinUserPassword') + //domainJoinOU: 'CN="${replace(storageOuPath, '"', '\\"')}"' + domainJoinOU: 'CN=${storageOuPath}' + dnsServers: dnsServers + smbServerNamePrefix: smbServerNamePrefix + location: location + // aesEncryption: ************* + // customerManagedKey: ************* + capacityPools:[ + { + name: capacityPoolName + serviceLevel: performance + size: capacityPoolSize * 1073741824 + volumes: volumes + } + ] + tags: tags + + } +} + +// =========== // +// Outputs // +// =========== // +output anfFslogixVolumeResourceId string = createFslogixStorage + ? azureNetAppFiles.outputs.capacityPoolResourceIds[0].volumeResourceIds[0] + : '' +output anfAppAttachVolumeResourceId string = (createAppAttachStorage && createFslogixStorage) + ? azureNetAppFiles.outputs.capacityPoolResourceIds[1].volumeResourceIds[1] + : ((createAppAttachStorage && !createFslogixStorage) + ? azureNetAppFiles.outputs.capacityPoolResourceIds[0].volumeResourceIds[0] + : '') diff --git a/workload/bicep/modules/networking/deploy.bicep b/workload/bicep/modules/networking/deploy.bicep index 02fdac939..dcb32d900 100644 --- a/workload/bicep/modules/networking/deploy.bicep +++ b/workload/bicep/modules/networking/deploy.bicep @@ -34,6 +34,9 @@ param avdNetworksecurityGroupName string @sys.description('Private endpoint Network Security Group Name') param privateEndpointNetworksecurityGroupName string +@sys.description('ANF Network Security Group Name') +param anfNetworksecurityGroupName string + @sys.description('Created if a new VNet for AVD is created. Application Security Group (ASG) for the session hosts.') param applicationSecurityGroupName string @@ -67,6 +70,9 @@ param deployDDoSNetworkProtection bool @sys.description('Optional. AVD Accelerator will deploy with private endpoints by default.') param deployPrivateEndpointSubnet bool +@sys.description('Deploy with ANf subnet.') +param deployAnfSubnet bool + @sys.description('Optional. Deploys private endpoints for the AVD Private Link Service. (Default: false)') param deployAvdPrivateLinkService bool @@ -79,12 +85,18 @@ param vnetAvdSubnetName string @sys.description('Private endpoint subnet Name.') param vnetPrivateEndpointSubnetName string +@sys.description('ANF subnet Name.') +param vnetAnfSubnetName string + @sys.description('AVD VNet subnet address prefix.') param vnetAvdSubnetAddressPrefix string @sys.description('Private endpoint VNet subnet address prefix.') param vnetPrivateEndpointSubnetAddressPrefix string +@sys.description('ANF VNet subnet address prefix.') +param vnetAnfSubnetAddressPrefix string + @sys.description('custom DNS servers IPs') param dnsServers array @@ -286,9 +298,7 @@ var varDefaultStaticRoutes = (varAzureCloudName == 'AzureCloud') } ] : [] - var varStaticRoutes = union(varDefaultStaticRoutes, customStaticRoutes) - var privateDnsZoneNames = { AutomationAgentService: 'privatelink.agentsvc.azure-automation.${privateDnsZoneSuffixes_AzureAutomation[environment().name]}' Automation: 'privatelink.azure-automation.${privateDnsZoneSuffixes_AzureAutomation[environment().name]}' @@ -303,7 +313,6 @@ var privateDnsZoneNames = { MonitorODS: 'privatelink.ods.opinsights.${privateDnsZoneSuffixes_Monitor[environment().name]}' MonitorOMS: 'privatelink.oms.opinsights.${privateDnsZoneSuffixes_Monitor[environment().name]}' } - var privateDnsZoneSuffixes_AzureAutomation = { AzureCloud: 'net' AzureUSGovernment: 'us' @@ -316,6 +325,69 @@ var privateDnsZoneSuffixes_Monitor = { AzureCloud: 'azure.com' AzureUSGovernment: 'azure.us' } +var varAvdSubnet = deployPrivateEndpointSubnet ? [ + { + name: vnetAvdSubnetName + addressPrefix: vnetAvdSubnetAddressPrefix + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroupResourceId: createVnet ? networksecurityGroupAvd.outputs.resourceId : '' + routeTableResourceId: createVnet ? routeTableAvd.outputs.resourceId : '' + } +] : [ + { + name: vnetAvdSubnetName + addressPrefix: vnetAvdSubnetAddressPrefix + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroupResourceId: createVnet ? networksecurityGroupAvd.outputs.resourceId : '' + routeTableResourceId: createVnet ? routeTableAvd.outputs.resourceId : '' + serviceEndpoints: [ + { + service: 'Microsoft.Storage' + locations: ['${location}'] + } + { + service: 'Microsoft.KeyVault' + locations: ['${location}'] + } + ] + } +] +var varPrivateEndpointSubnet = [ + { + name: vnetPrivateEndpointSubnetName + addressPrefix: vnetPrivateEndpointSubnetAddressPrefix + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroupResourceId: (createVnet && deployPrivateEndpointSubnet) + ? networksecurityGroupPrivateEndpoint.outputs.resourceId + : '' + routeTableResourceId: (createVnet && deployPrivateEndpointSubnet) + ? routeTablePrivateEndpoint.outputs.resourceId + : '' + } +] +var varAnfSubnet = [ + { + name: vnetAnfSubnetName + addressPrefix: vnetAnfSubnetAddressPrefix + privateEndpointNetworkPolicies: 'Disabled' + privateLinkServiceNetworkPolicies: 'Enabled' + networkSecurityGroupResourceId: (createVnet && deployAnfSubnet) + ? networksecurityGroupAnf.outputs.resourceId + : '' + delegations: [ + { + name: 'delegation' + properties: { + serviceName: 'Microsoft.NetApp/volumes' + } + } + ] + } +] +var varSubnets = union(varAvdSubnet,(deployAnfSubnet ? varAnfSubnet : []),(deployPrivateEndpointSubnet ? varPrivateEndpointSubnet : [])) // =========== // // Deployments // @@ -476,6 +548,20 @@ module networksecurityGroupPrivateEndpoint '../../../../avm/1.0.0/res/network/ne dependsOn: [] } +// ANF network security group. +module networksecurityGroupAnf '../../../../avm/1.0.0/res/network/network-security-group/main.bicep' = if (createVnet && deployAnfSubnet) { + scope: resourceGroup('${workloadSubsId}', '${networkObjectsRgName}') + name: 'NSG-ANF-${time}' + params: { + name: anfNetworksecurityGroupName + location: location + tags: tags + diagnosticSettings: varDiagnosticSettings + securityRules: [] + } + dependsOn: [] +} + // Application security group. module applicationSecurityGroup '../../../../avm/1.0.0/res/network/application-security-group/main.bicep' = if (deployAsg) { scope: resourceGroup('${workloadSubsId}', '${computeObjectsRgName}') @@ -554,49 +640,7 @@ module virtualNetwork '../../../../avm/1.0.0/res/network/virtual-network/main.bi } ] : [] - subnets: deployPrivateEndpointSubnet - ? [ - { - name: vnetAvdSubnetName - addressPrefix: vnetAvdSubnetAddressPrefix - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - networkSecurityGroupResourceId: createVnet ? networksecurityGroupAvd.outputs.resourceId : '' - routeTableResourceId: createVnet ? routeTableAvd.outputs.resourceId : '' - } - { - name: vnetPrivateEndpointSubnetName - addressPrefix: vnetPrivateEndpointSubnetAddressPrefix - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - networkSecurityGroupResourceId: (createVnet && deployPrivateEndpointSubnet) - ? networksecurityGroupPrivateEndpoint.outputs.resourceId - : '' - routeTableResourceId: (createVnet && deployPrivateEndpointSubnet) - ? routeTablePrivateEndpoint.outputs.resourceId - : '' - } - ] - : [ - { - name: vnetAvdSubnetName - addressPrefix: vnetAvdSubnetAddressPrefix - privateEndpointNetworkPolicies: 'Disabled' - privateLinkServiceNetworkPolicies: 'Enabled' - networkSecurityGroupResourceId: createVnet ? networksecurityGroupAvd.outputs.resourceId : '' - routeTableResourceId: createVnet ? routeTableAvd.outputs.resourceId : '' - serviceEndpoints: [ - { - service: 'Microsoft.Storage' - locations: ['${location}'] - } - { - service: 'Microsoft.KeyVault' - locations: ['${location}'] - } - ] - } - ] + subnets: varSubnets ddosProtectionPlanResourceId: deployDDoSNetworkProtection ? ddosProtectionPlan.outputs.resourceId : '' tags: tags diagnosticSettings: varDiagnosticSettings @@ -605,6 +649,7 @@ module virtualNetwork '../../../../avm/1.0.0/res/network/virtual-network/main.bi ? [ networksecurityGroupAvd networksecurityGroupPrivateEndpoint + networksecurityGroupAnf routeTableAvd routeTablePrivateEndpoint ] diff --git a/workload/bicep/modules/storageAzureFiles/.bicep/managementVm.bicep b/workload/bicep/modules/sharedModules/managementVm.bicep similarity index 74% rename from workload/bicep/modules/storageAzureFiles/.bicep/managementVm.bicep rename to workload/bicep/modules/sharedModules/managementVm.bicep index 7df7aaaee..e65b173cb 100644 --- a/workload/bicep/modules/storageAzureFiles/.bicep/managementVm.bicep +++ b/workload/bicep/modules/sharedModules/managementVm.bicep @@ -1,4 +1,7 @@ targetScope = 'subscription' +metadata name = 'AVD LZA storage management VM' +metadata description = 'This module deploys a management VM to join Azure Files to domain and for tools.' +metadata owner = 'Azure/avdaccelerator' // ========== // // Parameters // @@ -8,7 +11,7 @@ targetScope = 'subscription' param diskEncryptionSetResourceId string @sys.description('AVD workload subscription ID, multiple subscriptions scenario.') -param workloadSubsId string +param subId string @sys.description('Virtual machine time zone.') param computeTimeZone string @@ -19,8 +22,8 @@ param identityServiceProvider string @sys.description('Resource Group Name for Azure Files.') param serviceObjectsRgName string -@sys.description('AVD subnet ID.') -param subnetId string +@sys.description('Subnet resource ID.') +param subnetResourceId string @sys.description('Enable accelerated networking on the session host VMs.') param enableAcceleratedNetworking bool @@ -41,7 +44,7 @@ param location string param encryptionAtHost bool @sys.description('Session host VM size.') -param mgmtVmSize string +param vmSize string @sys.description('OS disk type for session host.') param osDiskType string @@ -58,8 +61,8 @@ param vmLocalUserName string @sys.description('Identity domain name.') param identityDomainName string -@sys.description('Keyvault name to get credentials from.') -param wrklKvName string +@sys.description('Keyvault resource ID to get credentials from.') +param keyVaultResourceId string @sys.description('AVD session host domain join credentials.') param domainJoinUserName string @@ -74,7 +77,7 @@ param applicationSecurityGroupResourceId string param tags object @sys.description('Name for management virtual machine. for tools and to join Azure Files to domain.') -param managementVmName string +param vmName string @sys.description('Do not modify, used to set unique value for resource deployment.') param time string = utcNow() @@ -82,32 +85,35 @@ param time string = utcNow() // =========== // // Variable declaration // // =========== // - -var varManagedDisk = empty(diskEncryptionSetResourceId) ? { - storageAccountType: osDiskType -} : { - diskEncryptionSet: { - id: diskEncryptionSetResourceId +var varKeyVaultSubId = split(keyVaultResourceId, '/')[2] +var varKeyVaultRgName = split(keyVaultResourceId, '/')[4] +var varKeyVaultName = split(keyVaultResourceId, '/')[8] +var varManagedDisk = empty(diskEncryptionSetResourceId) + ? { + storageAccountType: osDiskType + } : { + diskEncryptionSet: { + id: diskEncryptionSetResourceId + } + storageAccountType: osDiskType } - storageAccountType: osDiskType -} // =========== // // Deployments // // =========== // // Call on the KV. -resource avdWrklKeyVaultget 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = { - name: wrklKvName - scope: resourceGroup('${workloadSubsId}', '${serviceObjectsRgName}') +resource keyVaultget 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = { + name: varKeyVaultName + scope: resourceGroup('${varKeyVaultSubId}', '${varKeyVaultRgName}') } // Provision temporary VM and add it to domain. -module managementVm '../../../../../avm/1.0.0/res/compute/virtual-machine/main.bicep' = { - scope: resourceGroup('${workloadSubsId}', '${serviceObjectsRgName}') +module managementVm '../../../../avm/1.0.0/res/compute/virtual-machine/main.bicep' = { + scope: resourceGroup('${subId}', '${serviceObjectsRgName}') name: 'MGMT-VM-${time}' params: { - name: managementVmName + name: vmName location: location timeZone: computeTimeZone managedIdentities: { @@ -119,7 +125,7 @@ module managementVm '../../../../../avm/1.0.0/res/compute/virtual-machine/main.b encryptionAtHost: encryptionAtHost zone: 0 osType: 'Windows' - vmSize: mgmtVmSize + vmSize: vmSize securityType: securityType secureBootEnabled: secureBootEnabled vTpmEnabled: vTpmEnabled @@ -131,16 +137,16 @@ module managementVm '../../../../../avm/1.0.0/res/compute/virtual-machine/main.b managedDisk: varManagedDisk } adminUsername: vmLocalUserName - adminPassword: avdWrklKeyVaultget.getSecret('vmLocalUserPassword') + adminPassword: keyVaultget.getSecret('vmLocalUserPassword') nicConfigurations: [ { - name: 'nic-01-${managementVmName}' + name: 'nic-01-${vmName}' deleteOption: 'Delete' enableAcceleratedNetworking: enableAcceleratedNetworking ipConfigurations: !empty(applicationSecurityGroupResourceId) ? [ { name: 'ipconfig01' - subnetResourceId: subnetId + subnetResourceId: subnetResourceId applicationSecurityGroups: [ { id: applicationSecurityGroupResourceId @@ -150,19 +156,23 @@ module managementVm '../../../../../avm/1.0.0/res/compute/virtual-machine/main.b ] : [ { name: 'ipconfig01' - subnetResourceId: subnetId + subnetResourceId: subnetResourceId } ] } ] // Join domain allowExtensionOperations: true - extensionDomainJoinPassword: avdWrklKeyVaultget.getSecret('domainJoinUserPassword') + extensionDomainJoinPassword: keyVaultget.getSecret('domainJoinUserPassword') extensionDomainJoinConfig: { - enabled: contains(identityServiceProvider, 'EntraID') ? false: true + enabled: contains(identityServiceProvider, 'EntraID') + ? false + : true settings: { name: identityDomainName - ouPath: !empty(ouPath) ? ouPath : null + ouPath: !empty(ouPath) + ? ouPath + : null user: domainJoinUserName restart: 'true' options: '3' @@ -170,7 +180,9 @@ module managementVm '../../../../../avm/1.0.0/res/compute/virtual-machine/main.b } // Entra ID Join. extensionAadJoinConfig: { - enabled: contains(identityServiceProvider, 'EntraID') ? true: false + enabled: contains(identityServiceProvider, 'EntraID') + ? true + : false } tags: tags } diff --git a/workload/bicep/modules/storageAzureFiles/.bicep/azureFilesDomainJoin.bicep b/workload/bicep/modules/sharedModules/smbUtilities.bicep similarity index 93% rename from workload/bicep/modules/storageAzureFiles/.bicep/azureFilesDomainJoin.bicep rename to workload/bicep/modules/sharedModules/smbUtilities.bicep index 915a8f45b..c85470bd5 100644 --- a/workload/bicep/modules/storageAzureFiles/.bicep/azureFilesDomainJoin.bicep +++ b/workload/bicep/modules/sharedModules/smbUtilities.bicep @@ -32,7 +32,7 @@ param time string = utcNow() // =========== // // Add Azure Files to AD DS domain. -module dscStorageScript '../../../../../avm/1.0.0/res/compute/virtual-machine/extension/main.bicep' = { +module dscStorageScript '../../../../avm/1.0.0/res/compute/virtual-machine/extension/main.bicep' = { name: 'VM-Ext-AVM-${time}' params: { name: 'AzureFilesDomainJoin' diff --git a/workload/bicep/modules/sharedModules/storage.bicep b/workload/bicep/modules/sharedModules/storage.bicep new file mode 100644 index 000000000..e690ba296 --- /dev/null +++ b/workload/bicep/modules/sharedModules/storage.bicep @@ -0,0 +1,487 @@ +targetScope = 'subscription' + +// ========== // +// Parameters // +// ========== // + +@description('The subscription ID for the AVD workload.') +param subId string = subscription().subscriptionId + +@description('The deployment prefix in lowercase.') +param deploymentPrefix string + +@description('The deployment environment in lowercase.') +param deploymentEnvironment string + +@description('The session host location acronym derived from the resource group location.') +param location string + +@description('The session host or AVD management plane location acronym For example, "eus" for East US.') +param locationAcronym string + +@description('The storage service to use (AzureFiles or ANF).') +param storageService string + +@description('Indicates whether to use custom naming for AVD.') +param useCustomNaming bool + +@description('The naming standard for compute storage resources coming from the main template.') +param computeStorageResourcesNamingStandard string + +@description('The custom name for the FSLogix file share.') +param fslogixFileShareCustomName string = '' + +@description('The custom name for the App Attach file share.') +param appAttachFileShareCustomName string = '' + +@description('The OS image for the management VM.') +param managementVmOsImage string + +@sys.description('AVD FSLogix and App Attach storage account prefix custom name.') +param storageAccountPrefixCustomName string = 'st' + +@description('The custom name for the ANF account.') +param anfAccountCustomName string = '' + +@description('Deployment prefix one character.') +param deploymentEnvironmentOneCharacter string + +@description('The resource ID of the Key Vault.') +param keyVaultResourceId string + +@description('The Azure Log Analytics workspace resource ID.') +param alaWorkspaceResourceId string + +@description('The private DNS zone files resource ID.') +param privateDnsZoneFilesResourceId string + +@description('The client ID of the managed identity.') +param managedIdentityClientId string + +@description('The FSLogix file share quota size in GiBs.') +param fslogixFileShareQuotaSize int + +@description('The App Attach file share quota size in GiBs.') +param appAttachFileShareQuotaSize int + +@description('The storage performance level for FSLogix.') +param fslogixStoragePerformance string + +@description('The storage performance level for App Attach.') +param appAttachStoragePerformance string + +@description('Subnet resource ID for ANF volumes.') +param anfSubnetResourceId string + +@description('Subnet resource ID for VMs.') +param vmsSubnetResourceId string + +@description('The security type for the VM (e.g., TrustedLaunch, Standard).') +param securityType string + +@description('Subnet resource ID for private endpoints.') +param privateEndpointSubnetResourceId string + +@description('The resource ID of the disk encryption set.') +param diskEncryptionSetResourceId string + +@description('Indicates whether encryption at host is enabled for the VM.') +param encryptionAtHost bool + +@description('The resource ID of the managed identity for storage.') +param storageManagedIdentityResourceId string + +@description('Indicates whether secure boot is enabled for the VM.') +param secureBootEnabled bool + +@description('Indicates whether vTPM is enabled for the VM.') +param vTpmEnabled bool + +@description('The time zone for the session host.') +param sessionHostTimeZone string + +@description('The resource ID of the application security group.') +param applicationSecurityGroupResourceId string + +@description('USe or not zone redundant storage.') +param storageAvailabilityZones bool + +@description('The custom DNS IPs.') +param dnsServers string + +@description('The identity service provider (e.g., EntraDS).') +param identityServiceProvider string + +@description('The domain join username.') +param domainJoinUserName string + +@description('The VM local username.') +param vmLocalUserName string + +@description('The service objects resource group name.') +param serviceObjectsRgName string + +@description('The storage objects resource group name.') +param storageObjectsRgName string + +@description('The VM size for the management VM.') +param managementVmSize string + +@description('The OU path for AVD session hosts.') +param avdSessionHostsOuPath string + +@description('The storage OU path.') +param storageOuPath string = '' + +@description('The custom resource tags.') +param customResourceTags object = {} + +@description('The AVD default tags.') +param defaultTags object = {} + +@description('Indicates whether to create resource tags.') +param createResourceTags bool + +@description('The base script URI.') +param baseScriptUri string + +@description('The security principal name.') +param securityPrincipalName string = '' + +@description('The identity domain name.') +param identityDomainName string + +@description('The identity domain GUID.') +param identityDomainGuid string = '' + +@description('Indicates whether to deploy private endpoints for Key Vault and storage.') +param deployPrivateEndpointKeyvaultStorage bool + +@description('Indicates whether to create FSLogix deployment.') +param createFslogixDeployment bool + +@description('Indicates whether to create App Attach deployment.') +param createAppAttachDeployment bool + +@description('The deployment timestamp.') +param time string = utcNow() + +// =========== // +// Variable declaration // +// =========== // +var varNamingUniqueStringThreeChar = take('${uniqueString(subId, deploymentPrefix, time)}', 3) +var varAnfCapacityPoolSize = ((createFslogixDeployment ? fslogixFileShareQuotaSize : 0) + (createAppAttachDeployment + ? appAttachFileShareQuotaSize + : 0)) > 4096 + ? ((createFslogixDeployment ? fslogixFileShareQuotaSize : 0) + (createAppAttachDeployment + ? appAttachFileShareQuotaSize + : 0)) + : 4096 +var varFslogixFileShareName = storageService == 'AzureFiles' + ? (useCustomNaming + ? fslogixFileShareCustomName + : 'fslogix-pc-${deploymentPrefix}-${deploymentEnvironment}-${locationAcronym}-001') + : storageService == 'ANF' ? 'fsl${deploymentPrefix}${deploymentEnvironment}${locationAcronym}001' : '' +var varAnfSmbServerNamePrefix = 'anf${deploymentPrefix}${deploymentEnvironment}' +var varAppAttachFileShareName = storageService == 'AzureFiles' + ? (useCustomNaming + ? appAttachFileShareCustomName + : 'appa-${deploymentPrefix}-${deploymentEnvironment}-${locationAcronym}-001') + : storageService == 'ANF' ? 'appa${deploymentPrefix}01' : '' +var varFslogixAnfVolume = createFslogixDeployment + ? [ + { + name: varFslogixFileShareName + coolAccess: false + encryptionKeySource: 'Microsoft.NetApp' + zones: [] // storageAvailabilityZones + //? availabilityZones + //: [] + serviceLevel: fslogixStoragePerformance + networkFeatures: 'Standard' + usageThreshold: fslogixFileShareQuotaSize * 1073741824 // Convert GiBs to bytes + protocolTypes: [ + 'CIFS' + ] + subnetResourceId: anfSubnetResourceId + creationToken: varFslogixFileShareName + smbContinuouslyAvailable: true + securityStyle: 'ntfs' + } + ] + : [] +var varAppAttchAnfVolume = createAppAttachDeployment + ? [ + { + name: varAppAttachFileShareName + coolAccess: false + encryptionKeySource: 'Microsoft.NetApp' + zones: [] // storageAvailabilityZones + //? availabilityZones + //: [] + serviceLevel: appAttachStoragePerformance + networkFeatures: 'Standard' + usageThreshold: appAttachFileShareQuotaSize * 1073741824 // Convert GiBs to bytes + protocolTypes: [ + 'CIFS' + ] + subnetResourceId: anfSubnetResourceId + creationToken: varAppAttachFileShareName + smbContinuouslyAvailable: true + securityStyle: 'ntfs' + } + ] + : [] +var varAnfVolumes = union(varFslogixAnfVolume, varAppAttchAnfVolume) +var varFslogixStorageName = useCustomNaming + ? '${storageAccountPrefixCustomName}fsl${deploymentPrefix}${deploymentEnvironmentOneCharacter}${varNamingUniqueStringThreeChar}' + : 'stfsl${deploymentPrefix}${deploymentEnvironmentOneCharacter}${varNamingUniqueStringThreeChar}' +var varAnfAccountName = useCustomNaming + ? anfAccountCustomName + : 'anf-acc-${computeStorageResourcesNamingStandard}-001' +var varAnfCapacityPoolName = 'anf-cpool-${computeStorageResourcesNamingStandard}-001' +var varFslogixStorageFqdn = createFslogixDeployment + ? ((storageService == 'AzureFiles') + ? '${varFslogixStorageName}.file.${environment().suffixes.storage}' + : (storageService == 'ANF') + ? '${varFslogixFileShareName}...${location}.netapp.azure.com' + : '') + : '' +var varAppAttachStorageFqdn = createAppAttachDeployment + ? ((storageService == 'AzureFiles') + ? '${varAppAttachStorageName}.file.${environment().suffixes.storage}' + : (storageService == 'ANF') + ? '${varAppAttachFileShareName}...${location}.netapp.azure.com' + : '') + : '' +var varAppAttachStorageName = useCustomNaming + ? '${storageAccountPrefixCustomName}appa${deploymentPrefix}${deploymentEnvironmentOneCharacter}${varNamingUniqueStringThreeChar}' + : 'stappa${deploymentPrefix}${deploymentEnvironmentOneCharacter}${varNamingUniqueStringThreeChar}' +var varManagementVmName = 'vmmgmt${deploymentPrefix}${deploymentEnvironmentOneCharacter}${locationAcronym}' +var varFslogixFileSharePath = createFslogixDeployment + ? (storageService == 'AzureFiles' + ? '\\\\${varFslogixStorageName}.file.${environment().suffixes.storage}\\${varFslogixFileShareName}' + : (storageService == 'ANF' + ? '\\\\${netAppAccountGet.outputs.anfSmbServerFqdn}\\${last(split(azureNetAppFiles.outputs.anfFslogixVolumeResourceId, '/'))}' + : '')) + : '' +var varAppAttachFileSharePath = createAppAttachDeployment + ? (storageService == 'AzureFiles' + ? '\\\\${varAppAttachStorageName}.file.${environment().suffixes.storage}\\${varAppAttachFileShareName}' + : (storageService == 'ANF' + ? '\\\\${netAppAccountGet.outputs.anfSmbServerFqdn}\\${last(split(azureNetAppFiles.outputs.anfAppAttachVolumeResourceId, '/'))}' + : '')) + : '' +var varFslogixStoragePerformance = fslogixStoragePerformance == 'Ultra' + ? 'Premium' + : fslogixStoragePerformance +var varAppAttachStoragePerformance = appAttachStoragePerformance == 'Ultra' + ? 'Premium' + : appAttachStoragePerformance +var varFslogixStorageSku = (storageAvailabilityZones && storageService == 'AzureFiles') + ? '${varFslogixStoragePerformance}_ZRS' + : '${varFslogixStoragePerformance}_LRS' +var varAppAttachStorageSku = storageAvailabilityZones + ? '${varAppAttachStoragePerformance}_ZRS' + : '${varAppAttachStoragePerformance}_LRS' +var varStorageAzureFilesDscAgentPackageLocation = 'https://github.com/Azure/avdaccelerator/raw/main/workload/scripts/DSCStorageScripts/1.0.3/DSCStorageScripts.zip' +var varStorageToDomainScriptUri = '${baseScriptUri}scripts/Manual-DSC-Storage-Scripts.ps1' +var varStorageToDomainScript = './Manual-DSC-Storage-Scripts.ps1' +var varOuStgPath = !empty(storageOuPath) + ? '"${storageOuPath}"' + : '"${varDefaultStorageOuPath}"' +var varDefaultStorageOuPath = (identityServiceProvider == 'EntraDS') + ? 'AADDC Computers' + : 'Computers' +var varStorageCustomOuPath = !empty(storageOuPath) + ? 'true' + : 'false' +var varMarketPlaceGalleryWindows = loadJsonContent('../../../variables/osMarketPlaceImages.json') +var varAnfVolumeResourceIdGet = (createFslogixDeployment && (storageService == 'ANF')) + ? azureNetAppFiles.outputs.anfFslogixVolumeResourceId + : ((createAppAttachDeployment && (storageService == 'ANF')) ? azureNetAppFiles.outputs.anfAppAttachVolumeResourceId + : '') +// =========== // +// Deployments // +// =========== // +// Management VM deployment +module managementVm './managementVm.bicep' = if (identityServiceProvider != 'EntraID' && (createFslogixDeployment || createAppAttachDeployment)) { + name: 'Storage-MGMT-VM-${time}' + params: { + diskEncryptionSetResourceId: diskEncryptionSetResourceId + identityServiceProvider: identityServiceProvider + vmName: varManagementVmName + computeTimeZone: sessionHostTimeZone + applicationSecurityGroupResourceId: applicationSecurityGroupResourceId + domainJoinUserName: domainJoinUserName + keyVaultResourceId: keyVaultResourceId + serviceObjectsRgName: serviceObjectsRgName + identityDomainName: identityDomainName + ouPath: avdSessionHostsOuPath + osDiskType: 'Standard_LRS' + location: location + vmSize: managementVmSize + subnetResourceId: vmsSubnetResourceId + enableAcceleratedNetworking: true + securityType: securityType + secureBootEnabled: secureBootEnabled + vTpmEnabled: vTpmEnabled + vmLocalUserName: vmLocalUserName + subId: subId + encryptionAtHost: encryptionAtHost + storageManagedIdentityResourceId: storageManagedIdentityResourceId + osImage: varMarketPlaceGalleryWindows[managementVmOsImage] + tags: createResourceTags + ? union(customResourceTags, defaultTags) + : defaultTags + } + dependsOn: [] +} + +// Azure NetApp Files +module azureNetAppFiles '../azureNetappFiles/deploy.bicep' = if ((storageService == 'ANF') && (!contains(identityServiceProvider,'EntraID'))) { + name: 'Storage-ANF-${time}' + params: { + accountName: varAnfAccountName + capacityPoolName: varAnfCapacityPoolName + volumes: varAnfVolumes + smbServerNamePrefix: varAnfSmbServerNamePrefix + capacityPoolSize: varAnfCapacityPoolSize + dnsServers: dnsServers + performance: fslogixStoragePerformance + createFslogixStorage: createFslogixDeployment + createAppAttachStorage: createAppAttachDeployment + storageOuPath: !empty(storageOuPath) + ? storageOuPath + : varDefaultStorageOuPath + domainJoinUserName: domainJoinUserName + keyVaultResourceId: keyVaultResourceId + identityDomainName: identityDomainName + location: location + storageObjectsRgName: storageObjectsRgName + subId: subId + tags: createResourceTags + ? union(customResourceTags, defaultTags) + : defaultTags + // alaWorkspaceResourceId: deployMonitoring + // ? (deployAlaWorkspace + // ? monitoringDiagnosticSettings.outputs.avdAlaWorkspaceResourceId + // : alaExistingWorkspaceResourceId) + // : '' + } + dependsOn: [ + managementVm + ] +} + + +// Call on ANF account and volume to get the SMB server FQDN. +module netAppAccountGet '../azureNetappFiles/.bicep/getNetAppVolumeSmbServerFqdn.bicep' = if (storageService == 'ANF') { + name: 'Get-ANF-SMB-Server-FQDN-${time}' + params: { + netAppVolumeResourceId: varAnfVolumeResourceIdGet + } +} + +// FSLogix Azure Files +module fslogixAzureFilesStorage '../storageAzureFiles/deploy.bicep' = if (createFslogixDeployment && (storageService != 'ANF')) { + name: 'Storage-FSLogix-ST-${time}' + params: { + storagePurpose: 'fslogix' + vmLocalUserName: vmLocalUserName + fileShareName: varFslogixFileShareName + fileShareMultichannel: (varFslogixStoragePerformance == 'Premium') + ? true + : false + storageSku: varFslogixStorageSku + fileShareQuotaSize: fslogixFileShareQuotaSize + storageAccountFqdn: varFslogixStorageFqdn + storageAccountName: varFslogixStorageName + storageToDomainScript: varStorageToDomainScript + storageToDomainScriptUri: varStorageToDomainScriptUri + identityServiceProvider: identityServiceProvider + dscAgentPackageLocation: varStorageAzureFilesDscAgentPackageLocation + storageCustomOuPath: varStorageCustomOuPath + managementVmName: varManagementVmName + deployPrivateEndpoint: deployPrivateEndpointKeyvaultStorage + keyVaultResourceId: keyVaultResourceId + storageOuPath: varOuStgPath + managedIdentityClientId: managedIdentityClientId + securityPrincipalName: securityPrincipalName + domainJoinUserName: domainJoinUserName + serviceObjectsRgName: serviceObjectsRgName + identityDomainName: identityDomainName + identityDomainGuid: identityDomainGuid + location: location + storageObjectsRgName: storageObjectsRgName + privateEndpointSubnetResourceId: privateEndpointSubnetResourceId + vmsSubnetResourceId: vmsSubnetResourceId + privateDnsZoneFilesResourceId: privateDnsZoneFilesResourceId + subId: subId + tags: createResourceTags + ? union(customResourceTags, defaultTags) + : defaultTags + alaWorkspaceResourceId: alaWorkspaceResourceId + } + dependsOn: [ + managementVm + ] +} + +// App Attach Azure Files +module appAttachAzureFilesStorage '../storageAzureFiles/deploy.bicep' = if (createFslogixDeployment && (storageService != 'ANF')) { + name: 'Storage-AppA-${time}' + params: { + storagePurpose: 'AppAttach' + vmLocalUserName: vmLocalUserName + fileShareName: varAppAttachFileShareName + fileShareMultichannel: (varAppAttachStoragePerformance == 'Premium') + ? true + : false + storageSku: varAppAttachStorageSku + fileShareQuotaSize: appAttachFileShareQuotaSize + storageAccountFqdn: varAppAttachStorageFqdn + storageAccountName: varAppAttachStorageName + storageToDomainScript: varStorageToDomainScript + storageToDomainScriptUri: varStorageToDomainScriptUri + identityServiceProvider: identityServiceProvider + dscAgentPackageLocation: varStorageAzureFilesDscAgentPackageLocation + storageCustomOuPath: varStorageCustomOuPath + managementVmName: varManagementVmName + deployPrivateEndpoint: deployPrivateEndpointKeyvaultStorage + storageOuPath: varOuStgPath + managedIdentityClientId: managedIdentityClientId + securityPrincipalName: securityPrincipalName + domainJoinUserName: domainJoinUserName + keyVaultResourceId: keyVaultResourceId + serviceObjectsRgName: serviceObjectsRgName + identityDomainName: identityDomainName + identityDomainGuid: identityDomainGuid + location: location + storageObjectsRgName: storageObjectsRgName + privateEndpointSubnetResourceId: privateEndpointSubnetResourceId + vmsSubnetResourceId: vmsSubnetResourceId + privateDnsZoneFilesResourceId: privateDnsZoneFilesResourceId + subId: subId + tags: createResourceTags + ? union(customResourceTags, defaultTags) + : defaultTags + alaWorkspaceResourceId: alaWorkspaceResourceId + } + dependsOn: [ + managementVm + ] +} + +// =========== // +// Outputs // +// =========== // + +output fslogixFileSharePath string = varFslogixFileSharePath +output appAttachFileSharePath string = varAppAttachFileSharePath +output fslogixStorageAccountResourceId string = (createFslogixDeployment && (storageService == 'AzureFiles')) + ? fslogixAzureFilesStorage.outputs.storageAccountResourceId + : '' +output appAttachStorageAccountResourceId string = (createAppAttachDeployment && (storageService == 'AzureFiles')) + ? appAttachAzureFilesStorage.outputs.storageAccountResourceId + : '' diff --git a/workload/bicep/modules/storageAzureFiles/deploy.bicep b/workload/bicep/modules/storageAzureFiles/deploy.bicep index 26565300d..f83474a99 100644 --- a/workload/bicep/modules/storageAzureFiles/deploy.bicep +++ b/workload/bicep/modules/storageAzureFiles/deploy.bicep @@ -9,7 +9,7 @@ targetScope = 'subscription' // ========== // @sys.description('AVD workload subscription ID, multiple subscriptions scenario.') -param workloadSubsId string +param subId string @sys.description('Resource Group Name for Azure Files.') param storageObjectsRgName string @@ -27,10 +27,10 @@ param storageAccountName string param fileShareName string @sys.description('Private endpoint subnet ID.') -param privateEndpointSubnetId string +param privateEndpointSubnetResourceId string @sys.description('VMs subnet ID.') -param vmsSubnetId string +param vmsSubnetResourceId string @sys.description('Location where to deploy resources.') param location string @@ -44,8 +44,8 @@ param identityDomainName string @sys.description('AD domain GUID.') param identityDomainGuid string -@sys.description('Keyvault name to get credentials from.') -param wrklKvName string +@sys.description('Key Vault Resource ID.') +param keyVaultResourceId string @sys.description('AVD session host domain join credentials.') param domainJoinUserName string @@ -60,7 +60,7 @@ param storageSku string param fileShareQuotaSize int @sys.description('Use Azure private DNS zones for private endpoints.') -param vnetPrivateDnsZoneFilesId string +param privateDnsZoneFilesResourceId string @sys.description('Script name for adding storage account to Active Directory.') param storageToDomainScript string @@ -69,7 +69,7 @@ param storageToDomainScript string param storageToDomainScriptUri string @sys.description('Tags to be applied to resources') -param tags object +param tags object = {} @sys.description('Name for management virtual machine. for tools and to join Azure Files to domain.') param managementVmName string @@ -94,7 +94,7 @@ param dscAgentPackageLocation string param storageCustomOuPath string @sys.description('OU Storage Path') -param ouStgPath string +param storageOuPath string @sys.description('Managed Identity Client ID') param managedIdentityClientId string @@ -109,11 +109,16 @@ param storageAccountFqdn string // Variable declaration // // =========== // +var varKeyVaultSubId = split(keyVaultResourceId, '/')[2] +var varKeyVaultRgName = split(keyVaultResourceId, '/')[4] +var varKeyVaultName = split(keyVaultResourceId, '/')[8] var varAzureCloudName = environment().name var varWrklStoragePrivateEndpointName = 'pe-${storageAccountName}-file' var varSecurityPrincipalName = !empty(securityPrincipalName) ? securityPrincipalName : 'none' -var varAdminUserName = contains(identityServiceProvider, 'EntraID') ? vmLocalUserName : domainJoinUserName -var varStorageToDomainScriptArgs = '-DscPath ${dscAgentPackageLocation} -StorageAccountName ${storageAccountName} -StorageAccountRG ${storageObjectsRgName} -StoragePurpose ${storagePurpose} -DomainName ${identityDomainName} -IdentityServiceProvider ${identityServiceProvider} -AzureCloudEnvironment ${varAzureCloudName} -SubscriptionId ${workloadSubsId} -AdminUserName ${varAdminUserName} -CustomOuPath ${storageCustomOuPath} -OUName ${ouStgPath} -ShareName ${fileShareName} -ClientId ${managedIdentityClientId} -SecurityPrincipalName "${varSecurityPrincipalName}" -StorageAccountFqdn ${storageAccountFqdn} ' +var varAdminUserName = contains(identityServiceProvider, 'EntraID') + ? vmLocalUserName + : domainJoinUserName +var varStorageToDomainScriptArgs = '-DscPath ${dscAgentPackageLocation} -StorageAccountName ${storageAccountName} -StorageAccountRG ${storageObjectsRgName} -StoragePurpose ${storagePurpose} -DomainName ${identityDomainName} -IdentityServiceProvider ${identityServiceProvider} -AzureCloudEnvironment ${varAzureCloudName} -SubscriptionId ${subId} -AdminUserName ${varAdminUserName} -CustomOuPath ${storageCustomOuPath} -OUName ${storageOuPath} -ShareName ${fileShareName} -ClientId ${managedIdentityClientId} -SecurityPrincipalName "${varSecurityPrincipalName}" -StorageAccountFqdn ${storageAccountFqdn} ' var varDiagnosticSettings = !empty(alaWorkspaceResourceId) ? [ { @@ -122,28 +127,35 @@ var varDiagnosticSettings = !empty(alaWorkspaceResourceId) } ] : [] + // =========== // // Deployments // // =========== // // Call on the KV. resource avdWrklKeyVaultget 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = { - name: wrklKvName - scope: resourceGroup('${workloadSubsId}', '${serviceObjectsRgName}') + name: varKeyVaultName + scope: resourceGroup('${varKeyVaultSubId}', '${varKeyVaultRgName}') } // Provision the storage account and Azure Files. module storageAndFile '../../../../avm/1.0.0/res/storage/storage-account/main.bicep' = { - scope: resourceGroup('${workloadSubsId}', '${storageObjectsRgName}') + scope: resourceGroup('${subId}', '${storageObjectsRgName}') name: 'Storage-${storagePurpose}-${time}' params: { name: storageAccountName location: location skuName: storageSku allowBlobPublicAccess: false - publicNetworkAccess: deployPrivateEndpoint ? 'Disabled' : 'Enabled' - kind: ((storageSku == 'Premium_LRS') || (storageSku == 'Premium_ZRS')) ? 'FileStorage' : 'StorageV2' - largeFileSharesState: (storageSku == 'Standard_LRS') || (storageSku == 'Standard_ZRS') ? 'Enabled' : 'Disabled' + publicNetworkAccess: deployPrivateEndpoint + ? 'Disabled' + : 'Enabled' + kind: (storageSku == 'Premium_LRS' || storageSku == 'Premium_ZRS') + ? 'FileStorage' + : 'StorageV2' + largeFileSharesState: (storageSku == 'Standard_LRS' || storageSku == 'Standard_ZRS') + ? 'Enabled' + : 'Disabled' azureFilesIdentityBasedAuthentication: identityServiceProvider != 'EntraID' ? { directoryServiceOptions: identityServiceProvider == 'EntraDS' @@ -168,7 +180,7 @@ module storageAndFile '../../../../avm/1.0.0/res/storage/storage-account/main.bi defaultAction: 'Deny' virtualNetworkRules: [ { - id: vmsSubnetId + id: vmsSubnetResourceId action: 'Allow' } ] @@ -178,7 +190,7 @@ module storageAndFile '../../../../avm/1.0.0/res/storage/storage-account/main.bi shares: [ { name: fileShareName - shareQuota: fileShareQuotaSize * 100 //Portal UI steps scale + shareQuota: fileShareQuotaSize } ] protocolSettings: fileShareMultichannel @@ -196,12 +208,12 @@ module storageAndFile '../../../../avm/1.0.0/res/storage/storage-account/main.bi ? [ { name: varWrklStoragePrivateEndpointName - subnetResourceId: privateEndpointSubnetId + subnetResourceId: privateEndpointSubnetResourceId customNetworkInterfaceName: 'nic-01-${varWrklStoragePrivateEndpointName}' service: 'file' - privateDnsZoneGroupName: split(vnetPrivateDnsZoneFilesId, '/')[8] + privateDnsZoneGroupName: split(privateDnsZoneFilesResourceId, '/')[8] privateDnsZoneResourceIds: [ - vnetPrivateDnsZoneFilesId + privateDnsZoneFilesResourceId ] } ] @@ -212,8 +224,8 @@ module storageAndFile '../../../../avm/1.0.0/res/storage/storage-account/main.bi } // Custom Extension call in on the DSC script to join Azure storage account to domain. -module addShareToDomainScript './.bicep/azureFilesDomainJoin.bicep' = if (identityServiceProvider != 'EntraID') { - scope: resourceGroup('${workloadSubsId}', '${serviceObjectsRgName}') +module addShareToDomainScript '../sharedModules/smbUtilities.bicep' = if (identityServiceProvider != 'EntraID') { + scope: resourceGroup('${subId}', '${serviceObjectsRgName}') name: 'Add-${storagePurpose}-Storage-Setup-${time}' params: { location: location @@ -233,4 +245,5 @@ module addShareToDomainScript './.bicep/azureFilesDomainJoin.bicep' = if (identi // =========== // // Outputs // // =========== // + output storageAccountResourceId string = storageAndFile.outputs.resourceId diff --git a/workload/portal-ui/brownfield/portalUiNewSessionHosts.json b/workload/portal-ui/brownfield/portalUiNewSessionHosts.json index 4b6d5cf12..a287f9b38 100644 --- a/workload/portal-ui/brownfield/portalUiNewSessionHosts.json +++ b/workload/portal-ui/brownfield/portalUiNewSessionHosts.json @@ -82,6 +82,120 @@ } ] }, + { + "name": "identity", + "label": "Identity", + "visible": true, + "elements": [ + { + "name": "identityServiceProvider", + "type": "Microsoft.Common.OptionsGroup", + "label": "Identity Service Provider", + "defaultValue": "Active Directory (AD DS)", + "toolTip": "Choose the identity service provider for your users and session hosts. Your choices explained:

Active Directory (ADDS): The users are sourced from ADDS and session hosts will be domain joined.

Microsoft Entra Domain Services: The users are sourced from Entra ID or ADDS and the session hosts are joined to Entra Domain Services.

Microsoft Entra ID: The users are sourced from Entra ID and the session hosts will join Entra ID.

Microsoft Entra ID Kerberos: The Users are sourced from ADDS, but the session hosts will be joined to Entra ID.", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "Active Directory (AD DS)", + "value": "ADDS" + }, + { + "label": "Microsoft Entra Domain Services", + "value": "EntraDS" + }, + { + "label": "Microsoft Entra ID", + "value": "EntraID" + }, + { + "label": "Microsoft Entra ID Kerberos", + "value": "EntraIDKerberos" + } + ] + } + }, + { + "name": "identityServiceProviderInfo1", + "type": "Microsoft.Common.InfoBox", + "visible": "[equals(steps('identity').identityServiceProvider, 'EntraID')]", + "options": { + "text": "If you are deploying this solution with FSLogix Storage, storage account key access must be permitted within your subscription. This solution will securely configure your session host to access the FSLogix Azure Files storage with the storage account key.", + "uri": "https://techcommunity.microsoft.com/blog/fslogix-blog/fslogix-profile-containers-for-azure-ad-cloud-only-identities/3739352", + "style": "Warning" + } + }, + { + "name": "intuneEnrollment", + "type": "Microsoft.Common.CheckBox", + "visible": "[contains(steps('identity').identityServiceProvider, 'EntraID')]", + "label": "Intune enrollment", + "toolTip": "If Intune is configured in your Microsoft Entra ID tenant, you can choose to have the VM automatically enrolled during the deployment by selecting this box." + }, + { + "name": "identityDomainName", + "type": "Microsoft.Common.TextBox", + "label": "Domain name", + "visible": "[contains(steps('identity').identityServiceProvider, 'DS')]", + "toolTip": "The full qualified domain name of the domain to which the virtual machines will be joined.", + "placeholder": "Example: contoso.com", + "constraints": { + "required": true + } + }, + { + "name": "ouPath", + "type": "Microsoft.Common.TextBox", + "visible": "[contains(steps('identity').identityServiceProvider, 'DS')]", + "label": "OU Path", + "toolTip": "Optionally, input the distinguished name of the desired organization unit for the AVD session hosts.", + "placeholder": "Example: OU=pooled,OU=avd,DC=contoso,DC=com", + "constraints": { + "required": false + } + }, + { + "name": "domainJoinUserPrincipalName", + "type": "Microsoft.Common.TextBox", + "placeholder": "domainjoin@contoso.com", + "label": "Domain Join User Principal Name", + "visible": "[contains(steps('identity').identityServiceProvider, 'DS')]", + "toolTip": "Provide username with permissions to join session host to the domain, the expected format is @.", + "constraints": { + "regex": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", + "required": true + } + }, + { + "name": "secretsInfoBox1", + "type": "Microsoft.Common.InfoBox", + "visible": "[contains(steps('identity').identityServiceProvider, 'EntraID')]", + "options": { + "text": "This add-on requires that the following secret names and values are stored in a key vault that you will select below:
  • vmLocalUserName: The virtual machine administrator username.
  • vmLocalUserPassword: The virtual machine administrator password.", + "style": "Info" + } + }, + { + "name": "secretsInfoBox2", + "type": "Microsoft.Common.InfoBox", + "visible": "[contains(steps('identity').identityServiceProvider, 'DS')]", + "options": { + "text": "This add-on requires that the following secret names and values are stored in a key vault that you will select below:
    • domainJoinUserPassword: The password associated with the user principal name specified above.
    • vmLocalUserName: The virtual machine administrator username.
    • vmLocalUserPassword: The virtual machine administrator password.", + "style": "Info" + } + }, + { + "name": "keyVault", + "type": "Microsoft.Solutions.ResourceSelector", + "label": "Select Keyvault containing workload secrets", + "resourceType": "Microsoft.KeyVault/vaults", + "toolTip": "Select Keyvault which contains the secrets", + "constraints": { + "required": true + } + } + ] + }, { "name": "sessionHosts", "label": "Session Hosts", @@ -164,7 +278,7 @@ "defaultValue": 1, "toolTip": "Select the start number of the virtual machine suffix.", "min": 1, - "max": 4998, + "max": 9998, "showStepMarkers": true, "constraints": { "required": true @@ -177,7 +291,7 @@ "defaultValue": 1, "toolTip": "Select the number of virtual machines to deploy in your AVD host pool.", "min": 1, - "max": 4999, + "max": 1999, "showStepMarkers": true, "constraints": { "required": true @@ -252,7 +366,7 @@ "defaultValue": "win11-24h2-avd-m365", "toolTip": "Select the desired marketplace image SKU.", "constraints": { - "allowedValues": "[map(filter(steps('sessionHosts').image.skusApi, (sku) => and(startsWith(sku.name, 'win'), not(contains(sku.name, 'entn')), or(contains(sku.name, 'ent'), contains(sku.name, 'avd')))), (sku) => parse(concat('{\"label\":\"', sku.name, '\",\"value\":\"', sku.name, '\"}')))]", + "allowedValues": "[map(filter(steps('sessionHosts').image.skusApi, (sku) => and(startsWith(sku.name, 'win'), not(contains(sku.name, 'ent')), or(contains(sku.name, 'ent'), contains(sku.name, 'avd')))), (sku) => parse(concat('{\"label\":\"', sku.name, '\",\"value\":\"', sku.name, '\"}')))]", "required": true }, "visible": "[equals(steps('sessionHosts').image.source, 'marketplace')]" @@ -506,238 +620,82 @@ ] }, { - "name": "security", - "type": "Microsoft.Common.Section", - "label": "Other Security Settings", - "elements": [ - { - "name": "asg", - "type": "Microsoft.Solutions.ArmApiControl", - "request": { - "method": "GET", - "path": "[concat(steps('basics').resourceGroup.id, '/providers/Microsoft.Network/applicationSecurityGroups?api-version=2024-05-01')]" - } - }, - { - "name": "encryptionAtHost", - "type": "Microsoft.Common.CheckBox", - "label": "Encryption At Host", - "toolTip": "Check to enable Encryption At Host", - "defaultValue": "[if(equals(steps('basics').encryptionAtHostFeatureApi.properties.state, 'Registered'), true, false)]", - "visible": "[equals(steps('basics').encryptionAtHostFeatureApi.properties.state, 'Registered')]" - }, - { - "name": "customerManagedKeys", - "type": "Microsoft.Common.CheckBox", - "label": "Enable Customer-Managed Keys on OS Disk", - "defaultValue": "[if(empty(filter(steps('basics').diskEncryptionSetsApi.value, (des) => equals(des.tags.cm-resource-parent, steps('basics').hostPool.id))), false, true)]", - "toolTip": "Enables a zero trust configuration on the session host disks.", - "visible": "[not(empty(steps('basics').diskEncryptionSetsApi.value))]" - }, - { - "name": "diskEncryptionSet", - "type": "Microsoft.Common.DropDown", - "defaultValue": "[first(map(filter(steps('basics').diskEncryptionSetsApi.value, (des) => equals(des.tags.cm-resource-parent, steps('basics').hostPool.id)), (des) => des.name))]", - "label": "Disk Encryption Set", - "constraints": { - "allowedValues": "[steps('basics').diskEncryptionSetsApi.transformed.list]", - "required": true - }, - "visible": "[and(not(empty(steps('basics').diskEncryptionSetsApi.value)), steps('sessionHosts').security.customerManagedKeys)]" - }, - { - "name": "deployAntiMalwareExt", - "type": "Microsoft.Common.CheckBox", - "label": "Deploy Anti-Malware Extension", - "toolTip": "Deploys anti-malware extension on session hosts." - } - ] + "name": "asg", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('basics').resourceGroup.id, '/providers/Microsoft.Network/applicationSecurityGroups?api-version=2024-05-01')]" + } }, { - "name": "identityAndAccounts", - "type": "Microsoft.Common.Section", - "label": "Identity and Account Settings", - "elements": [ - { - "name": "identityServiceProvider", - "type": "Microsoft.Common.OptionsGroup", - "visible": true, - "label": "Identity Service Provider", - "defaultValue": "Active Directory (AD DS)", - "toolTip": "Choose the identity service provider for your users and session hosts. Your choices explained:

      Active Directory (ADDS): The users are sourced from ADDS and session hosts will be domain joined.

      Microsoft Entra Domain Services: The users are sourced from Entra ID or ADDS and the session hosts are joined to Entra Domain Services.

      Microsoft Entra ID: The users are sourced from Entra ID and the session hosts will join Entra ID.

      Microsoft Entra ID Kerberos: The Users are sourced from ADDS, but the session hosts will be joined to Entra ID.", - "constraints": { - "required": true, - "allowedValues": [ - { - "label": "Active Directory (AD DS)", - "value": "ADDS" - }, - { - "label": "Microsoft Entra Domain Services", - "value": "EntraDS" - }, - { - "label": "Microsoft Entra ID", - "value": "EntraID" - }, - { - "label": "Microsoft Entra ID Kerberos", - "value": "EntraIDKerberos" - } - ] - } - }, - { - "name": "identityServiceProviderInfo1", - "type": "Microsoft.Common.InfoBox", - "visible": "[equals(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'EntraID')]", - "options": { - "text": "If you are deploying this solution with FSLogix Storage, storage account key access must be permitted within your subscription. This solution will securely configure your session host to access the FSLogix Azure Files storage with the storage account key.", - "uri": "https://techcommunity.microsoft.com/blog/fslogix-blog/fslogix-profile-containers-for-azure-ad-cloud-only-identities/3739352", - "style": "Warning" - } - }, - { - "name": "intuneEnrollment", - "type": "Microsoft.Common.CheckBox", - "visible": "[contains(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'EntraID')]", - "label": "Intune enrollment", - "toolTip": "If Intune is configured in your Microsoft Entra ID tenant, you can choose to have the VM automatically enrolled during the deployment by selecting this box." - }, - { - "name": "identityDomainName", - "type": "Microsoft.Common.TextBox", - "label": "Domain name", - "visible": "[contains(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'DS')]", - "toolTip": "The full qualified domain name of the domain to which the virtual machines will be joined.", - "placeholder": "Example: contoso.com", - "constraints": { - "required": true - } - }, - { - "name": "ouPath", - "type": "Microsoft.Common.TextBox", - "visible": "[contains(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'DS')]", - "label": "OU Path", - "toolTip": "Optionally, input the distinguished name of the desired organization unit for the AVD session hosts.", - "placeholder": "Example: OU=pooled,OU=avd,DC=contoso,DC=com", - "constraints": { - "required": false - } - }, - { - "name": "domainJoinUserPrincipalName", - "type": "Microsoft.Common.TextBox", - "placeholder": "domainjoin@contoso.com", - "label": "Domain Join User Principal Name", - "toolTip": "The User Principal Name of the domain join account.", - "constraints": { - "regex": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", - "required": true - }, - "visible": "[contains(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'DS')]" - }, - { - "name": "secretsInfoBox1", - "type": "Microsoft.Common.InfoBox", - "visible": "[contains(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'EntraID')]", - "options": { - "text": "This add-on requires that the following secret names and values are stored in a key vault that you will select below:
      • vmLocalUserName: The virtual machine administrator username.
      • vmLocalUserPassword: The virtual machine administrator password.", - "style": "Info" - } - }, - { - "name": "secretsInfoBox2", - "type": "Microsoft.Common.InfoBox", - "visible": "[contains(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'DS')]", - "options": { - "text": "This add-on requires that the following secret names and values are stored in a key vault that you will select below:
        • domainJoinUserPassword: The password associated with the user principal name specified above.
        • vmLocalUserName: The virtual machine administrator username.
        • vmLocalUserPassword: The virtual machine administrator password.", - "style": "Info" - } - }, - { - "name": "keyVault", - "type": "Microsoft.Solutions.ResourceSelector", - "label": "Select Keyvault containing workload secrets", - "resourceType": "Microsoft.KeyVault/vaults", - "toolTip": "Select Keyvault which contains the secrets", - "constraints": { - "required": true - } - } - ] + "name": "encryptionAtHost", + "type": "Microsoft.Common.CheckBox", + "label": "Encryption At Host", + "toolTip": "Check to enable Encryption At Host", + "defaultValue": "[if(equals(steps('basics').encryptionAtHostFeatureApi.properties.state, 'Registered'), true, false)]", + "visible": "[equals(steps('basics').encryptionAtHostFeatureApi.properties.state, 'Registered')]" }, { - "name": "network", - "type": "Microsoft.Common.Section", - "label": "Network", - "elements": [ - { - "name": "virtualNetwork", - "type": "Microsoft.Solutions.ResourceSelector", - "label": "Virtual Network", - "resourceType": "Microsoft.Network/virtualNetworks", - "constraints": { - "required": true - }, - "scope": { - "subscriptionId": "[steps('basics').resourceScope.subscription.subscriptionId]", - "location": "[steps('basics').resourceScope.location.name]" - } - }, - { - "name": "subnetsApi", - "condition": "[not(empty(steps('sessionHosts').network.virtualNetwork))]", - "type": "Microsoft.Solutions.ArmApiControl", - "request": { - "method": "GET", - "path": "[concat(steps('sessionHosts').network.virtualNetwork.id, '/subnets?api-version=2022-05-01')]", - "transforms": { - "list": "value|[*].{label:name, value:id}" - } - } - }, - { - "name": "subnet", - "type": "Microsoft.Common.DropDown", - "visible": true, - "label": "Subnet", - "defaultValue": "", - "toolTip": "Select an existing subnet for the AVD session hosts.", - "constraints": { - "required": true, - "allowedValues": "[steps('sessionHosts').network.subnetsApi.transformed.list]" - } - } - ] + "name": "customerManagedKeys", + "type": "Microsoft.Common.CheckBox", + "label": "Enable Customer-Managed Keys on OS Disk", + "defaultValue": "[if(empty(filter(steps('basics').diskEncryptionSetsApi.value, (des) => equals(des.tags.cm-resource-parent, steps('basics').hostPool.id))), false, true)]", + "toolTip": "Enables a zero trust configuration on the session host disks.", + "visible": "[not(empty(steps('basics').diskEncryptionSetsApi.value))]" }, { - "name": "monitoring", - "type": "Microsoft.Common.Section", - "label": "Monitoring", - "elements": [ - { - "name": "enableMonitoring", - "type": "Microsoft.Common.CheckBox", - "label": "Deploy Azure Monitor Agent and Associate Data Collection Rule", - "toolTip": "Deploy AVD monitoring resources and setings." - }, - { - "name": "dataCollectionRule", - "type": "Microsoft.Solutions.ResourceSelector", - "visible": "[steps('sessionHosts').monitoring.enableMonitoring]", - "label": "AVD Insights Data Collection Rule", - "resourceType": "Microsoft.Insights/dataCollectionRules", - "constraints": { - "required": true - }, - "scope": { - "subscriptionId": "[steps('basics').resourceScope.subscription.subscriptionId]", - "location": "[steps('basics').resourceScope.location.name]" - } + "name": "diskEncryptionSet", + "type": "Microsoft.Common.DropDown", + "defaultValue": "[first(map(filter(steps('basics').diskEncryptionSetsApi.value, (des) => equals(des.tags.cm-resource-parent, steps('basics').hostPool.id)), (des) => des.name))]", + "label": "Disk Encryption Set", + "constraints": { + "allowedValues": "[steps('basics').diskEncryptionSetsApi.transformed.list]", + "required": true + }, + "visible": "[and(not(empty(steps('basics').diskEncryptionSetsApi.value)), steps('sessionHosts').customerManagedKeys)]" + } + ] + }, + { + "name": "networking", + "label": "Networking", + "elements": [ + { + "name": "virtualNetwork", + "type": "Microsoft.Solutions.ResourceSelector", + "label": "Virtual Network", + "resourceType": "Microsoft.Network/virtualNetworks", + "constraints": { + "required": true + }, + "scope": { + "subscriptionId": "[steps('basics').resourceScope.subscription.subscriptionId]", + "location": "[steps('basics').resourceScope.location.name]" + } + }, + { + "name": "subnetsApi", + "condition": "[not(empty(steps('networking').virtualNetwork))]", + "type": "Microsoft.Solutions.ArmApiControl", + "request": { + "method": "GET", + "path": "[concat(steps('networking').virtualNetwork.id, '/subnets?api-version=2022-05-01')]", + "transforms": { + "list": "value|[*].{label:name, value:id}" } - ] + } + }, + { + "name": "subnet", + "type": "Microsoft.Common.DropDown", + "visible": true, + "label": "Subnet", + "defaultValue": "", + "toolTip": "Select an existing subnet for the AVD session hosts.", + "constraints": { + "required": true, + "allowedValues": "[steps('networking').subnetsApi.transformed.list]" + } } ] }, @@ -799,6 +757,32 @@ } ] }, + { + "name": "monitoring", + "label": "Monitoring", + "elements": [ + { + "name": "enableMonitoring", + "type": "Microsoft.Common.CheckBox", + "label": "Deploy Azure Monitor Agent and Associate Data Collection Rule", + "toolTip": "Deploy AVD monitoring resources and settings." + }, + { + "name": "dataCollectionRule", + "type": "Microsoft.Solutions.ResourceSelector", + "visible": "[steps('monitoring').enableMonitoring]", + "label": "AVD Insights Data Collection Rule", + "resourceType": "Microsoft.Insights/dataCollectionRules", + "constraints": { + "required": true + }, + "scope": { + "subscriptionId": "[steps('basics').resourceScope.subscription.subscriptionId]", + "location": "[steps('basics').resourceScope.location.name]" + } + } + ] + }, { "name": "tags", "label": "Tags", @@ -1040,19 +1024,19 @@ "securityType": "[if(equals(steps('sessionHosts').image.source, 'computegallery'), if(contains(steps('sessionHosts').image.imageDefinitionApi.transformed.SecurityType, 'TrustedLaunch'), steps('sessionHosts').securityProfile.securityType, 'Standard'), if(or(contains(steps('sessionHosts').image.sku, 'win11'), contains(steps('sessionHosts').image.sku, 'g2')), steps('sessionHosts').securityProfile.securityType, 'Standard'))]", "secureBootEnabled": "[if(equals(steps('sessionHosts').securityProfile.securityType, 'TrustedLaunch'), steps('sessionHosts').securityProfile.secureBootEnabled, false)]", "vTpmEnabled": "[if(equals(steps('sessionHosts').securityProfile.securityType, 'TrustedLaunch'), steps('sessionHosts').securityProfile.vTpmEnabled, false)]", - "asgResourceId": "[if(empty(steps('sessionHosts').security.asg), '', first(map(steps('sessionHosts').security.asg.value, (asg) => asg.id)))]", - "encryptionAtHost": "[if(equals(steps('basics').encryptionAtHostFeatureApi.properties.state, 'Registered'), steps('sessionHosts').security.encryptionAtHost, false)]", - "diskEncryptionSetResourceId": "[if(steps('sessionHosts').security.customerManagedKeys, steps('sessionHosts').security.diskEncryptionSet, '')]", - "deployAntiMalwareExt": "[steps('sessionHosts').security.deployAntiMalwareExt]", - "identityServiceProvider": "[steps('sessionHosts').identityAndAccounts.identityServiceProvider]", - "createIntuneEnrollment": "[steps('sessionHosts').identityAndAccounts.intuneEnrollment]", - "identityDomainName": "[if(contains(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'DS'), steps('sessionHosts').identityAndAccounts.identityDomainName, '')]", - "sessionHostOuPath": "[if(contains(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'DS'), steps('sessionHosts').identityAndAccounts.ouPath, '')]", - "keyVaultResourceId": "[steps('sessionHosts').identityAndAccounts.keyVault.id]", - "domainJoinUserPrincipalName": "[if(contains(steps('sessionHosts').identityAndAccounts.identityServiceProvider, 'DS'), steps('sessionHosts').identityAndAccounts.domainJoinUserPrincipalName, '')]", - "subnetResourceId": "[steps('sessionHosts').network.subnet]", - "enableMonitoring": "[steps('sessionHosts').monitoring.enableMonitoring]", - "dataCollectionRuleId": "[if(steps('sessionHosts').monitoring.enableMonitoring, steps('sessionHosts').monitoring.dataCollectionRule.id, '')]", + "asgResourceId": "[if(empty(steps('sessionHosts').asg), '', first(map(steps('sessionHosts').asg.value, (asg) => asg.id)))]", + "encryptionAtHost": "[if(equals(steps('basics').encryptionAtHostFeatureApi.properties.state, 'Registered'), steps('sessionHosts').encryptionAtHost, false)]", + "diskEncryptionSetResourceId": "[if(steps('sessionHosts').customerManagedKeys, steps('sessionHosts').diskEncryptionSet, '')]", + "deployAntiMalwareExt": "[steps('sessionHosts').settings.enableAntiMalwareExt]", + "identityServiceProvider": "[steps('identity').identityServiceProvider]", + "createIntuneEnrollment": "[if(contains(steps('identity').identityServiceProvider, 'ID'), steps('identity').intuneEnrollment, false)]", + "identityDomainName": "[if(contains(steps('identity').identityServiceProvider, 'DS'), steps('identity').identityDomainName, '')]", + "sessionHostOuPath": "[if(contains(steps('identity').identityServiceProvider, 'DS'), steps('identity').ouPath, '')]", + "keyVaultResourceId": "[steps('identity').keyVault.id]", + "domainJoinUserPrincipalName": "[if(contains(steps('identity').identityServiceProvider, 'DS'), steps('identity').domainJoinUserPrincipalName, '')]", + "subnetResourceId": "[steps('networking').subnet]", + "enableMonitoring": "[steps('monitoring').enableMonitoring]", + "dataCollectionRuleId": "[if(steps('monitoring').enableMonitoring, steps('monitoring').dataCollectionRule.id, '')]", "configureFslogix": "[steps('fslogix').configureFslogix]", "fslogixStorageAccountResourceId": "[if(steps('fslogix').configureFslogix, steps('fslogix').storageAccount, '')]", "fslogixFileShareName": "[if(steps('fslogix').configureFslogix, steps('fslogix').fileShareName, '')]", diff --git a/workload/portal-ui/portal-ui-baseline.json b/workload/portal-ui/portal-ui-baseline.json index df008a163..f0f6a5bd4 100644 --- a/workload/portal-ui/portal-ui-baseline.json +++ b/workload/portal-ui/portal-ui-baseline.json @@ -132,7 +132,6 @@ "name": "identityDomainInformation", "type": "Microsoft.Common.Section", "visible": true, - "label": "Domain to join", "elements": [ { "name": "identityServiceProvider", @@ -288,10 +287,11 @@ "name": "identityDomainJoinUserName", "type": "Microsoft.Common.TextBox", "label": "User principal name", - "toolTip": "Provide username with permissions to join session host to the domain.", + "toolTip": "Provide username with permissions to join session host to the domain, the expected format is @.", "placeholder": "Example: 'avdadmin@contoso.com'", "defaultValue": "", "constraints": { + "regex": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", "required": true } }, @@ -842,7 +842,7 @@ "defaultValue": 1, "toolTip": "Select the start number of the virtual machine suffix.", "min": 1, - "max": 4998, + "max": 9998, "showStepMarkers": true, "constraints": { "required": true @@ -855,7 +855,7 @@ "defaultValue": 1, "toolTip": "Select the number of virtual machines to deploy in your AVD host pool.", "min": 1, - "max": 4999, + "max": 1999, "showStepMarkers": true, "constraints": { "required": true @@ -1111,12 +1111,46 @@ { "name": "identityDomainOuPathStorageExisting", "type": "Microsoft.Common.TextBox", - "visible": "[not(contains(steps('identity').identityDomainInformation.identityServiceProvider, 'EntraID'))]", + "visible": "[or(equals(steps('identity').identityDomainInformation.identityServiceProvider, 'ADDS'), equals(steps('identity').identityDomainInformation.identityServiceProvider, 'EntraDS'))]", "label": "Custom OU path (Optional)", "toolTip": "Provide OU where to locate storage account file share. If not provided, file share will be placed on the default (computers) OU.", "placeholder": "Example: OU=storage,OU=avd,DC=contoso,DC=com", "constraints": {} }, + { + "name": "storageServiceSelector", + "type": "Microsoft.Common.DropDown", + "label": "Storage service", + "visible": "[or(equals(steps('identity').identityDomainInformation.identityServiceProvider, 'ADDS'), equals(steps('identity').identityDomainInformation.identityServiceProvider, 'EntraDS'))]", + "filter": true, + "toolTip": "Select the storage service (Azure Files or ANF) to use for FSLogix and/or App Attach containers. ANF is only available when using AD DS or Microsoft Entra Domain Services identity service providers", + "constraints": { + "required": true, + "allowedValues": [ + { + "label": "AzureFiles", + "description": "", + "value": "AzureFiles" + }, + { + "label": "Azure NetApp Files", + "description": "", + "value": "ANF", + "visible": false + } + ] + } + }, + { + "name": "storageServiceSelectionWarning", + "type": "Microsoft.Common.InfoBox", + "visible": "[not(or(equals(steps('identity').identityDomainInformation.identityServiceProvider, 'ADDS'), equals(steps('identity').identityDomainInformation.identityServiceProvider, 'EntraDS')))]", + "options": { + "text": "Azure NetApp Files is only available when using AD DS or Entra ID Domain Services as the identity service provider. Therefore the FSLogix or App Attach deployments will use Azure Files.", + "uri": "https://learn.microsoft.com/en-us/azure/azure-netapp-files/azure-netapp-files-introduction", + "style": "Warning" + } + }, { "name": "storageApi", "type": "Microsoft.Solutions.ArmApiControl", @@ -1129,6 +1163,7 @@ "name": "storageGeneralSettingsZoneRedundancy", "type": "Microsoft.Common.DropDown", "label": "Storage account type", + "visible": "[equals(steps('storage').storageGeneralSettings.storageServiceSelector, 'AzureFiles')]", "defaultValue": "[if(equals(steps('sessionHosts').sessionHostsRegionSection.sessionHostsAvailabilitySettings, true), 'Zone-Redundant Storage', 'Locally Redundant Storage')]", "toolTip": "Select to replicate storage across availability zones or only use local redundancy.", "constraints": { @@ -1147,7 +1182,8 @@ { "name": "fslogixDeployment", "type": "Microsoft.Common.CheckBox", - "label": "FSLogix profile management", + "label": "Deploy storage", + "visible": true, "defaultValue": true, "toolTip": "Deploys FSLogix containers and session host setup for user's profiles." }, @@ -1155,10 +1191,10 @@ "name": "fslogixStorageAccountSku", "type": "Microsoft.Common.DropDown", "visible": "[steps('storage').storageFslogix.fslogixDeployment]", - "label": "File share peformance", + "label": "File share performance", "filter": true, - "defaultValue": "Premium", - "toolTip": "Storage account performance for FSLogix storage. Recommended tier is Premium.", + "defaultValue": "[if(equals(steps('storage').storageGeneralSettings.storageServiceSelector, 'AzureFiles'), 'Premium', 'Standard')]", + "toolTip": "Performance for App Attach storage (Azure files or ANF). Recommended tier is Premium for storage account and Standard for ANF.", "constraints": { "required": true, "allowedValues": [ @@ -1171,6 +1207,11 @@ "label": "Standard", "description": "", "value": "Standard" + }, + { + "label": "Ultra (ANF only)", + "description": "", + "value": "Ultra" } ] } @@ -1180,10 +1221,10 @@ "type": "Microsoft.Common.Slider", "visible": "[steps('storage').storageFslogix.fslogixDeployment]", "label": "File share size", - "subLabel": "x 100GB", + "subLabel": "GB", "toolTip": "Size of Azure File share quota, the maximum sizes are 5TB for standard SKU and 100TB for premium SKU", - "min": 1, - "max": 100, + "min": 50, + "max": 100000, "defaultValue": 1, "showStepMarkers": true, "constraints": { @@ -1212,7 +1253,7 @@ "name": "appAttachStorageDeployment", "type": "Microsoft.Common.CheckBox", "visible": "[not(contains(steps('basics').resourceScope.location.name, 'china'))]", - "label": "Create App Attach storage", + "label": "Deploy storage", "toolTip": "Deploys App Attach containers and permissions setup." }, { @@ -1221,8 +1262,8 @@ "visible": "[and(equals(steps('storage').storageAppAttach.appAttachStorageDeployment, true), not(contains(steps('basics').resourceScope.location.name, 'china')))]", "label": "File share performance", "filter": true, - "defaultValue": "Premium", - "toolTip": "Storage account performance for App Attach storage. Recommended tier is Premium.", + "defaultValue": "[if(equals(steps('storage').storageGeneralSettings.storageServiceSelector, 'AzureFiles'), 'Premium', 'Standard')]", + "toolTip": "Performance for App Attach storage (Azure files or ANF). Recommended tier is Premium for storage account and Standard for ANF.", "constraints": { "required": true, "allowedValues": [ @@ -1235,6 +1276,11 @@ "label": "Standard", "description": "", "value": "Standard" + }, + { + "label": "Ultra (ANF only)", + "description": "", + "value": "Ultra" } ] } @@ -1244,10 +1290,10 @@ "type": "Microsoft.Common.Slider", "visible": "[and(equals(steps('storage').storageAppAttach.appAttachStorageDeployment, true), not(contains(steps('basics').resourceScope.location.name, 'china')))]", "label": "File share size", - "subLabel": "x 100GB", + "subLabel": "GB", "toolTip": "Size of Azure File share quota, the maximum sizes are 5TB for standard SKU and 100TB for premium SKU", - "min": 1, - "max": 100, + "min": 50, + "max": 100000, "defaultValue": 1, "showStepMarkers": true, "constraints": { @@ -1432,6 +1478,19 @@ "validationMessage": "Invalid CIDR range. The address prefix must be in the range 10 to 24." } }, + { + "name": "virtualNetworkAnfSubnetSize", + "type": "Microsoft.Common.TextBox", + "visible": "[and(steps('network').createAvdVirtualNetwork, equals(steps('storage').storageGeneralSettings.storageServiceSelector, 'ANF'))]", + "label": "Azure NetApp Files subnet address prefix", + "toolTip": "Virtual network subnet CIDR for Azure NetApp Files service", + "placeholder": "Example: 10.10.3.0/26", + "constraints": { + "required": true, + "regex": "^(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(?:\/(1[0-9]|2[0-6]))$", + "validationMessage": "Invalid CIDR range. The address prefix must be in the range 10 to 24." + } + }, { "name": "virtualNetworkDns", "type": "Microsoft.Common.TextBox", @@ -1458,7 +1517,7 @@ "name": "avdVirtualNetworkSelectorId", "type": "Microsoft.Solutions.ResourceSelector", "visible": "[not(steps('network').createAvdVirtualNetwork)]", - "label": "Azure Virtual Desktop virtual network", + "label": "Virtual network", "resourceType": "Microsoft.Network/virtualNetworks", "constraints": { "required": true @@ -1486,7 +1545,24 @@ "type": "Microsoft.Common.DropDown", "visible": "[not(steps('network').createAvdVirtualNetwork)]", "defaultValue": "", - "toolTip": "Select the subnet.", + "toolTip": "Select the AVD subnet.", + "multiselect": false, + "selectAll": false, + "filter": true, + "filterPlaceholder": "Filter items ...", + "multiLine": true, + "constraints": { + "allowedValues": "[map(steps('network').avdSubnetApi.value,(item) => parse(concat('{\"label\":\"', item.name, '\",\"value\":\"', item.id, '\",\"description\":\"', 'Resource Group: ', last(take(split(item.id, '/'), 5)), '\"}')))]", + "required": true + } + }, + { + "name": "virtualNetworkAnfSubnetSelectorName", + "label": "Azure NetApp Files subnet", + "type": "Microsoft.Common.DropDown", + "visible": "[and(not(steps('network').createAvdVirtualNetwork), equals(steps('storage').storageGeneralSettings.storageServiceSelector, 'ANF'))]", + "defaultValue": "", + "toolTip": "Select the Azure NetApp Files subnet, this subnet needs to be delegated to service Microsoft.Netapp/volumes.", "multiselect": false, "selectAll": false, "filter": true, @@ -2759,7 +2835,7 @@ "avdArmServicePrincipalObjectId": "[if(and(equals(steps('identity').identityDomainInformation.identityServiceProvider, 'EntraID'), steps('storage').storageAppAttach.appAttachStorageDeployment), first(map(steps('storage').storageAppAttach.armServicePrincipalPickerBlade.transformed.selection, (sp) => sp.id)), '')]", "deploymentPrefix": "[steps('basics').deploymentSpecs.deploymentPrefix]", "deploymentEnvironment": "[steps('basics').deploymentSpecs.deploymentEnvironment]", - "diskZeroTrust": "[steps('sessionHosts').sessionHostsSettingsSection.sessionHostDiskZeroTrust]", + "storageService": "[if(or(steps('storage').storageFslogix.fslogixDeployment, steps('storage').storageAppAttach.appAttachStorageDeployment), if(or(equals(steps('identity').identityDomainInformation.identityServiceProvider, 'ADDS'), equals(steps('identity').identityDomainInformation.identityServiceProvider, 'EntraDS')), steps('storage').storageGeneralSettings.storageServiceSelector, 'AzureFiles'), '')]", "avdManagementPlaneLocation": "[steps('basics').resourceScope.location.name]", "avdSessionHostLocation": "[if(equals(steps('sessionHosts').deploySessionHosts, true), steps('sessionHosts').sessionHostsRegionSection.sessionHostsRegion, steps('basics').resourceScope.location.name)]", "avdWorkloadSubsId": "[steps('basics').resourceScope.subscription.subscriptionId]", @@ -2781,11 +2857,13 @@ "createAvdVnet": "[steps('network').createAvdVirtualNetwork]", "avdVnetworkAddressPrefixes": "[if(equals(steps('network').createAvdVirtualNetwork, true), steps('network').virtualNetworkSize, '10.10.0.0/16')]", "vNetworkAvdSubnetAddressPrefix": "[if(equals(steps('network').createAvdVirtualNetwork, true), steps('network').virtualNetworkAvdSubnetSize, '10.10.1.0/24')]", + "vNetworkAnfSubnetAddressPrefix": "[if(and(equals(steps('network').createAvdVirtualNetwork, true), equals(steps('storage').storageGeneralSettings.storageServiceSelector, 'ANF')), steps('network').virtualNetworkAnfSubnetSize, '10.10.3.0/26')]", "vNetworkPrivateEndpointSubnetAddressPrefix": "[if(and(equals(steps('network').createAvdVirtualNetwork, true), equals(steps('network').deployPrivateEndpointKeyvaultStorage, true)), steps('network').virtualNetworkPrivateEndpointSubnetSize, '10.10.2.0/27')]", "customDnsIps": "[if(equals(steps('network').createAvdVirtualNetwork, true), steps('network').virtualNetworkDns, '')]", "existingHubVnetResourceId": "[if(equals(steps('network').createAvdVirtualNetwork, true), steps('network').hubVirtualNetworkPeering.existingHubVirtualNetwork, '')]", "vNetworkGatewayOnHub": "[if(equals(steps('network').createAvdVirtualNetwork, true), steps('network').hubVirtualNetworkPeering.hubVirtualNetworkGateway, false)]", "existingVnetAvdSubnetResourceId": "[if(equals(steps('network').createAvdVirtualNetwork, false), steps('network').virtualNetworkAvdSubnetSelectorName, '')]", + "existingVnetAnfSubnetResourceId": "[if(and(equals(steps('network').createAvdVirtualNetwork, false), equals(steps('storage').storageGeneralSettings.storageServiceSelector, 'ANF')), steps('network').virtualNetworkAnfSubnetSelectorName, '')]", "existingVnetPrivateEndpointSubnetResourceId": "[if(equals(steps('network').createAvdVirtualNetwork, false), steps('network').virtualNetworkPrivateEndpointSubnetSelectorName, '')]", "avdDeploySessionHosts": "[steps('sessionHosts').deploySessionHosts]", "avdStartVmOnConnect": "[steps('managementPlane').managementPlaneHostPoolScaling.startVmOnConnect]", diff --git a/workload/scripts/DSCStorageScripts/1.0.3/Configuration.ps1 b/workload/scripts/DSCStorageScripts/1.0.3/Configuration.ps1 index cd600392c..21807734c 100644 --- a/workload/scripts/DSCStorageScripts/1.0.3/Configuration.ps1 +++ b/workload/scripts/DSCStorageScripts/1.0.3/Configuration.ps1 @@ -10,9 +10,13 @@ param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] + [string] $StorageService, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] [string] $StorageAccountName, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $StorageAccountRG, @@ -62,7 +66,7 @@ param [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $StorageAccountFqdn, + [string] $StorageFqdn, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -76,9 +80,13 @@ Configuration DomainJoinFileShare ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] + [string] $StorageService, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] [string] $StorageAccountName, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $StorageAccountRG, @@ -128,7 +136,7 @@ Configuration DomainJoinFileShare [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $StorageAccountFqdn, + [string] $StorageFqdn, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -163,7 +171,7 @@ Configuration DomainJoinFileShare . (Join-Path $using:ScriptPath "Logger.ps1") try { Write-Log "DSC DomainJoinStorage SetScript Domain joining storage account $Using:StorageAccountName" - & "$using:ScriptPath\Script-DomainJoinStorage.ps1" -StorageAccountName $Using:StorageAccountName -StorageAccountRG $Using:StorageAccountRG -SubscriptionId $Using:SubscriptionId -ClientId $Using:ClientId -SecurityPrincipalName $Using:SecurityPrincipalName -ShareName $Using:ShareName -DomainName $Using:DomainName -IdentityServiceProvider $Using:IdentityServiceProvider -AzureCloudEnvironment $Using:AzureCloudEnvironment -CustomOuPath $Using:CustomOuPath -OUName $Using:OUName -StoragePurpose $Using:StoragePurpose -StorageAccountFqdn $Using:StorageAccountFqdn + & "$using:ScriptPath\Script-DomainJoinStorage.ps1" -StorageAccountName $Using:StorageAccountName -StorageAccountRG $Using:StorageAccountRG -SubscriptionId $Using:SubscriptionId -ClientId $Using:ClientId -SecurityPrincipalName $Using:SecurityPrincipalName -ShareName $Using:ShareName -DomainName $Using:DomainName -IdentityServiceProvider $Using:IdentityServiceProvider -AzureCloudEnvironment $Using:AzureCloudEnvironment -CustomOuPath $Using:CustomOuPath -OUName $Using:OUName -StoragePurpose $Using:StoragePurpose -StorageFqdn $Using:StorageFqdn Write-Log "Successfully domain joined and/or NTFS permission set on Storage account" } @@ -217,4 +225,4 @@ $config = @{ ) } -DomainJoinFileShare -ConfigurationData $config -StorageAccountName $StorageAccountName -StorageAccountRG $StorageAccountRG -SubscriptionId $SubscriptionId -ShareName $ShareName -DomainName $DomainName -IdentityServiceProvider $IdentityServiceProvider -AzureCloudEnvironment $AzureCloudEnvironment -CustomOuPath $CustomOuPath -OUName $OUName -AdminUserName $AdminUserName -AdminUserPassword $AdminUserPassword -ClientId $ClientId -SecurityPrincipalName $SecurityPrincipalName -StoragePurpose $StoragePurpose -StorageAccountFqdn $StorageAccountFqdn -Verbose; \ No newline at end of file +DomainJoinFileShare -ConfigurationData $config -StorageService $StorageService -StorageAccountName $StorageAccountName -StorageAccountRG $StorageAccountRG -SubscriptionId $SubscriptionId -ShareName $ShareName -DomainName $DomainName -IdentityServiceProvider $IdentityServiceProvider -AzureCloudEnvironment $AzureCloudEnvironment -CustomOuPath $CustomOuPath -OUName $OUName -AdminUserName $AdminUserName -AdminUserPassword $AdminUserPassword -ClientId $ClientId -SecurityPrincipalName $SecurityPrincipalName -StoragePurpose $StoragePurpose -StorageFqdn $StorageFqdn -Verbose; \ No newline at end of file diff --git a/workload/scripts/DSCStorageScripts/1.0.3/DSCStorageScripts.zip b/workload/scripts/DSCStorageScripts/1.0.3/DSCStorageScripts.zip index 9aeab669f..8d670855d 100644 Binary files a/workload/scripts/DSCStorageScripts/1.0.3/DSCStorageScripts.zip and b/workload/scripts/DSCStorageScripts/1.0.3/DSCStorageScripts.zip differ diff --git a/workload/scripts/DSCStorageScripts/1.0.3/Script-DomainJoinStorage.ps1 b/workload/scripts/DSCStorageScripts/1.0.3/Script-DomainJoinStorage.ps1 index 7b1ec5120..4fd2f09d1 100644 --- a/workload/scripts/DSCStorageScripts/1.0.3/Script-DomainJoinStorage.ps1 +++ b/workload/scripts/DSCStorageScripts/1.0.3/Script-DomainJoinStorage.ps1 @@ -8,9 +8,13 @@ param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] + [string] $StorageService, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] [string] $StorageAccountName, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $StorageAccountRG, @@ -52,7 +56,7 @@ param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $StorageAccountFqdn, + [string] $StorageFqdn, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] @@ -81,7 +85,7 @@ Install-Module -Name Az.Storage -Force Install-Module -Name Az.Network -Force Install-Module -Name Az.Resources -Force -if ($IdentityServiceProvider -eq 'ADDS') { +if ($IdentityServiceProvider -eq 'ADDS' && $StorageService -eq 'AzureFiles') { Write-Log "Installing AzFilesHybrid module" $AzFilesZipLocation = Get-ChildItem -Path $PSScriptRoot -Filter "AzFilesHybrid*.zip" Expand-Archive $AzFilesZipLocation.FullName -DestinationPath $PSScriptRoot -Force @@ -90,7 +94,7 @@ if ($IdentityServiceProvider -eq 'ADDS') { & $AzFilesHybridPath } -if ($IdentityServiceProvider -eq 'ADDS') { +if ($IdentityServiceProvider -eq 'ADDS' && $StorageService -eq 'AzureFiles') { # Please note: ActiveDirectory powershell module is only available on AD joined machines. # To install it, RSAT administrative tools must be installed on the VM which will # install the ActiveDirectory powershell module. AzFilesHybrid module takes care of @@ -114,18 +118,22 @@ Connect-AzAccount -Identity -AccountId $ClientId Write-Log "Setting Azure subscription to $SubscriptionId" Select-AzSubscription -SubscriptionId $SubscriptionId -if ($IdentityServiceProvider -eq 'ADDS') { +if ($IdentityServiceProvider -eq 'ADDS' && $StorageService -eq 'AzureFiles') { Write-Log "Domain joining storage account $StorageAccountName in Resource group $StorageAccountRG" if ( $CustomOuPath -eq 'true') { #Join-AzStorageAccountForAuth -ResourceGroupName $StorageAccountRG -StorageAccountName $StorageAccountName -DomainAccountType 'ComputerAccount' -OrganizationalUnitDistinguishedName $OUName -OverwriteExistingADObject Join-AzStorageAccount -ResourceGroupName $StorageAccountRG -StorageAccountName $StorageAccountName -OrganizationalUnitDistinguishedName $OUName -DomainAccountType 'ComputerAccount' -OverwriteExistingADObject #-SamAccountName $SamAccountName Write-Log -Message "Successfully domain joined the storage account $StorageAccountName to custom OU path $OUName" } - else { - #Join-AzStorageAccountForAuth -ResourceGroupName $StorageAccountRG -StorageAccountName $StorageAccountName -DomainAccountType 'ComputerAccount' -OrganizationalUnitName $OUName -OverwriteExistingADObject - Join-AzStorageAccount -ResourceGroupName $StorageAccountRG -StorageAccountName $StorageAccountName -OrganizationalUnitName $OUName -DomainAccountType 'ComputerAccount' -OverwriteExistingADObject #-SamAccountName $SamAccountName - Write-Log -Message "Successfully domain joined the storage account $StorageAccountName to default OU path $OUName" - } +} + +if ($IdentityServiceProvider -eq 'EntraDS' && $StorageService -eq 'AzureFiles') { + Write-Log "Domain joining storage account $StorageAccountName in Resource group $StorageAccountRG" + if ( $CustomOuPath -eq 'true') { + #Join-AzStorageAccountForAuth -ResourceGroupName $StorageAccountRG -StorageAccountName $StorageAccountName -DomainAccountType 'ComputerAccount' -OrganizationalUnitName $OUName -OverwriteExistingADObject + Join-AzStorageAccount -ResourceGroupName $StorageAccountRG -StorageAccountName $StorageAccountName -OrganizationalUnitName $OUName -DomainAccountType 'ComputerAccount' -OverwriteExistingADObject #-SamAccountName $SamAccountName + Write-Log -Message "Successfully domain joined the storage account $StorageAccountName to default OU path $OUName" + } } if ($StoragePurpose -eq 'fslogix') { @@ -134,33 +142,52 @@ if ($StoragePurpose -eq 'fslogix') { if ($StoragePurpose -eq 'AppAttach') { $DriveLetter = 'X' } -Write-Log "Mounting $StoragePurpose storage account on Drive $DriveLetter" - -$FileShareLocation = '\\' + $StorageAccountFqdn + '\' + $ShareName -$connectTestResult = Test-NetConnection -ComputerName $StorageAccountFqdn -Port 445 - -Write-Log "Test connection access to port 445 for $StorageAccountFqdn was $connectTestResult" - -Try { - Write-Log "Mounting Profile storage $StorageAccountName as a drive $DriveLetter" - if (-not (Get-PSDrive -Name $DriveLetter -ErrorAction SilentlyContinue)) { - $UserStorage = "/user:Azure\$StorageAccountName" - Write-Log "User storage: $UserStorage" - $StorageKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccountRG -AccountName $StorageAccountName) | Where-Object { $_.KeyName -eq "key1" } - Write-Log "File Share location: $FileShareLocation" - net use ${DriveLetter}: $FileShareLocation $UserStorage $StorageKey.Value - #$StorageKey1 = ConvertTo-SecureString $StorageKey.value -AsPlainText -Force - #$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ("Azure\stfsly206dorg", $StorageKey1) - #New-PSDrive -Name $DriveLetter -PSProvider FileSystem -Root $FileShareLocation -Credential $credential +Write-Log "Mounting $StoragePurpose storage on Drive $DriveLetter" + +$FileShareLocation = '\\' + $StorageFqdn + '\' + $ShareName +$connectTestResult = Test-NetConnection -ComputerName $StorageFqdn -Port 445 + +Write-Log "Test connection access to port 445 for $StorageFqdn was $connectTestResult" + +if ($StorageService -eq 'AzureFiles') { + Try { + Write-Log "Mounting Profile storage $StorageAccountName as a drive $DriveLetter" + if (-not (Get-PSDrive -Name $DriveLetter -ErrorAction SilentlyContinue)) { + $UserStorage = "/user:Azure\$StorageAccountName" + Write-Log "User storage: $UserStorage" + $StorageKey = (Get-AzStorageAccountKey -ResourceGroupName $StorageAccountRG -AccountName $StorageAccountName) | Where-Object { $_.KeyName -eq "key1" } + Write-Log "File Share location: $FileShareLocation" + net use ${DriveLetter}: $FileShareLocation $UserStorage $StorageKey.Value + #$StorageKey1 = ConvertTo-SecureString $StorageKey.value -AsPlainText -Force + #$credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ("Azure\stfsly206dorg", $StorageKey1) + #New-PSDrive -Name $DriveLetter -PSProvider FileSystem -Root $FileShareLocation -Credential $credential + } + else { + Write-Log "Drive $DriveLetter already mounted." + } } - else { - Write-Log "Drive $DriveLetter already mounted." + Catch { + Write-Log -Err "Error while mounting profile storage as drive $DriveLetter" + Write-Log -Err $_.Exception.Message + Throw $_ } } -Catch { - Write-Log -Err "Error while mounting profile storage as drive $DriveLetter" - Write-Log -Err $_.Exception.Message - Throw $_ + +if ($StorageService -eq 'ANF') { + Try { + Write-Log "Mounting Profile storage $StorageAccountName as a drive $DriveLetter" + if (-not (Get-PSDrive -Name $DriveLetter -ErrorAction SilentlyContinue)) { + net use ${DriveLetter}: $FileShareLocation $UserStorage $StorageKey.Value + } + else { + Write-Log "Drive $DriveLetter already mounted." + } + } + Catch { + Write-Log -Err "Error while mounting profile storage as drive $DriveLetter" + Write-Log -Err $_.Exception.Message + Throw $_ + } } Try { diff --git a/workload/scripts/Manual-DSC-Storage-Scripts.ps1 b/workload/scripts/Manual-DSC-Storage-Scripts.ps1 index 5916a7148..00c98ccc7 100644 --- a/workload/scripts/Manual-DSC-Storage-Scripts.ps1 +++ b/workload/scripts/Manual-DSC-Storage-Scripts.ps1 @@ -1,13 +1,17 @@ param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $DscPath, + [string] $StorageService, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] + [string] $DscPath, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] [string] $StorageAccountName, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $StorageAccountRG, @@ -57,11 +61,12 @@ param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] - [string] $StorageAccountFqdn, + [string] $StorageFqdn, [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $StoragePurpose + ) if ($IdentityServiceProvider -ne 'EntraIDKerberos') { @@ -133,7 +138,7 @@ function Set-EscapeCharacters { } $AdminUserPasswordEscaped = Set-EscapeCharacters $AdminUserPassword -$DscCompileCommand = "./Configuration.ps1 -StorageAccountName """ + $StorageAccountName + """ -StorageAccountRG """ + $StorageAccountRG + """ -StoragePurpose """ + $StoragePurpose + """ -StorageAccountFqdn """ + $StorageAccountFqdn + """ -ShareName """ + $ShareName + """ -SubscriptionId """ + $SubscriptionId + """ -ClientId """ + $ClientId + """ -SecurityPrincipalName """ + $SecurityPrincipalName + """ -DomainName """ + $DomainName + """ -IdentityServiceProvider """ + $IdentityServiceProvider + """ -AzureCloudEnvironment """ + $AzureCloudEnvironment + """ -CustomOuPath " + $CustomOuPath + " -OUName """ + $OUName + """ -AdminUserName """ + $AdminUserName + """ -AdminUserPassword """ + $AdminUserPasswordEscaped + """ -Verbose" +$DscCompileCommand = "./Configuration.ps1 -StorageService """ + $StorageService + """ -StorageAccountName """ + $StorageAccountName + """ -StorageAccountRG """ + $StorageAccountRG + """ -StoragePurpose """ + $StoragePurpose + """ -StorageFqdn """ + $StorageFqdn + """ -ShareName """ + $ShareName + """ -SubscriptionId """ + $SubscriptionId + """ -ClientId """ + $ClientId + """ -SecurityPrincipalName """ + $SecurityPrincipalName + """ -DomainName """ + $DomainName + """ -IdentityServiceProvider """ + $IdentityServiceProvider + """ -AzureCloudEnvironment """ + $AzureCloudEnvironment + """ -CustomOuPath " + $CustomOuPath + " -OUName """ + $OUName + """ -AdminUserName """ + $AdminUserName + """ -AdminUserPassword """ + $AdminUserPasswordEscaped + """ -Verbose" Write-Host "Executing the command $DscCompileCommand" Invoke-Expression -Command $DscCompileCommand diff --git a/workload/scripts/Set-NtfsPermissions.ps1 b/workload/scripts/Set-NtfsPermissions.ps1 new file mode 100644 index 000000000..393eefb6d --- /dev/null +++ b/workload/scripts/Set-NtfsPermissions.ps1 @@ -0,0 +1,529 @@ +param +( + [Parameter(Mandatory = $false)] + [string]$AdminGroupNames, + + [Parameter(Mandatory = $true)] + [String]$Shares, + + [Parameter(Mandatory = $false)] + [string]$ShardAzureFilesStorage, + + [Parameter(Mandatory = $false)] + [String]$DomainAccountType = "ComputerAccount", + + [Parameter(Mandatory = $true)] + [String]$DomainJoinUserPwd, + + [Parameter(Mandatory = $true)] + [String]$DomainJoinUserPrincipalName, + + [Parameter(Mandatory = $false)] + [ValidateSet("AES256", "RC4")] + [String]$KerberosEncryptionType, + + [Parameter(Mandatory = $false)] + [String]$NetAppServers, + + [Parameter(Mandatory = $false)] + [String]$OuPath, + + [Parameter(Mandatory = $false)] + [string]$ResourceManagerUri, + + [Parameter(Mandatory = $false)] + [String]$StorageAccountPrefix, + + [Parameter(Mandatory = $false)] + [String]$StorageAccountResourceGroupName, + + [Parameter(Mandatory = $false)] + [String]$StorageCount, + + [Parameter(Mandatory = $false)] + [String]$StorageIndex, + + [Parameter(Mandatory = $true)] + [String]$StorageSolution, + + [Parameter(Mandatory = $false)] + [String]$StorageSuffix, + + [Parameter(Mandatory = $false)] + [string]$SubscriptionId, + + [Parameter(Mandatory = $false)] + [String]$UserGroupNames, + + [Parameter(Mandatory = $false)] + [string]$UserAssignedIdentityClientId +) + +$ErrorActionPreference = 'Stop' +$WarningPreference = 'SilentlyContinue' + +[string]$Script:LogDir = "C:\WindowsAzure\Logs\RunCommands" +[string]$Script:Name = 'Set-NTFSPermissions' + +Function ConvertFrom-JsonString { + [CmdletBinding()] + param ( + [string]$JsonString, + [string]$Name, + [switch]$SensitiveValues + ) + If ($JsonString -ne '[]' -and $JsonString -ne $null) { + [array]$Array = $JsonString.replace('\"', '"') | ConvertFrom-Json + If ($Array.Length -gt 0) { + If ($SensitiveValues) { Write-Log -message "Array '$Name' has $($Array.Length) members" } Else { Write-Log -message "$($Name): '$($Array -join "', '")'" } + Return $Array + } + Else { + Return $null + } + } + Else { + Return $null + } +} + +Function Get-FullyQualifiedGroupName { + [CmdletBinding()] + param ( + [Parameter()] + [string]$GroupDisplayName, + [pscredential]$Credential + ) + $Group = $null + $Group = Get-ADGroup -Filter "Name -eq '$groupDisplayName'" -Credential $Credential + If ($null -ne $Group) { + # Extract the domain components from the distinguished name + $domainComponents = ($group.DistinguishedName -split ',') | Where-Object { $_ -like 'DC=*' } + # Construct the domain name + $domainName = ($domainComponents -replace 'DC=', '') -join '.' + # Get the domain information + $domain = Get-ADDomain -Identity $domainName + # Get the NetBIOS name + $netbiosName = $domain.NetBIOSName + # Combine NetBIOS name and group name + $GroupName = "$netbiosName\$($group.SamAccountName)" + Return $GroupName + } + Return $null +} + +function Update-ACL { + Param ( + [Parameter(Mandatory = $false)] + [Array]$AdminGroups, + [Parameter(Mandatory = $true)] + [pscredential]$Credential, + [Parameter(Mandatory = $true)] + [String]$FileShare, + [Parameter(Mandatory = $true)] + [Array]$UserGroups + ) + # Map Drive + Write-Log -message "[Update-ACL]: Mapping Drive to $FileShare" + New-PSDrive -Name 'Z' -PSProvider 'FileSystem' -Root $FileShare -Credential $Credential | Out-Null + # Set recommended NTFS permissions on the file share + Write-Log -message "[Update-ACL]: Getting Existing ACL for $FileShare" + $ACL = Get-Acl -Path 'Z:' + $CreatorOwner = [System.Security.Principal.Ntaccount]("Creator Owner") + Write-Log -message "[Update-ACL]: Purging Existing Access Control Entries for 'Creater Owner' from ACL" + $ACL.PurgeAccessRules($CreatorOwner) + $AuthenticatedUsers = [System.Security.Principal.Ntaccount]("Authenticated Users") + Write-Log -message "[Update-ACL]: Purging Existing Access Control Entries for 'Authenticated Users' from ACL" + $ACL.PurgeAccessRules($AuthenticatedUsers) + $Users = [System.Security.Principal.Ntaccount]("Users") + Write-Log -message "[Update-ACL]: Purging Existing Access Control Entries for 'Users' from ACL" + $ACL.PurgeAccessRules($Users) + If ($AdminGroups.Count -gt 0) { + ForEach ($Group in $AdminGroups) { + Write-Log -message "[Update-ACL]: Adding ACE '$($Group):Full Control' to ACL." + $Ntaccount = [System.Security.Principal.Ntaccount]("$Group") + $ACE = ([System.Security.AccessControl.FileSystemAccessRule]::new("$Ntaccount", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")) + $ACL.SetAccessRule($ACE) + } + } + + ForEach ($Group in $UserGroups) { + Write-Log -message "[Update-ACL]: Adding ACE '$($Group):Modify (This Folder Only)' to ACL." + $Ntaccount = [System.Security.Principal.Ntaccount]("$Group") + $ACE = ([System.Security.AccessControl.FileSystemAccessRule]::new("$Ntaccount", "Modify", "None", "None", "Allow")) + $ACL.SetAccessRule($ACE) + } + + Write-Log -message "[Update-ACL]: Adding ACE 'Creator Owner:Modify (Subfolder and Files Only)' to ACL." + $ACE = ([System.Security.AccessControl.FileSystemAccessRule]::new("$CreatorOwner", "Modify", "ContainerInherit,ObjectInherit", "InheritOnly", "Allow")) + $ACL.SetAccessRule($ACE) + Write-Log -message "[Update-ACL]: Applying the following ACL to $($FileShare):" + Write-Log -message "$($ACL.access | Format-Table | Out-String)" + $ACL | Set-Acl -Path 'Z:' | Out-Null + Start-Sleep -Seconds 5 | Out-Null + $ACL = Get-Acl -Path 'Z:' + Write-Log -message "[Update-ACL]: Current ACL of $($FileShare):" + Write-Log -message "$($ACL.access | Format-Table | Out-String)" + # Unmount file share + Write-Log -message "[Update-ACL]: Unmapping Drive from $FileShare" + Remove-PSDrive -Name 'Z' -PSProvider 'FileSystem' -Force | Out-Null + Start-Sleep -Seconds 5 | Out-Null +} + +function New-Log { + <# + .SYNOPSIS + Sets default log file and stores in a script accessible variable $script:Log + Log File name "packageExecution_$date.log" + + .PARAMETER Path + Path to the log file + + .EXAMPLE + New-Log c:\Windows\Logs + Create a new log file in c:\Windows\Logs + #> + + Param ( + [Parameter(Mandatory = $true, Position = 0)] + [string] $Path + ) + + # Create central log file with given date + + $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" + Set-Variable logFile -Scope Script + $script:logFile = "$Script:Name-$date.log" + + if ((Test-Path $path ) -eq $false) { + $null = New-Item -Path $path -type directory + } + + $script:Log = Join-Path $path $logfile + + Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" +} + +function Write-Log { + + <# + .SYNOPSIS + Creates a log file and stores logs based on categories with tab seperation + + .PARAMETER category + Category to put into the trace + + .PARAMETER message + Message to be loged + + .EXAMPLE + Log 'Info' 'Message' + + #> + + Param ( + [Parameter(Mandatory = $false, Position = 0)] + [ValidateSet("Info", "Warning", "Error")] + $category = 'Info', + [Parameter(Mandatory = $true, Position = 1)] + $message + ) + + $date = get-date + $content = "[$date]`t$category`t`t$message" + Add-Content $Script:Log $content -ErrorAction Stop +} + +try { + + New-Log -Path $Script:LogDir + write-log -message "*** Parameter Values ***" + + # Convert Parameters passed as a JSON String to an array and remove any backslashes + [array]$AdminGroupNames = ConvertFrom-JsonString -JsonString $AdminGroupNames -Name 'AdminGroupNames' + [array]$Shares = ConvertFrom-JsonString -JsonString $Shares -Name 'Shares' + [array]$UserGroupNames = ConvertFrom-JsonString -JsonString $UserGroupNames -Name 'UserGroupNames' + + # Check if the Active Directory module is installed + $RsatInstalled = (Get-WindowsFeature -Name 'RSAT-AD-PowerShell').Installed + if (!$RsatInstalled) { + Install-WindowsFeature -Name 'RSAT-AD-PowerShell' | Out-Null + } + # Create Domain credential + $DomainJoinUserName = $DomainJoinUserPrincipalName.Split('@')[0] + $DomainPassword = ConvertTo-SecureString -String $DomainJoinUserPwd -AsPlainText -Force + [pscredential]$DomainCredential = New-Object System.Management.Automation.PSCredential ($DomainJoinUserName, $DomainPassword) + + # Get Domain information + $Domain = Get-ADDomain -Credential $DomainCredential -Current 'LocalComputer' + Write-Log -message "Domain Information:" + Write-Log -message "DistiguishedName: $($Domain.DistinguishedName)" + Write-Log -message "DNSRoot: $($Domain.DNSRoot)" + Write-Log -message "NetBIOSName: $($Domain.NetBIOSName)" + + # Get the SamAccountName for all the DisplayNames provided. + if ($AdminGroupNames.Count -gt 0) { + [array]$AdminGroups = @() + Write-Log -message "Processing AdminGroupNames by searching AD for Groups with the provided display name and returning the SamAccountName" + ForEach ($DisplayName in $AdminGroupNames) { + Write-Log -message "Processing AdminGroupName: $DisplayName" + $FullyQualifiedGroupName = $null + $FullyQualifiedGroupName = Get-FullyQualifiedGroupName -GroupDisplayName $DisplayName -Credential $DomainCredential + If ($null -ne $FullyQualifiedGroupName) { + Write-Log -message "Found Group: $FullyQualifiedGroupName" + $AdminGroups += $FullyQualifiedGroupName + } + Else { + Write-Log -message "Admin Group not found in Active Directory" + } + } + } + + Write-Log -message "Processing UserGroupNames by searching AD for Groups with the provided display name and returning the SamAccountName" + [array]$UserGroups = @() + ForEach ($DisplayName in $UserGroupNames) { + Write-Log -message "Processing UserGroupName: $DisplayName" + $FullyQualifiedGroupName = $null + $FullyQualifiedGroupName = Get-FullyQualifiedGroupName -GroupDisplayName $DisplayName -Credential $DomainCredential + If ($null -ne $FullyQualifiedGroupName) { + Write-Log -message "Found Group: $FullyQualifiedGroupName" + $UserGroups += $FullyQualifiedGroupName + } + Else { + Write-Log -message "User not found" + } + } + + Switch ($StorageSolution) { + 'AzureFiles' { + Write-Log -message "Processing Azure Files" + # Convert strings to integers + [int]$StCount = $StorageCount.replace('\"', '"') + [int]$StIndex = $StorageIndex.replace('\"', '"') + Write-Log -message "Storage Account Count: $StCount" + Write-Log -message "Storage Account Index: $StIndex" + # Remove any escape characters from strings + $OuPath = $OuPath.Replace('\"', '"') + Write-Log -message "OU Path: $OuPath" + $ResourceManagerUri = $ResourceManagerUri.Replace('\"', '"') + Write-Log -message "ResourceManagerUri: $ResourceManagerUri" + $StorageAccountPrefix = $StorageAccountPrefix.ToLower().replace('\"', '"') + Write-Log -message "Storage Account Prefix: $StorageAccountPrefix" + $StorageAccountResourceGroupName = $StorageAccountResourceGroupName.Replace('\"', '"') + Write-Log -message "Storage Account Resource Group Name: $StorageAccountResourceGroupName" + $SubscriptionId = $SubscriptionId.replace('\"', '"') + Write-Log -message "Subscription Id: $SubscriptionId" + $UserAssignedIdentityClientId = $UserAssignedIdentityClientId.replace('\"', '"') + Write-Log -message "User Assigned Identity Client Id: $UserAssignedIdentityClientId" + # Set the suffix for the Azure Files + $FilesSuffix = ".file.$($StorageSuffix.Replace('\"', '"'))" + Write-Log -message "Files Suffix: $FilesSuffix" + # Fix the resource manager URI since only AzureCloud contains a trailing slash + $ResourceManagerUriFixed = if ($ResourceManagerUri[-1] -eq '/') { $ResourceManagerUri.Substring(0, $ResourceManagerUri.Length - 1) } else { $ResourceManagerUri } + # Get an access token for Azure resources + Write-Log -message "Getting an access token for Azure resources" + $AzureManagementAccessToken = (Invoke-RestMethod ` + -Headers @{Metadata = "true" } ` + -Uri $('http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=' + $ResourceManagerUriFixed + '&client_id=' + $UserAssignedIdentityClientId)).access_token + # Set header for Azure Management API + $AzureManagementHeader = @{ + 'Content-Type' = 'application/json' + 'Authorization' = 'Bearer ' + $AzureManagementAccessToken + } + for ($i = 0; $i -lt $StCount; $i++) { + # Build the Storage Account Name and FQDN + $StorageAccountName = $StorageAccountPrefix + ($i + $StIndex).ToString().PadLeft(2, '0') + Write-Log -message "Processing Storage Account Name: $StorageAccountName" + $FileServer = '\\' + $StorageAccountName + $FilesSuffix + # Get the storage account key + $StorageKey = (Invoke-RestMethod ` + -Headers $AzureManagementHeader ` + -Method 'POST' ` + -Uri $($ResourceManagerUriFixed + '/subscriptions/' + $SubscriptionId + '/resourceGroups/' + $StorageAccountResourceGroupName + '/providers/Microsoft.Storage/storageAccounts/' + $StorageAccountName + '/listKeys?api-version=2023-05-01')).keys[0].value + + # Create credential for accessing the storage account + Write-Log -message "Building Storage Key Credential" + $StorageUsername = 'Azure\' + $StorageAccountName + $StoragePassword = ConvertTo-SecureString -String "$($StorageKey)" -AsPlainText -Force + [pscredential]$StorageKeyCredential = New-Object System.Management.Automation.PSCredential ($StorageUsername, $StoragePassword) + Write-Log -message "Successfully Built Storage Key Credential" + # Get / create kerberos key for Azure Storage Account + Write-Log -message "Getting Kerberos Key for Azure Storage Account" + $KerberosKey = ((Invoke-RestMethod ` + -Headers $AzureManagementHeader ` + -Method 'POST' ` + -Uri $($ResourceManagerUriFixed + '/subscriptions/' + $SubscriptionId + '/resourceGroups/' + $StorageAccountResourceGroupName + '/providers/Microsoft.Storage/storageAccounts/' + $StorageAccountName + '/listKeys?api-version=2023-05-01&$expand=kerb')).keys | Where-Object { $_.Keyname -contains 'kerb1' }).Value + + if (!$KerberosKey) { + Write-Log -message "Kerberos Key not found, Generating a new key" + $null = Invoke-RestMethod ` + -Body (@{keyName = 'kerb1' } | ConvertTo-Json) ` + -Headers $AzureManagementHeader ` + -Method 'POST' ` + -Uri $($ResourceManagerUriFixed + '/subscriptions/' + $SubscriptionId + '/resourceGroups/' + $StorageAccountResourceGroupName + '/providers/Microsoft.Storage/storageAccounts/' + $StorageAccountName + '/regenerateKey?api-version=2023-05-01') + $Key = ((Invoke-RestMethod ` + -Headers $AzureManagementHeader ` + -Method 'POST' ` + -Uri $($ResourceManagerUriFixed + '/subscriptions/' + $SubscriptionId + '/resourceGroups/' + $StorageAccountResourceGroupName + '/providers/Microsoft.Storage/storageAccounts/' + $StorageAccountName + '/listKeys?api-version=2023-05-01&$expand=kerb')).keys | Where-Object { $_.Keyname -contains 'kerb1' }).Value + } + else { + Write-Log -message "Kerberos Key found" + $Key = $KerberosKey + } + # Creates a password for the Azure Storage Account in AD using the Kerberos key + Write-Log -message "Creating a password for the Azure Storage Account in AD using the Kerberos key" + $ComputerPassword = ConvertTo-SecureString -String $Key.Replace("'", "") -AsPlainText -Force + # Create the SPN value for the Azure Storage Account; attribute for computer object in AD + Write-Log -message "Creating the SPN value for the Azure Storage Account" + $SPN = 'cifs/' + $StorageAccountName + $FilesSuffix + # Create the Description value for the Azure Storage Account; attribute for computer object in AD + $Description = "Computer account object for Azure storage account $($StorageAccountName)." + + # Create the AD computer object for the Azure Storage Account + Write-Log -message "Searching for existing computer account object for Azure Storage Account" + $Computer = Get-ADComputer -Credential $DomainCredential -Filter { Name -eq $StorageAccountName } + if ($Computer) { + Write-Log -message "Computer account object for Azure Storage Account found, removing the existing object" + Remove-ADComputer -Credential $DomainCredential -Identity $StorageAccountName -Confirm:$false + } + Else { + Write-Log -message "Computer account object for Azure Storage Account not found" + } + Write-Log -message "Creating the AD computer object for the Azure Storage Account" + $ComputerObject = New-ADComputer -Credential $DomainCredential -Name $StorageAccountName -Path $OuPath -ServicePrincipalNames $SPN -AccountPassword $ComputerPassword -Description $Description -PassThru + # Update the Azure Storage Account with the domain join 'INFO' + Write-Log -message "Updating the Azure Storage Account with the domain join 'INFO'" + $SamAccountName = switch ($KerberosEncryptionType) { + 'AES256' { $StorageAccountName } + 'RC4' { $ComputerObject.SamAccountName } + } + $Body = (@{ + properties = @{ + azureFilesIdentityBasedAuthentication = @{ + activeDirectoryProperties = @{ + accountType = 'Computer' + azureStorageSid = $ComputerObject.SID.Value + domainGuid = $Domain.ObjectGUID.Guid + domainName = $Domain.DNSRoot + domainSid = $Domain.DomainSID.Value + forestName = $Domain.Forest + netBiosDomainName = $Domain.NetBIOSName + samAccountName = $samAccountName + } + directoryServiceOptions = 'AD' + } + } + } | ConvertTo-Json -Depth 6 -Compress) + + $null = Invoke-RestMethod ` + -Body $Body ` + -Headers $AzureManagementHeader ` + -Method 'PATCH' ` + -Uri $($ResourceManagerUriFixed + '/subscriptions/' + $SubscriptionId + '/resourceGroups/' + $StorageAccountResourceGroupName + '/providers/Microsoft.Storage/storageAccounts/' + $StorageAccountName + '?api-version=2023-05-01') + + # Enable AES256 encryption if selected + if ($KerberosEncryptionType -eq 'AES256') { + Write-Log -message "Setting the Kerberos encryption to $KerberosEncryptionType the computer object" + # Set the Kerberos encryption on the computer object + $DistinguishedName = 'CN=' + $StorageAccountName + ',' + $OuPath + Set-ADComputer -Credential $DomainCredential -Identity $DistinguishedName -KerberosEncryptionType 'AES256' | Out-Null + + # Reset the Kerberos key on the Storage Account + Write-Log -message "Resetting the kerb1 key on the Storage Account" + $null = Invoke-RestMethod ` + -Body (@{keyName = 'kerb1' } | ConvertTo-Json) ` + -Headers $AzureManagementHeader ` + -Method 'POST' ` + -Uri $($ResourceManagerUriFixed + '/subscriptions/' + $SubscriptionId + '/resourceGroups/' + $StorageAccountResourceGroupName + '/providers/Microsoft.Storage/storageAccounts/' + $StorageAccountName + '/regenerateKey?api-version=2023-05-01') + + Write-Log -message "Resetting the kerb2 key on the Storage Account" + $null = Invoke-RestMethod ` + -Body (@{keyName = 'kerb2' } | ConvertTo-Json) ` + -Headers $AzureManagementHeader ` + -Method 'POST' ` + -Uri $($ResourceManagerUriFixed + '/subscriptions/' + $SubscriptionId + '/resourceGroups/' + $StorageAccountResourceGroupName + '/providers/Microsoft.Storage/storageAccounts/' + $StorageAccountName + '/regenerateKey?api-version=2023-05-01') + + $Key = ((Invoke-RestMethod ` + -Headers $AzureManagementHeader ` + -Method 'POST' ` + -Uri $($ResourceManagerUriFixed + '/subscriptions/' + $SubscriptionId + '/resourceGroups/' + $StorageAccountResourceGroupName + '/providers/Microsoft.Storage/storageAccounts/' + $StorageAccountName + '/listKeys?api-version=2023-05-01&$expand=kerb')).keys | Where-Object { $_.Keyname -contains 'kerb1' }).Value + + # Update the password on the computer object with the new Kerberos key on the Storage Account + Write-Log -message "Updating the password on the computer object with the new Kerberos key (kerb1) on the Storage Account" + $NewPassword = ConvertTo-SecureString -String $Key -AsPlainText -Force + Set-ADAccountPassword -Credential $DomainCredential -Identity $DistinguishedName -Reset -NewPassword $NewPassword | Out-Null + } + if ($ShardAzureFilesStorage -eq 'true') { + foreach ($Share in $Shares) { + $FileShare = $FileServer + '\' + $Share + $UserGroup = $null + [array]$UserGroup += $UserGroups[$i] + Write-Log -message "Processing File Share: $FileShare with UserGroup = $($UserGroups[$i])" + if ($AdminGroups.Count -gt 0) { + Write-Log -message "Admin Groups provided, executing Update-ACL with Admin Groups" + Update-ACL -AdminGroups $AdminGroups -Credential $StorageKeyCredential -FileShare $FileShare -UserGroups $UserGroup + } + Else { + Write-Log -message "Admin Groups not provided, executing Update-ACL without Admin Groups" + Update-ACL -Credential $StorageKeyCredential -FileShare $FileShare -UserGroups $UserGroup + } + } + } + Else { + foreach ($Share in $Shares) { + $FileShare = $FileServer + '\' + $Share + Write-Log -message "Processing File Share: $FileShare" + if ($AdminGroups.Count -gt 0) { + Write-Log -message "Admin Groups provided, executing Update-ACL with Admin Groups" + Update-ACL -AdminGroups $AdminGroups -Credential $StorageKeyCredential -FileShare $FileShare -UserGroups $UserGroups + } + Else { + Write-Log -message "Admin Groups not provided, executing Update-ACL without Admin Groups" + Update-ACL -Credential $StorageKeyCredential -FileShare $FileShare -UserGroups $UserGroups + } + } + } + } + } + 'AzureNetAppFiles' { + Write-Log -message "Processing Azure NetApp Files" + + [array]$NetAppServers = ConvertFrom-JsonString -JsonString $NetAppServers -Name 'NetAppServers' + + $ProfileShare = "\\$($NetAppServers[0])\$($Shares[0])" + Write-Log -message "Processing Profile Share: $ProfileShare" + if ($AdminGroups.Count -gt 0) { + Write-Log -message "Admin Groups and UserGroups provided, executing Update-ACL with Admin Groups and UserGroups" + Update-ACL -AdminGroups $AdminGroups -Credential $DomainCredential -FileShare $ProfileShare -UserGroups $UserGroups + } + Else { + Write-Log -message "UserGroups provided, executing Update-ACL with UserGroups only" + Update-ACL -Credential $DomainCredential -FileShare $ProfileShare -UserGroups $UserGroups + } + + If ($NetAppServers.Count -gt 1 -and $Shares.Count -gt 1) { + $OfficeShare = "\\" + $NetAppServers[1] + "\" + $Shares[1] + Write-Log -message "Processing Office Share: $OfficeShare" + If ($AdminGroups.Count -gt 0 -and $UserGroups.Count -gt 0) { + Write-Log -message "Admin Groups and UserGroups provided, executing Update-ACL with Admin Groups and UserGroups" + Update-ACL -AdminGroups $AdminGroups -Credential $DomainCredential -FileShare $OfficeShare -UserGroups $UserGroups + } + ElseIf ($AdminGroups.Count -gt 0 -and $UserGroups.Count -eq 0) { + Write-Log -message "Admin Groups provided, executing Update-ACL with Admin Groups only" + Update-ACL -AdminGroups $AdminGroups -Credential $DomainCredential -FileShare $OfficeShare + } + ElseIf ($AdminGroups.Count -eq 0 -and $UserGroups.Count -gt 0) { + Write-Log -message "UserGroups provided, executing Update-ACL with UserGroups only" + Update-ACL -Credential $DomainCredential -FileShare $OfficeShare -UserGroups $UserGroups + } + Else { + Write-Log -message "No Admin Groups or UserGroups provided, executing Update-ACL without Admin Groups or UserGroups" + Update-ACL -Credential $DomainCredential -FileShare $OfficeShare + } + } + } + } +} +catch { + throw +} \ No newline at end of file diff --git a/workload/scripts/Set-SessionHostConfiguration copy.ps1 b/workload/scripts/Set-SessionHostConfiguration copy.ps1 new file mode 100644 index 000000000..916525822 --- /dev/null +++ b/workload/scripts/Set-SessionHostConfiguration copy.ps1 @@ -0,0 +1,502 @@ +Param( + [parameter(Mandatory = $false)] + [string] + $IdentityDomainName, + + [parameter(Mandatory)] + [string] + $AmdVmSize, + + [parameter(Mandatory)] + [string] + $IdentityServiceProvider, + + [parameter(Mandatory)] + [string] + $FSLogix, + + [parameter(Mandatory = $false)] + [string] + $FSLogixStorageAccountKey, + + [parameter(Mandatory = $false)] + [string] + $FSLogixFileShare, + + [parameter(Mandatory)] + [string] + $HostPoolRegistrationToken, + + [parameter(Mandatory)] + [string] + $NvidiaVmSize + + # [parameter(Mandatory)] + # [string] + # $ScreenCaptureProtection +) + +function New-Log { + Param ( + [Parameter(Mandatory = $true, Position = 0)] + [string] $Path + ) + + $date = Get-Date -UFormat "%Y-%m-%d %H-%M-%S" + Set-Variable logFile -Scope Script + $script:logFile = "$Script:Name-$date.log" + + if ((Test-Path $path ) -eq $false) { + $null = New-Item -Path $path -ItemType directory + } + + $script:Log = Join-Path $path $logfile + + Add-Content $script:Log "Date`t`t`tCategory`t`tDetails" +} + +function Write-Log { + Param ( + [Parameter(Mandatory = $false, Position = 0)] + [ValidateSet("Info", "Warning", "Error")] + $Category = 'Info', + [Parameter(Mandatory = $true, Position = 1)] + $Message + ) + + $Date = get-date + $Content = "[$Date]`t$Category`t`t$Message`n" + Add-Content $Script:Log $content -ErrorAction Stop + If ($Verbose) { + Write-Verbose $Content + } + Else { + Switch ($Category) { + 'Info' { Write-Host $content } + 'Error' { Write-Error $Content } + 'Warning' { Write-Warning $Content } + } + } +} + +function Get-WebFile { + param( + [parameter(Mandatory)] + [string]$FileName, + + [parameter(Mandatory)] + [string]$URL + ) + $Counter = 0 + do { + Invoke-WebRequest -Uri $URL -OutFile $FileName -ErrorAction 'SilentlyContinue' + if ($Counter -gt 0) { + Start-Sleep -Seconds 30 + } + $Counter++ + } + until((Test-Path $FileName) -or $Counter -eq 9) +} + +Function Set-RegistryValue { + [CmdletBinding()] + param ( + [Parameter()] + [string] + $Name, + [Parameter()] + [string] + $Path, + [Parameter()] + [string]$PropertyType, + [Parameter()] + $Value + ) + Begin { + Write-Log -message "[Set-RegistryValue]: Setting Registry Value: $Name" + } + Process { + # Create the registry Key(s) if necessary. + If (!(Test-Path -Path $Path)) { + Write-Log -message "[Set-RegistryValue]: Creating Registry Key: $Path" + New-Item -Path $Path -Force | Out-Null + } + # Check for existing registry setting + $RemoteValue = Get-ItemProperty -Path $Path -Name $Name -ErrorAction SilentlyContinue + If ($RemoteValue) { + # Get current Value + $CurrentValue = Get-ItemPropertyValue -Path $Path -Name $Name + Write-Log -message "[Set-RegistryValue]: Current Value of $($Path)\$($Name) : $CurrentValue" + If ($Value -ne $CurrentValue) { + Write-Log -message "[Set-RegistryValue]: Setting Value of $($Path)\$($Name) : $Value" + Set-ItemProperty -Path $Path -Name $Name -Value $Value -Force | Out-Null + } + Else { + Write-Log -message "[Set-RegistryValue]: Value of $($Path)\$($Name) is already set to $Value" + } + } + Else { + Write-Log -message "[Set-RegistryValue]: Setting Value of $($Path)\$($Name) : $Value" + New-ItemProperty -Path $Path -Name $Name -PropertyType $PropertyType -Value $Value -Force | Out-Null + } + Start-Sleep -Milliseconds 500 + } + End { + } +} + +$ErrorActionPreference = 'Stop' +$Script:Name = 'Set-SessionHostConfiguration' +New-Log -Path (Join-Path -Path $env:SystemRoot -ChildPath 'Logs') +try { + + ############################################################## + # Add Recommended AVD Settings + ############################################################## + $Settings = @( + + # Disable Automatic Updates: https://docs.microsoft.com/en-us/azure/virtual-desktop/set-up-customize-master-image#disable-automatic-updates + [PSCustomObject]@{ + Name = 'NoAutoUpdate' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU' + PropertyType = 'DWord' + Value = 1 + }, + + # Enable Time Zone Redirection: https://docs.microsoft.com/en-us/azure/virtual-desktop/set-up-customize-master-image#set-up-time-zone-redirection + [PSCustomObject]@{ + Name = 'fEnableTimeZoneRedirection' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + } + ) + + ############################################################## + # Add GPU Settings + ############################################################## + # This setting applies to the VM Size's recommended for AVD with a GPU + if ($AmdVmSize -eq 'true' -or $NvidiaVmSize -eq 'true') { + $Settings += @( + + # Configure GPU-accelerated app rendering: https://docs.microsoft.com/en-us/azure/virtual-desktop/configure-vm-gpu#configure-gpu-accelerated-app-rendering + [PSCustomObject]@{ + Name = 'bEnumerateHWBeforeSW' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + }, + # Configure fullscreen video encoding: https://docs.microsoft.com/en-us/azure/virtual-desktop/configure-vm-gpu#configure-fullscreen-video-encoding + [PSCustomObject]@{ + Name = 'AVC444ModePreferred' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + }, + [PSCustomObject]@{ + Name = 'KeepAliveEnable' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + }, + [PSCustomObject]@{ + Name = 'KeepAliveInterval' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + }, + [PSCustomObject]@{ + Name = 'MinEncryptionLevel' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 3 + }, + [PSCustomObject]@{ + Name = 'AVCHardwareEncodePreferred' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + } + ) + } + # This setting applies only to VM Size's recommended for AVD with a Nvidia GPU + if ($NvidiaVmSize -eq 'true') { + $Settings += @( + + # Configure GPU-accelerated frame encoding: https://docs.microsoft.com/en-us/azure/virtual-desktop/configure-vm-gpu#configure-gpu-accelerated-frame-encoding + [PSCustomObject]@{ + Name = 'AVChardwareEncodePreferred' + Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + PropertyType = 'DWord' + Value = 1 + } + ) + } + + # ############################################################## + # # Add Screen Capture Protection Setting + # ############################################################## + # if ($ScreenCaptureProtection -eq 'true') { + # $Settings += @( + + # # Enable Screen Capture Protection: https://docs.microsoft.com/en-us/azure/virtual-desktop/screen-capture-protection + # [PSCustomObject]@{ + # Name = 'fEnableScreenCaptureProtect' + # Path = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services' + # PropertyType = 'DWord' + # Value = 1 + # } + # ) + # } + + ############################################################## + # Add Fslogix Settings + ############################################################## + if ($Fslogix -eq 'true') { + $FSLogixStorageFQDN = $FSLogixFileShare.Split('\')[2] + $Settings += @( + # Enables Fslogix profile containers: https://docs.microsoft.com/en-us/fslogix/profile-container-configuration-reference#enabled + [PSCustomObject]@{ + Name = 'Enabled' + Path = 'HKLM:\SOFTWARE\Fslogix\Profiles' + PropertyType = 'DWord' + Value = 1 + }, + # Deletes a local profile if it exists and matches the profile being loaded from VHD: https://docs.microsoft.com/en-us/fslogix/profile-container-configuration-reference#deletelocalprofilewhenvhdshouldapply + [PSCustomObject]@{ + Name = 'DeleteLocalProfileWhenVHDShouldApply' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 1 + }, + # The folder created in the Fslogix fileshare will begin with the username instead of the SID: https://docs.microsoft.com/en-us/fslogix/profile-container-configuration-reference#flipflopprofiledirectoryname + [PSCustomObject]@{ + Name = 'FlipFlopProfileDirectoryName' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 1 + }, + # # Loads FRXShell if there's a failure attaching to, or using an existing profile VHD(X): https://docs.microsoft.com/en-us/fslogix/profile-container-configuration-reference#preventloginwithfailure + # [PSCustomObject]@{ + # Name = 'PreventLoginWithFailure' + # Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + # PropertyType = 'DWord' + # Value = 1 + # }, + # # Loads FRXShell if it's determined a temp profile has been created: https://docs.microsoft.com/en-us/fslogix/profile-container-configuration-reference#preventloginwithtempprofile + # [PSCustomObject]@{ + # Name = 'PreventLoginWithTempProfile' + # Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + # PropertyType = 'DWord' + # Value = 1 + # }, + # List of file system locations to search for the user's profile VHD(X) file: https://docs.microsoft.com/en-us/fslogix/profile-container-configuration-reference#vhdlocations + [PSCustomObject]@{ + Name = 'VHDLocations' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'MultiString' + Value = $FSLogixFileShare + }, + [PSCustomObject]@{ + Name = 'VolumeType' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'String' + Value = 'vhdx' + }, + [PSCustomObject]@{ + Name = 'LockedRetryCount' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 3 + }, + [PSCustomObject]@{ + Name = 'LockedRetryInterval' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 15 + }, + [PSCustomObject]@{ + Name = 'ReAttachIntervalSeconds' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 15 + }, + [PSCustomObject]@{ + Name = 'ReAttachRetryCount' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 3 + } + ) + if ($IdentityServiceProvider -eq "EntraIDKerberos" -and $Fslogix -eq 'true') { + $Settings += @( + [PSCustomObject]@{ + Name = 'CloudKerberosTicketRetrievalEnabled' + Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\Kerberos\Parameters' + PropertyType = 'DWord' + Value = 1 + }, + [PSCustomObject]@{ + Name = 'LoadCredKeyFromProfile' + Path = 'HKLM:\Software\Policies\Microsoft\AzureADAccount' + PropertyType = 'DWord' + Value = 1 + }, + [PSCustomObject]@{ + Name = $IdentityDomainName + Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\Kerberos\domain_realm' + PropertyType = 'String' + Value = $FSLogixStorageFQDN + } + + ) + } + If ($FsLogixStorageAccountKey -ne '') { + $SAName = $FSLogixStorageFQDN.Split('.')[0] + Write-Log -Message "Adding Local Storage Account Key for '$FSLogixStorageFQDN' to Credential Manager" -Category 'Info' + $CMDKey = Start-Process -FilePath 'cmdkey.exe' -ArgumentList "/add:$FSLogixStorageFQDN /user:localhost\$SAName /pass:$FSLogixStorageAccountKey" -Wait -PassThru + If ($CMDKey.ExitCode -ne 0) { + Write-Log -Message "CMDKey Failed with '$($CMDKey.ExitCode)'. Failed to add Local Storage Account Key for '$FSLogixStorageFQDN' to Credential Manager" -Category 'Error' + } + Else { + Write-Log -Message "Successfully added Local Storage Account Key for '$FSLogixStorageFQDN' to Credential Manager" -Category 'Info' + } + $Settings += @( + # Attach the users VHD(x) as the computer: https://learn.microsoft.com/en-us/fslogix/reference-configuration-settings?tabs=profiles#accessnetworkascomputerobject + [PSCustomObject]@{ + Name = 'AccessNetworkAsComputerObject' + Path = 'HKLM:\SOFTWARE\FSLogix\Profiles' + PropertyType = 'DWord' + Value = 1 + } + ) + $Settings += @( + # Disable Roaming the Recycle Bin because it corrupts. https://learn.microsoft.com/en-us/fslogix/reference-configuration-settings?tabs=profiles#roamrecyclebin + [PSCustomObject]@{ + Name = 'RoamRecycleBin' + Path = 'HKLM:\SOFTWARE\FSLogix\Apps' + PropertyType = 'DWord' + Value = 0 + } + ) + # Disable the Recycle Bin + Reg LOAD "HKLM\TempHive" "$env:SystemDrive\Users\Default User\NtUser.dat" + Set-RegistryValue -Path 'HKLM:\TempHive\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer' -Name NoRecycleFiles -PropertyType DWord -Value 1 + Write-Log -Message "Unloading default user hive." + $null = cmd /c REG UNLOAD "HKLM\TempHive" '2>&1' + If ($LastExitCode -ne 0) { + # sometimes the registry doesn't unload properly so we have to perform powershell garbage collection first. + [GC]::Collect() + [GC]::WaitForPendingFinalizers() + Start-Sleep -Seconds 5 + $null = cmd /c REG UNLOAD "HKLM\TempHive" '2>&1' + If ($LastExitCode -eq 0) { + Write-Log -Message "Hive unloaded successfully." + } + Else { + Write-Log -category Error -Message "Default User hive unloaded with exit code [$LastExitCode]." + } + } + Else { + Write-Log -Message "Hive unloaded successfully." + } + } + $LocalAdministrator = (Get-LocalUser | Where-Object { $_.SID -like '*-500' }).Name + $LocalGroups = 'FSLogix Profile Exclude List', 'FSLogix ODFC Exclude List' + ForEach ($Group in $LocalGroups) { + If (-not (Get-LocalGroupMember -Group $Group | Where-Object { $_.Name -like "*$LocalAdministrator" })) { + Add-LocalGroupMember -Group $Group -Member $LocalAdministrator + } + } + } + + ############################################################## + # Add Microsoft Entra ID Join Setting + ############################################################## + if ($IdentityServiceProvider -match "EntraID") { + $Settings += @( + + # Enable PKU2U: https://docs.microsoft.com/en-us/azure/virtual-desktop/troubleshoot-azure-ad-connections#windows-desktop-client + [PSCustomObject]@{ + Name = 'AllowOnlineID' + Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\pku2u' + PropertyType = 'DWord' + Value = 1 + } + ) + } + + # Set registry settings + foreach ($Setting in $Settings) { + Set-RegistryValue -Name $Setting.Name -Path $Setting.Path -PropertyType $Setting.PropertyType -Value $Setting.Value -Verbose + } + + # Resize OS Disk + Write-Log -message "Resizing OS Disk" + $driveLetter = $env:SystemDrive.Substring(0, 1) + $size = Get-PartitionSupportedSize -DriveLetter $driveLetter + Resize-Partition -DriveLetter $driveLetter -Size $size.SizeMax + Write-Log -message "OS Disk Resized" + + ############################################################## + # Add Defender Exclusions for FSLogix + ############################################################## + # https://docs.microsoft.com/en-us/azure/architecture/example-scenario/wvd/windows-virtual-desktop-fslogix#antivirus-exclusions + if ($Fslogix -eq 'true') { + + $Files = @( + "%ProgramFiles%\FSLogix\Apps\frxdrv.sys", + "%ProgramFiles%\FSLogix\Apps\frxdrvvt.sys", + "%ProgramFiles%\FSLogix\Apps\frxccd.sys", + "%TEMP%\*.VHD", + "%TEMP%\*.VHDX", + "%Windir%\TEMP\*.VHD", + "%Windir%\TEMP\*.VHDX" + "$FslogixFileShareName\*.VHD" + "$FslogixFileShareName\*.VHDX" + ) + + foreach ($File in $Files) { + Add-MpPreference -ExclusionPath $File + } + Write-Log -Message 'Enabled Defender exlusions for FSLogix paths' -Category 'Info' + + $Processes = @( + "%ProgramFiles%\FSLogix\Apps\frxccd.exe", + "%ProgramFiles%\FSLogix\Apps\frxccds.exe", + "%ProgramFiles%\FSLogix\Apps\frxsvc.exe" + ) + + foreach ($Process in $Processes) { + Add-MpPreference -ExclusionProcess $Process + } + Write-Log -Message 'Enabled Defender exlusions for FSLogix processes' -Category 'Info' + } + + + ############################################################## + # Install the AVD Agent + ############################################################## + $BootInstaller = 'AVD-Bootloader.msi' + Get-WebFile -FileName $BootInstaller -URL 'https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrxrH' + Start-Process -FilePath 'msiexec.exe' -ArgumentList "/i $BootInstaller /quiet /qn /norestart /passive" -Wait -Passthru + Write-Log -Message 'Installed AVD Bootloader' -Category 'Info' + Start-Sleep -Seconds 5 + + $AgentInstaller = 'AVD-Agent.msi' + Get-WebFile -FileName $AgentInstaller -URL 'https://query.prod.cms.rt.microsoft.com/cms/api/am/binary/RWrmXv' + Start-Process -FilePath 'msiexec.exe' -ArgumentList "/i $AgentInstaller /quiet /qn /norestart /passive REGISTRATIONTOKEN=$HostPoolRegistrationToken" -Wait -PassThru + Write-Log -Message 'Installed AVD Agent' -Category 'Info' + Start-Sleep -Seconds 5 + + ############################################################## + # Restart VM + ############################################################## + if ($IdentityServiceProvider -eq "EntraIDKerberos" -and $AmdVmSize -eq 'false' -and $NvidiaVmSize -eq 'false') { + Start-Process -FilePath 'shutdown' -ArgumentList '/r /t 30' + } +} +catch { + Write-Log -Message $_ -Category 'Error' + throw +} \ No newline at end of file