AVDManagementFramework.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\AVDManagementFramework.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName AVDManagementFramework.Import.DoDotSource -Fallback $false if ($AVDManagementFramework_dotsourcemodule) { $script:doDotSource = $true } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced $importIndividualFiles = Get-PSFConfigValue -FullName AVDManagementFramework.Import.IndividualFiles -Fallback $false if ($AVDManagementFramework_importIndividualFiles) { $importIndividualFiles = $true } if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { . Import-ModuleFile -Path $path } # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions foreach ($function in (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { . Import-ModuleFile -Path $path } # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code <# This file loads the strings documents from the respective language folders. This allows localizing messages and errors. Load psd1 language files for each language you wish to support. Partial translations are acceptable - when missing a current language message, it will fallback to English or another available language. #> Import-PSFLocalizedString -Path "$($script:ModuleRoot)\en-us\*.psd1" -Module 'AVDManagementFramework' -Language 'en-US' function New-AVDMFResourceName { <# .SYNOPSIS This function generates resource names as per the naming convention. .DESCRIPTION The function reads naming conventions and abbreviations from configuration files and outputs resource names to use. .EXAMPLE TODO: Add Examples PS C:\> <example usage> Explanation of what the example does .INPUTS Inputs (if any) .OUTPUTS Output (if any) .NOTES General notes #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = "Does not change any states")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] param ( [Parameter()] [string] $ResourceType, [string] $DeploymentStage = $script:DeploymentStage, [string] $ResourceCategory, [string] $AccessLevel, # Enterprise, Specialist, Privileged [string] $HostPoolType, # Shared, Dedicated [string] $HostPoolInstance, [string] $ParentName, [string] $AddressPrefix, [Int] $InstanceNumber #TODO: Change parameters to overloads so we don't have to provide them. (Except deployment stage?) ) $namingStyle = $script:NamingStyles | Where-Object { $_.ResourceType -eq $ResourceType } if (-not $namingStyle) { $namingStyle = $script:NamingStyles | Where-Object { $_.ResourceType -eq 'Default' } } [array] $nameArray = foreach ($component in $namingStyle.NameComponents) { if ($component -like "*Abv") { $componentName = $component -replace "Abv", "" $componentNC = $component -replace "Abv", "NC" #Check if component naming convention is avaialbe # TODO: Use hashtable for naming conventions instead of dynamic variable names! # Assumption - hashtable stored in $script:namingConvention try { Get-Variable -Name $componentNC -ErrorAction Stop | Out-Null } catch { throw "Could not find a naming convention for component: $componentName. It should be supplied in configuration as .\NamingConvention\Components\$($componentName).json" } # Default or custom abbreviation $componentNCmembers = (get-variable -Name $componentNC).value | Get-Member -MemberType NoteProperty | Where-Object Name -NE $componentName $abbreviationMarker = ($componentNCmembers | Where-Object Name -EQ ("{0}Abv" -f $ResourceType)).Name if (-Not $abbreviationMarker) { $abbreviationMarker = "Abbreviation" } if ($componentName -eq 'Subscription') { $namingConvention = (Get-Variable -Name $componentNC -Scope Script).Value $filterScript = [ScriptBlock]::Create("`$_.DeploymentStage -eq `$DeploymentStage") #$Command = "(`$Script:$componentNC | Where-Object DeploymentStage -contains `$DeploymentStage).$abbreviationMarker" #TODO: Remove me } else { $namingConvention = (Get-Variable -Name $componentNC -Scope Script).Value $filterScript = [ScriptBlock]::Create("`$_.$componentName -eq `$$componentName") #$Command = "(`$Script:$componentNC | Where-Object $componentName -eq `$$componentName).$abbreviationMarker" #TODO: Remove me #TODO: Review with Fred } #FRED: $script:namingConvention[$componentName].$abbreviationMarker #$abv = Invoke-Expression -Command $Command #TODO: Remove me $abv = ($namingConvention | Where-Object -FilterScript $filterScript).$abbreviationMarker if (-not $abv) {throw "Could not find any abbreviation for $componentName`: $((Get-Variable -Name $componentName).Value)" } $abv } if ($component -in ('-', '_')) { $component } if ($component -eq 'ParentName') { $ParentName } if ($component -eq 'AddressPrefix') { $AddressPrefix -replace "/", "-" } if ($component -eq 'HostPoolInstance') { $HostPoolInstance } } $resourceName = $nameArray -join "" -replace "-All", "" -replace "All", "" if ($namingStyle.LowerCase) { $resourceName = $resourceName.ToLower() } if ($namingStyle.NameComponents[-1] -eq 'InstanceNumber') { #TODO: Move this part to the main loop. if ($InstanceNumber) { $resourceName = "{0}{1:D2}" -f $resourceName, $InstanceNumber } else { $scriptResourceType = (Get-Variable -Name "$($ResourceType)s" -Scope Script).Value $filterScript = [ScriptBlock]::Create("`$_ -like `"$resourceName*`"") $count = ($scriptResourceType.Keys | Where-Object -FilterScript $filterScript).Count #TODO: Fix this once we have resource name attribute for all resource if($count -eq 0){ $count = ($scriptResourceType.GetEnumerator() | ForEach-Object{$_.Value.ResourceName} | Where-Object -FilterScript $filterScript).count } $resourceName = "{0}{1:D2}" -f $resourceName, ($Count + 1) } } if ($resourceName.length -gt $namingStyle.MaxLength) { throw "resulting resource name is longer than $($namingStyle.MaxLength) characters '$resourceName'" } $resourceName } function Convert-HashtableToArray { [OutputType('System.Array')] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline=$true )] [Hashtable] $InputObject ) process { $output = foreach ($key in $InputObject.Keys){ $object = @{ Name = $key } $Members = $InputObject[$key] | Get-Member -MemberType NoteProperty # TODO: $hash['h104p01-vnet-pv-01'].psobject.Properties.name $Members | ForEach-Object {$object[$_.Name] = $InputObject[$key].($_.Name)} $object } ,$output # "The comma makes it output an array ALWAYS, that's it" -Fred! } } function Get-AVDMFResourceInfo { [CmdletBinding()] param ( [string] $ResourceId ) $pattern = '^\/subscriptions\/(?<SubscriptionId>.+)\/resourceGroups\/(?<ResourceGroupName>.+)\/providers.+\/(?<ResourceName>.+$)' if($ResourceId -match $pattern){ [PSCustomObject]@{ SubscriptionId = $Matches.SubscriptionId ResourceGroupName = $Matches.ResourceGroupName ResourceName = $Matches.ResourceName } } else {throw "Resource ID is not valid: $ResourceId"} } function Get-RandomPassword { #Link: https://gist.github.com/onlyann/00d9bb09d4b1338ffc88a213509a6caf param( [Parameter(Mandatory = $false)] [ValidateRange(12, 256)] [int] $length = 14 ) $symbols = '!@#$%^&*'.ToCharArray() $characterList = 'a'..'z' + 'A'..'Z' + '0'..'9' + $symbols do { $password = "" for ($i = 0; $i -lt $length; $i++) { $randomIndex = [System.Security.Cryptography.RandomNumberGenerator]::GetInt32(0, $characterList.Length) $password += $characterList[$randomIndex] } [int]$hasLowerChar = $password -cmatch '[a-z]' [int]$hasUpperChar = $password -cmatch '[A-Z]' [int]$hasDigit = $password -match '[0-9]' [int]$hasSymbol = $password.IndexOfAny($symbols) -ne -1 } until (($hasLowerChar + $hasUpperChar + $hasDigit + $hasSymbol) -ge 3) $password #| ConvertTo-SecureString -AsPlainText } function New-AVDMFSubnetRange { [cmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',Justification = "Does not change any states")] param( # Address Space for the subnet, this can be any of the address spaces created under the vNet in the format X.X.X.X/X [Parameter(Mandatory = $true)][string]$AddressSpace, # Mask bits of the new subnet, written as XX (example 27) [Parameter(Mandatory = $true)][int]$NewSubnetMaskBits ) #region: functions function ConvertFrom-DecimalIPtoBinary ([string]$DecimalIPAddress) { #Create an empty variable $Binary = $null #Extract octets from IP Address $Octets = $DecimalIPAddress.Split('.') #Convert each octet to Binary and add to the variable $Binary # Here we use ToString with '2' as the base, 2 means binary # We are also using padleft to make sure each octet is 8 bits long with leading zeros if needed $Octets | ForEach-Object { $Binary += ([convert]::ToString($_, 2)).PadLeft(8, "0") } return $Binary } function ConvertFrom-BinaryIPtoDecimal ([string]$BinaryIPAddress) { #Create an empty string $Decimal = $null #Split Binary address into 4 octets - And convert to decimal # Again, 2 is for the base. $Octets = for ($i = 0; $i -lt 4; $i++) { [convert]::ToInt32($BinaryIPAddress.Substring($i * 8, 8), 2) } # Join the octets into one string with "." as delimeter $Decimal = $Octets -join "." return $Decimal } function Find-IPAddressesInRange ($FirstIPAddress, $LastIPAddress) { # First we confirm the IP Addresses to Binary, then to int64 $Int64IP1 = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $FirstIPAddress), 2) $Int64IP2 = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $LastIPAddress), 2) # Then we just create a loop of all the values in the range of int64 $IPAddresses = for ($i = $Int64IP1; $i -le $Int64IP2; $i++) { #Finally, we convert the int64 to binary then back to a decimal IP Address ConvertFrom-BinaryIPtoDecimal ([convert]::ToString($i, 2)).padleft(32, "0") } return $IPAddresses } function Get-SubnetDetails ($IPAddress, $MaskBits) { $BinaryIPAddress = ConvertFrom-DecimalIPtoBinary $IPAddress $SubnetID = ConvertFrom-BinaryIPtoDecimal $BinaryIPAddress.Substring(0, $MaskBits).PadRight(32, '0') $BroadcastIP = ConvertFrom-BinaryIPtoDecimal $BinaryIPAddress.Substring(0, $MaskBits).PadRight(32, '1') return [PSCustomObject]@{ SubnetID = $SubnetID BroadcastIP = $BroadcastIP AddressPrefix = "$SubnetID/$MaskBits" } } function Test-OverlappingSubnets ($SubnetA, $SubnetB) { $SubnetAIDDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetA.SubnetID), 2) $SubnetABCDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetA.BroadcastIP), 2) $SubnetBIDDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetB.SubnetID), 2) $SubnetBBCDigital = [convert]::Toint64((ConvertFrom-DecimalIPtoBinary $SubnetB.BroadcastIP), 2) if ($SubnetAIDDigital -ge $SubnetBIDDigital -and $SubnetAIDDigital -le $SubnetBBCDigital) { $Overlap = $true } elseif ($SubnetABCDigital -ge $SubnetBIDDigital -and $SubnetABCDigital -le $SubnetBBCDigital) { $Overlap = $true } else { $Overlap = $false } return $Overlap } #endregion: functions #region: Analyzing Address Space Write-Verbose -Message "Analyzing Address Space" #Find the position of '/' in the provided address space. $AddressSpaceIndexOfMaskBits = $AddressSpace.IndexOf("/") $AddressSpaceID = $AddressSpace.Substring(0, $AddressSpaceIndexOfMaskBits) $AddressSpaceMaskBits = $AddressSpace.Substring($AddressSpaceIndexOfMaskBits + 1) Write-Verbose -Message "ID: $AddressSpaceID - MaskbBits: $AddressSpaceMaskBits" $AddressSpaceSize = [math]::Pow(2, 32 - $AddressSpaceMaskBits) Write-Verbose -Message "Address Space Size: $AddressSpaceSize" Write-Verbose -Message "Finished Analyzing Address Space" #endregion: Analyzing Address Space #region: Find all possible subnets Write-Verbose -Message "ENTER: Find all possible subnets" $NewSubnetSize = [math]::Pow(2, (32 - $NewSubnetMaskBits)) Write-Verbose -Message "New Subnet Size: $NewSubnetSize" $NumberOfPossibleSubnets = $AddressSpaceSize / $NewSubnetSize Write-Verbose -Message "Number of Possible Subnets: $NumberOfPossibleSubnets" $PossibleSubnetsArray = @(Get-SubnetDetails $AddressSpaceID $NewSubnetMaskBits) for ($i = 1; $i -lt $NumberOfPossibleSubnets; $i++) { $LastSubnetInt64 = ([convert]::Toint64((ConvertFrom-DecimalIPtoBinary $PossibleSubnetsArray[$i - 1].BroadcastIP), 2)) $NextSubnetID = ConvertFrom-BinaryIPtoDecimal ([convert]::ToString($LastSubnetInt64 + 1, 2)).padleft(32, '0') $PossibleSubnetsArray += Get-SubnetDetails $NextSubnetID $NewSubnetMaskBits } Write-Verbose -Message "Calculated $($PossibleSubnetsArray.Count) possible subnets." Write-Verbose -Message "Exit: Find all possible subnets" #endregion: Find all possible subnets #region: Collect vNet information Write-Verbose -Message "ENTER: Collect vNet information" $vNetSubnets = foreach ($key in $script:Subnets.Keys) { if ((ConvertFrom-DecimalIPtoBinary ($script:Subnets[$key].Properties.AddressPrefix.Substring(0, $script:Subnets[$key].Properties.AddressPrefix.IndexOf("/")))).Substring(0, $AddressSpaceMaskBits) ` -eq (ConvertFrom-DecimalIPtoBinary ($AddressSpaceID)).Substring(0, $AddressSpaceMaskBits)) { $script:Subnets[$key] } } Write-Verbose -Message "Found $($vNetSubnets.count) subnets in the vNet belonging to the address space $AddressSpace" $UtilizedAddressesArray = foreach ($Subnet in $vNetSubnets) { $IndexOfSubnetMask = $Subnet.AddressPrefix.indexOf("/") $SubnetID = $Subnet.AddressPrefix.Substring(0, $IndexOfSubnetMask) $MaskBits = $Subnet.AddressPrefix.Substring($IndexOfSubnetMask + 1) Get-SubnetDetails -IPAddress $SubnetID -MaskBits $MaskBits } Write-Verbose -Message "Calculated utilized addresses" Write-Verbose -Message "Exit: Collect vNet information" #endregion: Collect vNet information #region: Return the first free subnet Write-Verbose -Message "ENTER: Return the first free subnet" foreach ($PossibleSubnet in $PossibleSubnetsArray) { foreach ($ExistingSubnet in $UtilizedAddressesArray) { $Overlap = $false if (Test-OverlappingSubnets $PossibleSubnet $ExistingSubnet) { $Overlap = $true break } } if (!($Overlap)) { return $PossibleSubnet } } Write-Verbose -Message "Exit: Return the first free subnet" #endregion: Return the first free subnet # if we did not return any subnet, throw an error throw "Could not find any free subnets" } function Set-AVDMFNameMapping { <# .SYNOPSIS Takes a dataset and converts any %XXXX% into mapping. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Hashtable] $Dataset ) foreach ($item in ($dataset.GetEnumerator() | Where-Object { $null -ne $_.Value } )){ #if ($null -eq $item.Value) { continue } # Value is null nothing to replace if ($item.Value.GetType().Name -eq 'String'){ $stringMappings = ([regex]::Matches($item.Value, '%.+?%')).Value | ForEach-Object { if ($_) { $_ -replace "%", "" } } foreach ($mapping in $stringMappings) { $mappedValue = $script:NameMappings[$mapping] $item.Value = $item.Value -replace "%$mapping%", $mappedValue } $dataset[$item.Key] = $item.Value } if ($item.Value.GetType().Name -eq 'PSCustomObject') { $dataset[$item.Key] =[PSCustomObject] (Set-AVDMFNameMapping -Dataset ($item.Value | ConvertTo-PSFHashtable)) } } $Dataset } function Set-AVDMFStageEntries { <# .SYNOPSIS This function replaces "Stages" token in json objects depending on the current stage or a default one. .Example $json = @" { "SampleProperty": { "DeploymentStage": { "Development": 10, "Production": 5, "Default": 15 } } } "@ $dataset = $json | ConvertFrom-Json | ConvertTo-PSFHashtable Set-AVDMFStageEntries -Dataset $dataset Assuming the current stage name is "Development", the output will be that SampleProperty = 10 #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Hashtable] $Dataset, [string] $DeploymentStage = $script:DeploymentStage, [string] $StageToken = "DeploymentStage" ) foreach ($key in ([array]$Dataset.Keys)) { if ($null -eq $Dataset[$key]) { continue } if ($Dataset[$key].GetType().Name -eq 'PSCustomObject') { if ($Dataset[$key] | Get-Member -MemberType NoteProperty -Name $StageToken) { # Get list of configured stages under the stage token $configuredStages = ($Dataset[$key].$StageToken | Get-Member -MemberType NoteProperty).Name if ( $configuredStages -contains $DeploymentStage) { $Dataset[$key] = $Dataset[$key].$StageToken.($DeploymentStage) Write-PSFMessage -Level Verbose -Message "Set $key to $DeploymentStage value: $($Dataset[$key])" } elseif ( $configuredStages -contains "Default" ) { $Dataset[$key] = $Dataset[$key].$StageToken.Default Write-PSFMessage -Level Verbose -Message "Set $key to Default value: $($Dataset[$key])" } else { throw "Could not resolve stage value ($DeploymentStage) for `r`n $($Dataset | Out-String)" } } else { # key is a PSCustomObject that does not have a stage token, maybe one of its children. $Dataset[$key] = [PSCustomObject] (Set-AVDMFStageEntries -Dataset ($Dataset[$key] | ConvertTo-PSFHashtable)) } } } $Dataset } function Invoke-AVDMFConfiguration { [CmdletBinding()] param ( ) # Create resource groups Write-PSFMessage -Level Host -Message "Invoking resource groups." foreach ($rg in $script:ResourceGroups.Keys) { $newAzResourceGroup = @{ Name = $rg Location = $script:Location Force = $true } if($script:ResourceGroups[$rg].Tags){ $newAzResourceGroup['Tags'] = $script:ResourceGroups[$rg].Tags } New-AzResourceGroup @newAzResourceGroup } #TODO: Decide if we want to create RGs here or with deployment. decide on parallelism # Create network resources Write-PSFMessage -Level Host -Message "Invoking network resources." Invoke-AVDMFNetwork -ErrorAction Stop #Create storage resources Write-PSFMessage -Level Host -Message "Invoking Storage resources." Invoke-AVDMFStorage -ErrorAction Stop # Create Host Pools and Session Hosts Write-PSFMessage -Level Host -Message "Invoking Desktop Virtualization resources." Invoke-AVDMFDesktopVirtualization -ErrorAction Stop } function Set-AVDMFConfiguration { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = "Does not change any states")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $ConfigurationPath, [string] $AzSubscriptionId = (Get-AzContext).Subscription.Id, [switch] $Force ) #region: Initialize Variables $configurationVersion = '1.0.11' #endregion: Initialize Variables #region: Load Custom Environment Variables $environmentVariablesFilePath = Join-Path -Path $ConfigurationPath -ChildPath 'EnvironmentVariables.json' if (Test-Path -Path $environmentVariablesFilePath) { Write-Warning -Message "EnvironmentVariables.json file detected. This is not supposed to exist on DevOps. Please add it to .gitignore" $environmentVariables = Get-Content -Path $environmentVariablesFilePath | ConvertFrom-Json | ConvertTo-PSFHashtable $null = $environmentVariables.GetEnumerator() | ForEach-Object { New-Item -Path $_.Key -Value $_.Value -Force } } #endregion: Load Custom Environment Variables #region: Set DeploymentStage $script:DeploymentStage = $env:SYSTEM_STAGEDISPLAYNAME if ([string]::IsNullOrEmpty($DeploymentStage) -or [string]::IsNullOrWhiteSpace($DeploymentStage)) { throw "Deployment Stage is not defined, if running from local device create EnvironmentVariables.json file. Otherwise review environment variables." #TODO: Include environment variable name in error message. } #endregion: Set DeploymentStage #region: Register Name Mappings $nameMappingConfigPath = Join-Path -Path $ConfigurationPath -ChildPath "NameMappings" if (Test-Path $nameMappingConfigPath) { foreach ($file in Get-ChildItem -Path $nameMappingConfigPath -Filter "*.json") { foreach ($dataset in (Get-Content -Path $file.FullName | ConvertFrom-Json -ErrorAction Stop | ConvertTo-PSFHashtable )) { Register-AVDMFNameMapping @dataset } } } #endregion: Register Name Mappings #region: Populate Script Variables $script:AzSubscriptionId = $AzSubscriptionId #endregion: Populate Script Variables if ($script:WVDConfigurationLoaded -and -not $Force) { throw "Configuration is already loaded. Use the force to reload." } if ($Force) { & "$moduleRoot\internal\scripts\variables.ps1" } #region: General Configuration $generalConfiguration = Get-Content -Path (Join-Path -Path $ConfigurationPath -ChildPath '\GeneralConfiguration\GeneralConfiguration.json' -ErrorAction Stop ) | ConvertFrom-Json -ErrorAction Stop if($generalConfiguration.ConfigurationVersion -ne $configurationVersion) { throw "current configuration version $($generalConfiguration.ConfigurationVersion) must match $configurationVersion." } $script:Location = $GeneralConfiguration.Location $script:TimeZone = $generalConfiguration.TimeZone $Script:DomainJoinUserName = $generalConfiguration.DomainJoinCredential.SecretName $Script:DomainJoinPassword = Get-AzKeyVaultSecret -ResourceId $generalConfiguration.DomainJoinCredential.KeyVaultID -Name $generalConfiguration.DomainJoinCredential.SecretName -AsPlainText <# $script:DomainJoinCredential = @{ reference = @{ keyVault = @{ id = $generalConfiguration.DomainJoinCredential.KeyVaultID} secretName = $generalConfiguration.DomainJoinCredential.SecretName } } #> #endregion #region: Naming Conventions $namingConventionsRoot = Join-Path -Path $ConfigurationPath -ChildPath NamingConventions $script:NamingStyles = Get-Content -Path $namingConventionsRoot\NamingStyles.json -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop $namingConventionsComponentsRoot = Join-Path -Path $namingConventionsRoot -ChildPath "Components" foreach ($componentNC in (Get-ChildItem -Path $namingConventionsComponentsRoot -Filter "*.json")) { # We create a script variable for each component by adding 'NC' to the name of the file $NCContent = Get-Content -Path $componentNC.FullName -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop New-Variable -Scope 'script' -Name ("{0}NC" -f $componentNC.BaseName) -Value $NCContent } #endregion: Naming Conventions #region: Define Registrable Components $components = [ordered] @{ # Tags 'GlobalTags' = @{Command = (Get-Command -Name Register-AVDMFGlobalTag); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "GlobalTags") } # Network 'AddressSpaces' = @{Command = (Get-Command Register-AVDMFAddressSpace); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\AddressSpaces") } 'VirtualNetworks' = @{Command = (Get-Command Register-AVDMFVirtualNetwork); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\VirtualNetworks") } 'RouteTables' = @{Command = (Get-Command Register-AVDMFRouteTable); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\RouteTables") } 'NetworkSecurityGroups' = @{Command = (Get-Command Register-AVDMFNetworkSecurityGroup); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Network\NetworkSecurityGroups") } # Storage 'StorageAccounts' = @{Command = (Get-Command Register-AVDMFStorageAccount); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "Storage\StorageAccounts") } # Desktop Virtualization 'Workspaces' = @{Command = (Get-Command Register-AVDMFWorkspace); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\Workspaces") } 'VMTemplates' = @{Command = (Get-Command Register-AVDMFVMTemplate); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\VMTemplates") } 'HostPools' = @{Command = (Get-Command Register-AVDMFHostPool); ConfigurationPath = (Join-Path -Path $ConfigurationPath -ChildPath "DesktopVirtualization\HostPools") } } #endregion: Define Registrable Components #region: Load Component Configuration foreach ($key in $components.Keys) { if (-not (Test-Path $components[$key].ConfigurationPath)) { continue } Write-PSFMessage -Level Verbose -Message "Loading configuration for $key" foreach ($file in Get-ChildItem -Path $components[$key].ConfigurationPath -Recurse -Filter "*.json") { Write-PSFMessage -Level Verbose -Message "`tLoading $key from $($file.FullName)" foreach ($dataset in (Get-Content -Path $file.FullName | ConvertFrom-Json -ErrorAction Stop | ConvertTo-PSFHashtable -Include $($components[$key].Command.Parameters.Keys))) { Write-PSFMessage -Level Verbose -Message "`t`tRegistering dataset:`r`n $($dataset | Format-List | Out-String -Width 120)" $dataset = Set-AVDMFNameMapping -Dataset $dataset $dataset = Set-AVDMFStageEntries -Dataset $dataset & $components[$key].Command @dataset -ErrorAction Stop } } } #endregion: Load Component Configuration #region: Add Tags $taggedResources = @( 'ResourceGroup' 'VirtualNetwork' 'NetworkSecurityGroup' 'RouteTable' 'StorageAccount' 'PrivateLink' 'HostPool' 'ApplicationGroup' 'Workspace' 'SessionHost' ) foreach ($resourceType in $taggedResources) { #if($resourceType -eq 'RouteTable' ) {$BP="HERE"} $scriptVariable = Get-Variable -Scope script -Name "$($resourceType)s" -ValueOnly if (($script:GlobalTags.keys -contains $resourceType) -or ($script:GlobalTags.keys -contains 'All')) { $keys = [array] $scriptVariable.Keys foreach ($key in $keys) { $scriptVariable[$key] = Add-AVDMFTag -ResourceType $resourceType -ResourceObject $scriptVariable[$key] } } } #endregion: Add Tags $script:WVDConfigurationLoaded = $true } function Initialize-AVDMFDesktopVirtualization { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] [OutputType('System.Collections.Hashtable')] param ( [string] $ResourceGroupName, [string] $ResourceCategory ) switch ($ResourceCategory) { 'HostPool' { $filteredHostPools = @{} $script:HostPools.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredHostPools.Add($_.Key, $_.Value) } $filteredApplicationGroups = @{} $script:ApplicationGroups.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredApplicationGroups.Add($_.Key, $_.Value) } $filteredSessionHosts = @{} $script:SessionHosts.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredSessionHosts.Add($_.Key, $_.Value) } $templateParams = @{ HostPools = [array] ($filteredHostPools | Convert-HashtableToArray) ApplicationGroups = [array] ($filteredApplicationGroups | Convert-HashtableToArray) SessionHosts = [array] ($filteredSessionHosts | Convert-HashtableToArray) } } 'Workspace' { $filteredWorkspaces = @{} $script:Workspaces.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredWorkspaces.Add($_.Key, $_.Value) } $templateParams = @{ Workspaces = [array] ($filteredWorkspaces | Convert-HashtableToArray) } } } $templateParams } function Invoke-AVDMFDesktopVirtualization { [CmdletBinding()] param ( ) #region: Initialize Variables $bicepWorkspaces = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\Workspaces.bicep" $bicepHostPools = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\HostPools.bicep" #endregion: Initialize Variables # Host Pools $hostPoolJobs = @() foreach ($rg in $script:ResourceGroups.Keys) { if ($script:ResourceGroups[$rg].ResourceCategory -eq 'HostPool') { $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'HostPool' try{ $null = Get-AzResourceGroup -Name $rg -ErrorAction Stop } catch{ New-AzResourceGroup -Name $rg -Location $script:Location } $hostPoolJobs += New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepHostPools @templateParams -ErrorAction Stop -Confirm:$false -Force -AsJob } } $dateTime = Get-Date while($hostPoolJobs.State -contains "Running"){ Start-Sleep -Seconds 5 $timeSpan = New-TimeSpan -Start $dateTime -End (Get-date) $count = ($hostPoolJobs | Where-Object {$_.State -eq "Running"}).count Write-PSFMessage -Level Host -Message "Waiting for $count hostpool deployments to complete - Been waiting for $($timeSpan.ToString())" } Write-PSFMessage -Level Host -Message "Hostpool jobs completed. See output below." $hostPoolJobs | Receive-Job # Workspaces Write-PSFMessage -Level Host -Message "Creating workspaces" foreach ($rg in $script:ResourceGroups.Keys) { if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Workspace') { $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'Workspace' try{ $null = Get-AzResourceGroup -Name $rg -ErrorAction Stop } catch{ New-AzResourceGroup -Name $rg -Location $script:Location } New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepWorkspaces @templateParams -ErrorAction Stop -Confirm:$false -Force } } } function Test-AVDMFDesktopVirtualization { [CmdletBinding()] param ( ) #region: Initialize Variables $bicepHostPools = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\HostPools.bicep" $bicepWorkspaces = "$($moduleRoot)\internal\Bicep\DesktopVirtualization\Workspaces.bicep" #endregion: Initialize Variables # Host Pools foreach ($rg in $script:ResourceGroups.Keys) { if ($script:ResourceGroups[$rg].ResourceCategory -eq 'HostPool') { $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'HostPool' New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepHostPools @templateParams -ErrorAction Stop -WhatIf } } # Workspaces foreach ($rg in $script:ResourceGroups.Keys) { if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Workspace') { $templateParams = Initialize-AVDMFDesktopVirtualization -ResourceGroupName $rg -ResourceCategory 'Workspace' New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepWorkspaces @templateParams -ErrorAction Stop -WhatIf } } } function Get-AVDMFApplicationGroup { $script:ApplicationGroups } function Register-AVDMFApplicationGroup { [CmdletBinding()] param ( [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $HostPoolName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $HostPoolResourceId, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $ResourceGroupName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $FriendlyName, [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )] [string[]] $Users, [PSCustomObject] $Tags = [PSCustomObject]@{} ) process { $ResourceName = New-AVDMFResourceName -ResourceType 'ApplicationGroup' -ParentName $HostPoolName $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.DesktopVirtualization/applicationgroups/$ResourceName" $principalId = @() if ($Users.count -ge 1) { $principalId += foreach ($user in $Users) { try { if ($user -like "*@*" ) { Write-PSFMessage -Level Verbose -Message "Resolving Id for user: $user" $id = (Get-AzADUser -UserPrincipalName $user -ErrorAction Stop).Id } else { Write-PSFMessage -Level Verbose -Message "Resolving Id for group: $user" $id = (Get-AzADGroup -DisplayName $user -ErrorAction Stop).Id } if ($null -eq $id) { throw } $id } catch { throw "Could not resolve id for $user - If the name is correct then ensure the service principal used is assigned 'Directory readers' role." } } } $script:ApplicationGroups[$ResourceName] = [PSCustomObject]@{ PSTypeName = 'AVDMF.DesktopVirtualization.ApplicationGroup' ResourceGroupName = $ResourceGroupName HostPoolId = $HostPoolResourceId FriendlyName = $FriendlyName PrincipalId = $principalId Tags = $Tags } # Link Application group to workspace $script:Workspaces.GetEnumerator() | Where-Object { $_.value.ReferenceName -eq $script:hostpools.$hostpoolname.WorkspaceReference } | ForEach-Object { $_.value.ApplicationGroupReferences += $resourceID } } } function Get-AVDMFHostPool { $script:HostPools } function Register-AVDMFHostPool { [CmdletBinding()] param ( [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $AccessLevel, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $PoolType, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [int] $MaxSessionLimit, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [int] $NumberOfSessionHosts, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $FriendlyName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $WorkSpaceReference, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $VirtualNetworkReference, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $SubnetNSG, [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )] [string] $SubnetRouteTable, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $StorageAccountReference, [Parameter(Mandatory = $false , ValueFromPipelineByPropertyName = $true )] [string[]] $Users, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $VMTemplate, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $OrganizationalUnitDN, [PSCustomObject] $Tags = [PSCustomObject]@{} ) process { $ResourceName = New-AVDMFResourceName -ResourceType 'HostPool' -AccessLevel $AccessLevel -HostPoolType $PoolType $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'HostPool' -AccessLevel $AccessLevel -HostPoolType $PoolType Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'HostPool' $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.DesktopVirtualization/hostpools/$ResourceName" #Register Subnet $subnetParams = @{ Scope = $AccessLevel + 'Access' #TODO: Change the parameter name from scope to Access Level, also change it in subnet configurations NamePrefix = $resourceName VirtualNetworkName = $script:VirtualNetworks[$VirtualNetworkReference].ResourceName VirtualNetworkID = $script:VirtualNetworks[$VirtualNetworkReference].ResourceID NSGID = $script:NetworkSecurityGroups[$SubnetNSG].ResourceID RouteTableID = $script:RouteTables[$SubnetRouteTable].ResourceID } $subnetID = Register-AVDMFSubnet @subnetParams -PassThru # Pickup Storage Account #TODO: Change Storage Accounts into HashTables $StorageAccountRef = $StorageAccountReference #There is a bug in Script Analyzer that causes the parameter to report unused. $storageAccount = $script:StorageAccounts[$StorageAccountRef] Register-AVDMFFileShare -Name $resourceName.ToLower() -StorageAccountName $storageAccount.Name -ResourceGroupName $storageAccount.ResourceGroupName $script:HostPools[$ResourceName] = [PSCustomObject]@{ PSTypeName = 'AVDMF.DesktopVirtualization.HostPool' ResourceGroupName = $resourceGroupName ResourceID = $resourceID PoolType = $PoolType MaxSessionLimit = $MaxSessionLimit NumberOfSessionHosts = $NumberOfSessionHosts WorkSpaceReference = $WorkSpaceReference SubnetID = $subnetID VMTemplate = $VMTemplate Tags = $Tags } #TODO: Change this into splatting and check if users are provided. Register-AVDMFApplicationGroup -HostPoolName $resourceName -ResourceGroupName $resourceGroupName -HostPoolResourceId $resourceID -Tags $Tags -Users $Users -FriendlyName $FriendlyName # Register Session Host $hostPoolInstance = $ResourceName.Substring($ResourceName.Length - 2, 2) $domainName = ($OrganizationalUnitDN -split "," | Where-Object { $_ -like "DC=*" } | ForEach-Object { $_.replace("DC=", "") }) -join "." for ($i = 1; $i -le $NumberOfSessionHosts; $i++) { #TODO: Change all parameters to use splatting $SessionHostParams = @{ subnetID = $subnetID DomainName = $domainName OUPath = $OrganizationalUnitDN } Register-AVDMFSessionHost -ResourceGroupName $resourceGroupName -AccessLevel $AccessLevel -HostPoolType $PoolType -HostPoolInstance $hostPoolInstance -InstanceNumber $i -VMTemplate $script:VMTemplates[$VMTemplate] @SessionHostParams -Tags $Tags } } } function Get-AVDMFSessionHost { $script:SessionHosts } function Register-AVDMFSessionHost { [CmdletBinding()] param ( [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $AccessLevel, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $HostPoolType, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $HostPoolInstance, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $ResourceGroupName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [int] $InstanceNumber, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [object] $VMTemplate, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [object] $SubnetID, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $DomainName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $OUPath, [PSCustomObject] $Tags = [PSCustomObject]@{} ) process { $ResourceName = New-AVDMFResourceName -ResourceType 'VirtualMachine' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -HostPoolInstance $HostPoolInstance -InstanceNumber $InstanceNumber $script:SessionHosts[$resourceName] = [PSCustomObject]@{ ResourceGroupName = $ResourceGroupName VMSize = $VMTemplate.VMSize TimeZone = $script:TimeZone SubnetID = $SubnetID AdminUsername = $VMTemplate.AdminUserName AdminPassword = $VMTemplate.AdminPassword ImageReference = $VMTemplate.ImageReference Tags = $Tags # Add Session Host WVDArtifactsURL = $VMTemplate.WVDArtifactsURL # Domain Join DomainName = $DomainName OUPath = $OUPath DomainJoinUserName = $script:DomainJoinUserName DomainJoinPassword = $script:DomainJoinPassword } } } function Get-AVDMFVMTemplate { $script:VMTemplates } function Register-AVDMFVMTemplate { [CmdletBinding()] param ( [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $ReferenceName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $AdminUsername, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $VMSize, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [Object] $ImageReference, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $WVDArtifactsURL ) process { $script:VMTemplates[$ReferenceName] = @{ AdminUserName = $AdminUsername AdminPassword = Get-RandomPassword VMSize = $VMSize ImageReference = $ImageReference | ConvertTo-PSFHashtable WVDArtifactsURL = $WVDArtifactsURL } } } function Get-AVDMFWorkspace { $script:Workspaces } function Register-AVDMFWorkspace { [CmdletBinding()] param ( [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $AccessLevel, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $HostPoolType, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $ReferenceName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $FriendlyName, [PSCustomObject] $Tags = [PSCustomObject]@{} ) process { $ResourceName = New-AVDMFResourceName -ResourceType 'Workspace' -AccessLevel $AccessLevel -HostPoolType $HostPoolType $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Workspace' -AccessLevel $AccessLevel -HostPoolType $HostPoolType Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Workspace' $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.DesktopVirtualization/workspaces/$ResourceName" $script:Workspaces[$ResourceName] = [PSCustomObject]@{ PSTypeName = 'AVDMF.DesktopVirtualization.Workspace' ResourceID = $resourceID ReferenceName = $ReferenceName ResourceGroupName = $resourceGroupName FriendlyName = $FriendlyName ApplicationGroupReferences = @() Tags = $Tags } } } function Register-AVDMFGlobalSettings { param ( # Stage [string] $Stage ) $script:AVDMFGlobalSettings = [PSCustomObject]@{ Stage = $Stage } } function Add-AVDMFTag { <# .SYNOPSIS Adds tags to resources #> [CmdletBinding()] param ( # ResourceType [Parameter(Mandatory = $true)] [string] $ResourceType, # Resource Object [Parameter(Mandatory = $true)] $ResourceObject ) # Tags that apply to all resources if($script:GlobalTags['All']){ $effectiveTags = $script:GlobalTags['All'].Clone() } # Tags that apply to all instaces of a specific resource type if($script:GlobalTags[$ResourceType]){ $resourceTypeTags = $script:GlobalTags[$ResourceType] foreach($item in $resourceTypeTags.GetEnumerator()) {$effectiveTags[$item.Key] = $item.Value} } if($ResourceObject.Tags){ $resourceSpecificTags = $ResourceObject.Tags | ConvertTo-PSFHashtable foreach($item in $resourceSpecificTags.GetEnumerator()) {$effectiveTags[$item.Key] = $item.Value} } if ($effectiveTags) { $ResourceObject | Add-Member -MemberType NoteProperty -Name Tags -Value $effectiveTags -Force } $ResourceObject } function Get-AVDMFGlobalTag { $script:GlobalTags } function Register-AVDMFGlobalTag { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $ResourceType, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [PSCustomObject] $Tags ) process { $Tags = $Tags | ConvertTo-PSFHashtable $script:GlobalTags[$ResourceType] = $Tags } } function Get-AVDMFNameMapping { $script:NameMappings } function Register-AVDMFNameMapping { [CmdletBinding()] param ( [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $Name, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $VariableName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [Object] $Value ) process { if($VariableName -like "$*"){ throw "Variable names in Name Mapping cannot start with '$'" } $variableNameValue = if($VariableName -like "env:*"){ (Get-Item -Path $VariableName).Value } else{ (Get-Variable -Name $VariableName).Value } $script:NameMappings[$Name] = $Value.$variableNameValue } } function Initialize-AVDMFNetwork { [CmdletBinding()] [OutputType('System.Collections.Hashtable')] param ( [string] $ResourceGroupName ) $filteredVirtualNetworks = @{} $filteredSubnets = @{} $filteredNetworkSecurityGroups = @{} $filteredRouteTables = @{} $script:VirtualNetworks.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } -PipelineVariable vNet | ForEach-Object { $filteredVirtualNetworks.Add($vNet.Key, $vNet.Value) $script:Subnets.GetEnumerator() | Where-Object { $_.value.VirtualNetworkName -eq $vNet.Value.ResourceName } | ForEach-Object { $filteredSubnets.Add($_.Key, $_.Value) } } $script:RouteTables.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } -PipelineVariable routeTable | ForEach-Object { $filteredRouteTables.Add($routeTable.Key, $routeTable.Value) } foreach ($nsg in $script:NetworkSecurityGroups.keys) { if ($script:NetworkSecurityGroups[$nsg].ResourceGroupName -eq $ResourceGroupName) { $filteredNetworkSecurityGroups[$nsg] = $script:NetworkSecurityGroups[$nsg] } } $templateParams = @{ VirtualNetworks = [array] ($filteredVirtualNetworks | Convert-HashtableToArray) Subnets = [array] ($filteredSubnets | Convert-HashtableToArray) NetworkSecurityGroups = [array] ($filteredNetworkSecurityGroups | Convert-HashtableToArray) RouteTables = [array] ($filteredRouteTables | Convert-HashtableToArray) } $templateParams } function Initialize-AVDMFRemotePeering { [CmdletBinding()] [OutputType('System.Collections.Hashtable')] $templateParams = @{ RemotePeerings = [array] ($script:RemotePeerings | Convert-HashtableToArray) } $templateParams } function Invoke-AVDMFNetwork { [CmdletBinding()] param ( ) #Initialize Variables $bicepVirtualNetwork = "$($moduleRoot)\internal\Bicep\Network\Network.bicep" foreach ($rg in $script:ResourceGroups.Keys) { if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Network') { $templateParams = Initialize-AVDMFNetwork -ResourceGroupName $rg $BP = 'HERE' try{ $null = Get-AzResourceGroup -Name $rg -ErrorAction Stop } catch{ New-AzResourceGroup -Name $rg -Location $script:Location } New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepVirtualNetwork @templateParams -ErrorAction Stop -Confirm:$false -Force } } # Create remote peerings if($script:RemotePeerings.count){ $bicepRemotePeerings = "$($moduleRoot)\internal\Bicep\Network\RemotePeerings.bicep" $templateParams = Initialize-AVDMFRemotePeering New-AzResourceGroupDeployment -ResourceGroupName $rg -TemplateFile $bicepRemotePeerings @templateParams -ErrorAction Stop -Confirm:$false -Force } } function Test-AVDMFNetwork { [CmdletBinding()] param ( ) #region: Initialize Variables $bicepVirtualNetwork = "$($moduleRoot)\internal\Bicep\Network\Network.bicep" #endregion: Initialize Variables foreach ($rg in $script:ResourceGroups.Keys) { if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Network') { $templateParams = Initialize-AVDMFNetwork -ResourceGroupName $rg New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepVirtualNetwork @templateParams -ErrorAction Stop -WhatIf } } } function Get-AVDMFAddressSpace { $script:AddressSpaces } function Register-AVDMFAddressSpace { param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Scope, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $AddressSpace, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [int] $SubnetMask ) process { $Script:AddressSpaces += [PSCustomObject]@{ Scope = $Scope AddressSpace = $AddressSpace subnetMask = $SubnetMask } } } function Get-AVDMFNetworkSecurityGroup { $script:NetworkSecurityGroups } function Register-AVDMFNetworkSecurityGroup { param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $ReferenceName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [array] $SecurityRules, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $AccessLevel, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $HostPoolType, [PSCustomObject] $Tags = [PSCustomObject]@{} ) process { $resourceName = New-AVDMFResourceName -ResourceType 'NetworkSecurityGroup' -AccessLevel $AccessLevel -HostPoolType $HostPoolType #Register Resource Group if needed $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Network' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -InstanceNumber 1 # At the moment we do not have a reason for multiple network RGs. Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Network' $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/networkSecurityGroups/$resourceName" $script:NetworkSecurityGroups[$ReferenceName] = [PSCustomObject]@{ PSTypeName = 'AVDMF.Network.NetworkSecurityGroup' ResourceName = $resourceName ResourceGroupName = $resourceGroupName ResourceID = $resourceID ReferenceName = $ReferenceName SecurityRules = @($SecurityRules | ForEach-Object { $_ | ConvertTo-PSFHashtable }) Tags = $Tags } } } function Get-AVDMFRemotePeering { $script:RemotePeerings } function Register-AVDMFRemotePeering { param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $RemoteVNetResourceID, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $LocalVNetResourceId ) process { $remoteVNet = Get-AVDMFResourceInfo -ResourceId $RemoteVNetResourceID $localVNet = Get-AVDMFResourceInfo -ResourceId $LocalVNetResourceId $referenceName = "Peering_{0}_To_{1}" -f $RemoteVNet.ResourceName, $LocalVNet.ResourceName #this is used for the hashtable. $name = "Peering_To_{0}" -f $LocalVNet.ResourceName $script:RemotePeerings[$referenceName] = [PSCustomObject]@{ PSTypeName = 'AVDMF.Network.RemotePeering' Name = $name SubscriptionId = $remoteVNet.SubscriptionId #TODO: Implement Remote Subscription Support. ResourceGroupName = $remoteVNet.ResourceGroupName RemoteVNetName = $remoteVNet.ResourceName LocalVNetResourceId = $LocalVNetResourceId } } } function Get-AVDMFRouteTable { $script:RouteTables } function Register-AVDMFRouteTable { param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $ReferenceName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [array] $Routes, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [Boolean] $DisableBgpRoutePropagation, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $AccessLevel, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $HostPoolType, [PSCustomObject] $Tags = [PSCustomObject]@{} ) process { $resourceName = New-AVDMFResourceName -ResourceType 'RouteTable' -AccessLevel $AccessLevel -HostPoolType $HostPoolType #Register Resource Group if needed $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Network' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -InstanceNumber 1 # At the moment we do not have a reason for multiple network RGs. Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Network' $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/routeTables/$resourceName" $routesHashTable = @($Routes | ForEach-Object { $_ | ConvertTo-PSFHashtable }) foreach ($item in $routesHashTable) { $item.properties = $item.properties | ConvertTo-PSFHashtable } $script:RouteTables[$ReferenceName] = [PSCustomObject]@{ PSTypeName = 'AVDMF.Network.RouteTable' ResourceName = $resourceName ResourceGroupName = $resourceGroupName ResourceID = $resourceID ReferenceName = $ReferenceName Routes = $routesHashTable #@($Routes | ForEach-Object { $_ | ConvertTo-PSFHashtable }) DisableBgpRoutePropagation = $DisableBgpRoutePropagation Tags = $Tags } } } function Get-AVDMFSubnet { $script:Subnets } function Register-AVDMFSubnet { param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Scope, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $NamePrefix, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $VirtualNetworkName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $VirtualNetworkID, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [bool] $PrivateLink = $false, [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [string] $NSGID , [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [string] $RouteTableID , [switch] $PassThru ) process { #region: Calculate subnet range and prefix [array] $scope = ($Script:AddressSpaces | Where-Object { $_.Scope -eq $Scope }) if ($scope.count -gt 1) { throw "Found multiple scopes, please review address spaces configuration and avoid duplicates." } [string] $addressSpace = $scope.AddressSpace [int] $subnetMask = $scope.SubnetMask Write-Verbose "Will use the address space $addressSpace and subnet mask $subnetMask" if (-not ($addressSpace -match '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/\d{2}$')) { throw "$addressSpace is not a valid address space" } $addressPrefix = (New-AVDMFSubnetRange -AddressSpace $addressSpace -NewSubnetMaskBits $subnetMask -ErrorAction 'Stop').AddressPrefix #endregion: Calculate subnet range and prefix $resourceName = New-AVDMFResourceName -ResourceType 'Subnet' -ParentName $NamePrefix -AddressPrefix $addressPrefix $resourceID = "$VirtualNetworkID/subnets/$resourceName" #Build Subnet properties $properties = @{ addressPrefix = $addressPrefix privateEndpointNetworkPolicies = if ($PrivateLink) { "Disabled" } else { "Enabled" } } if ($NSGID) { $properties['networkSecurityGroup'] = @{id = $NSGID } } if ($RouteTableID) { $properties['routeTable'] = @{id = $RouteTableID } } $script:Subnets[$resourceName] = [PSCustomObject]@{ PSTypeName = 'AVDMF.Network.Subnet' VirtualNetworkName = $VirtualNetworkName ResourceID = $resourceID Properties = $properties } if ($PassThru) { $resourceID } } } function Get-AVDMFVirtualNetwork { $script:VirtualNetworks } function Register-AVDMFVirtualNetwork { param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $ReferenceName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $DNSServers, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [array] $DefaultSubnets, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [array] $VirtualNetworkPeerings, [string] $AccessLevel = 'All', [string] $HostPoolType = 'All', [PSCustomObject] $Tags = [PSCustomObject]@{} ) process { $resourceName = New-AVDMFResourceName -ResourceType 'VirtualNetwork' -AccessLevel $AccessLevel -HostPoolType $HostPoolType #Register Resource Group if needed $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Network' -AccessLevel $AccessLevel -HostPoolType $HostPoolType -InstanceNumber 1 Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Network' # At the moment we do not have a reason for multiple network RGs. $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/VirtualNetworks/$resourceName" #Register Virtual Networks [string]$addressSpace = ($Script:AddressSpaces | Where-Object Scope -EQ 'VirtualNetwork').AddressSpace if (-not ($addressSpace -match '^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\/\d{2}$')) { throw "$addressSpace is not a valid address space" } # Configure Peerings $peerings = @(foreach($peering in $VirtualNetworkPeerings){ $RemoteNetworkName = ($peering.RemoteVnetId -split "/")[-1] Write-PSFMessage -Level Verbose -Message "Configuring peering with $RemoteNetworkName" @{ Name = "PeeringTo_$RemoteNetworkName" RemoteNetworkID = $peering.RemoteVnetId UseRemoteGateways = [bool] $peering.useRemoteGateways } if($peering.CreateRemotePeering){ Write-PSFMessage -Level Verbose -Message "Registering remote peering." Register-AVDMFRemotePeering -RemoteVNetResourceID $peering.RemoteVNetId -LocalVNetResourceId $resourceID } else { Write-PSFMessage -Level Warning -Message "Peering of Virtual Network '$ReferenceName ($resourceName)' to '$RemoteNetworkName' is not configured to create remote peering. You must manually create peering in the remote network." # Add link to help on website. } }) $script:VirtualNetworks[$ReferenceName] = [PSCustomObject]@{ PSTypeName = 'AVDMF.Network.VirtualNetwork' ResourceName = $resourceName ResourceGroupName = $resourceGroupName ResourceID = $resourceID AddressSpace = $addressSpace DNSServers = $DNSServers VirtualNetworkPeerings = $peerings Tags = $Tags } #Register Default Subnets foreach ($subnet in $DefaultSubnets) { $subnet | Register-AVDMFSubnet -VirtualNetworkName $resourceName -VirtualNetworkID $resourceID -ErrorAction Stop #TODO Utilize value from pipeline of subnet object } } } function Get-AVDMFResourceGroup { $script:ResourceGroups } function Register-AVDMFResourceGroup { [CmdletBinding()] param ( [string] $Name, [string] $ResourceCategory ) $script:ResourceGroups[$Name] = [PSCustomObject]@{ PSTypeName = 'AVDMF.ResourceGroup' ResourceCategory = $ResourceCategory } } function Initialize-AVDMFStorage { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSReviewUnusedParameter", "")] [CmdletBinding()] [OutputType('System.Collections.Hashtable')] param ( [string] $ResourceGroupName ) $filteredStorageAccounts = @{} $filteredPrivateLinks = @{} $filteredFileShares = @{} $script:StorageAccounts.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredStorageAccounts.Add($_.Key, $_.Value) } $script:PrivateLinks.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object { $filteredPrivateLinks.Add($_.Key, $_.Value) } $script:FileShares.GetEnumerator() | Where-Object { $_.value.ResourceGroupName -eq $ResourceGroupName } | ForEach-Object {$filteredFileShares.Add($_.Key, $_.Value)} $templateParams = @{ StorageAccounts = [array] ($filteredStorageAccounts | Convert-HashtableToArray) PrivateLinks = [array] ($filteredPrivateLinks | Convert-HashtableToArray) FileShares = [array] ($filteredFileShares | Convert-HashtableToArray) } $templateParams } function Invoke-AVDMFStorage { [CmdletBinding()] param ( ) #region: Initialize Variables $bicepStorage = "$($moduleRoot)\internal\Bicep\Storage\Storage.bicep" #endregion: Initialize Variables foreach ($rg in $script:ResourceGroups.Keys) { if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Storage') { $templateParams = Initialize-AVDMFStorage -ResourceGroupName $rg try{ Get-AzResourceGroup -Name $rg -ErrorAction Stop | Out-Null } catch{ New-AzResourceGroup -Name $rg -Location $script:Location } New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Incremental -TemplateFile $bicepStorage @templateParams -ErrorAction Stop -Confirm:$false -Force # Cannot use Complete mode with Private links, see: https://feedback.azure.com/forums/217313-networking/suggestions/40395946-private-endpoint-arm-template-deployment-fix-comp } } } function Test-AVDMFStorage { [CmdletBinding()] param ( ) #region: Initialize Variables $bicepStorage = "$($moduleRoot)\internal\Bicep\Storage\Storage.bicep" #endregion: Initialize Variables foreach ($rg in $script:ResourceGroups.Keys) { if ($script:ResourceGroups[$rg].ResourceCategory -eq 'Storage') { $templateParams = Initialize-AVDMFStorage -ResourceGroupName $rg try{ Get-AzResourceGroup -Name $rg -ErrorAction Stop | Out-Null } catch{ Write-Warning -Message "Resourcegroup $rg does not exist. Skipping test for: `r`n$($templateParams.Values.ResourceID | out-string)" continue } New-AzResourceGroupDeployment -ResourceGroupName $rg -Mode Complete -TemplateFile $bicepStorage @templateParams -ErrorAction Stop -WhatIf } } } function Get-AVDMFFileShare { $script:FileShares } function Register-AVDMFFileShare { [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $Name, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $StorageAccountName, [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $ResourceGroupName ) process { $script:FileShares[$Name] = [PSCustomObject]@{ PSTypeName = 'AVDMF.Storage.FileShare' ResourceName = $Name ResourceGroupName = $resourceGroupName StorageAccountName = $StorageAccountName } } } function Get-AVDMFPrivateLink { $script:PrivateLinks } function Register-AVDMFPrivateLink { [CmdletBinding()] param ( [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $ResourceGroupName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $StorageAccountName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $StorageAccountID, [PSCustomObject] $Tags = [PSCustomObject]@{} ) process { $SubnetId = $script:subnets[($script:subnets.keys | Where-Object { $_ -like 'PrivateLinks*' })].ResourceId $resourceName = New-AVDMFResourceName -ResourceType 'PrivateLink' -ParentName $StorageAccountName $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Network/privateEndpoints/$ResourceName" $script:PrivateLinks[$resourceName]= [PSCustomObject]@{ PSTypeName = 'AVDMF.Storage.PrivateLink' ResourceGroupName = $resourceGroupName ResourceID = $resourceID StorageAccountID = $StorageAccountID SubnetID = $SubnetId Tags = $Tags } } } function Get-AVDMFStorageAccount { $script:StorageAccounts } function Register-AVDMFStorageAccount { [CmdletBinding()] param ( [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $AccessLevel, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $accountType, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $HostPoolType, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $Kind, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [string] $ReferenceName, [Parameter(Mandatory = $true , ValueFromPipelineByPropertyName = $true )] [int] $shareSoftDeleteRetentionDays, [PSCustomObject] $Tags = [PSCustomObject]@{} ) process { $ResourceName = New-AVDMFResourceName -ResourceType 'StorageAccount' -AccessLevel $AccessLevel -HostPoolType $HostPoolType $resourceGroupName = New-AVDMFResourceName -ResourceType "ResourceGroup" -ResourceCategory 'Storage' -AccessLevel 'All' -HostPoolType 'All' -InstanceNumber 1 Register-AVDMFResourceGroup -Name $resourceGroupName -ResourceCategory 'Storage' # At the moment we do not have a reason for multiple storage RGs. $resourceID = "/Subscriptions/$script:AzSubscriptionId/resourceGroups/$resourceGroupName/providers/Microsoft.Storage/storageAccounts/$ResourceName" $script:StorageAccounts[$ReferenceName] = [PSCustomObject]@{ PSTypeName = 'AVDMF.Storage.StorageAccount' ResourceGroupName = $resourceGroupName ResourceID = $resourceID Name = $ResourceName ReferenceName = $ReferenceName AccountType = $accountType Kind = $Kind SoftDeleteDays = $ShareSoftDeleteRetentionDays Tags = $Tags } #register Private Link Register-AVDMFPrivateLink -ResourceGroupName $resourceGroupName -StorageAccountName $ResourceName -StorageAccountID $resourceID } } <# This is an example configuration file By default, it is enough to have a single one of them, however if you have enough configuration settings to justify having multiple copies of it, feel totally free to split them into multiple files. #> <# # Example Configuration Set-PSFConfig -Module 'AVDManagementFramework' -Name 'Example.Setting' -Value 10 -Initialize -Validation 'integer' -Handler { } -Description "Example configuration setting. Your module can then use the setting using 'Get-PSFConfigValue'" #> Set-PSFConfig -Module 'AVDManagementFramework' -Name 'Import.DoDotSource' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be dotsourced on import. By default, the files of this module are read as string value and invoked, which is faster but worse on debugging." Set-PSFConfig -Module 'AVDManagementFramework' -Name 'Import.IndividualFiles' -Value $false -Initialize -Validation 'bool' -Description "Whether the module files should be imported individually. During the module build, all module code is compiled into few files, which are imported instead by default. Loading the compiled versions is faster, using the individual files is easier for debugging and testing out adjustments." <# Stored scriptblocks are available in [PsfValidateScript()] attributes. This makes it easier to centrally provide the same scriptblock multiple times, without having to maintain it in separate locations. It also prevents lengthy validation scriptblocks from making your parameter block hard to read. Set-PSFScriptblock -Name 'AVDManagementFramework.ScriptBlockName' -Scriptblock { } #> <# # Example: Register-PSFTeppScriptblock -Name "AVDManagementFramework.alcohol" -ScriptBlock { 'Beer','Mead','Whiskey','Wine','Vodka','Rum (3y)', 'Rum (5y)', 'Rum (7y)' } #> <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Alcohol -Parameter Type -Name AVDManagementFramework.alcohol #> New-PSFLicense -Product 'AVDManagementFramework' -Manufacturer 'wmoselhy' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2021-04-26") -Text @" Copyright (c) 2021 wmoselhy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "@ # General Settings $script:WVDConfigurationLoaded = $false $script:NameMappings = @{} # Resource Groups $script:ResourceGroups = @{} # Network $script:VirtualNetworks = @{} $script:Subnets = @{} $Script:AddressSpaces = @() $script:NetworkSecurityGroups = @{} $script:RemotePeerings = @{} $script:RouteTables = @{} # Storage $script:StorageAccounts = @{} $script:FileShares = @{} $script:PrivateLinks = @{} # DesktopVirtualization $script:HostPools = @{} $script:ApplicationGroups = @{} $script:Workspaces = @{} $script:VMTemplates = @{} $script:SessionHosts = @{} # Tags $script:GlobalTags = @{} #endregion Load compiled code |