AzOps.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\AzOps.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced $script:doDotSource = Get-PSFConfigValue -FullName AzOps.Import.DoDotSource -Fallback $false if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $script:doDotSource = $true } if ($AzOps_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 AzOps.Import.IndividualFiles -Fallback $false if ($AzOps_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 > . 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)\localized\en-us\*.psd1" -Module 'AzOps' -Language 'en-US' class AzOpsRoleAssignment { [string]$ResourceType [string]$Name [string]$Id [hashtable]$Properties AzOpsRoleAssignment($Properties) { $this.Properties = [ordered]@{ DisplayName = $Properties.DisplayName PrincipalId = $Properties.ObjectId RoleDefinitionName = $Properties.RoleDefinitionName ObjectType = $Properties.ObjectType RoleDefinitionId = '/providers/Microsoft.Authorization/RoleDefinitions/{0}' -f $Properties.RoleDefinitionId } $this.Id = $Properties.RoleAssignmentId $this.Name = ($Properties.RoleAssignmentId -split "/")[-1] $this.ResourceType = "Microsoft.Authorization/roleAssignments" } } class AzOpsRoleDefinition { [string]$ResourceType [string]$Name [string]$Id [hashtable]$Properties AzOpsRoleDefinition($Properties) { $this.Id = $Properties.AssignableScopes[0] + '/providers/Microsoft.Authorization/roleDefinitions/' + $Properties.Id $this.Name = $Properties.Id $this.Properties = [ordered]@{ AssignableScopes = @($Properties.AssignableScopes) Description = $Properties.Description Permissions = @( [ordered]@{ Actions = @($Properties.Actions) DataActions = @($Properties.DataActions) NotActions = @($Properties.NotActions) NotDataActions = @($Properties.NotDataActions) } ) RoleName = $Properties.Name } $this.ResourceType = "Microsoft.Authorization/roleDefinitions" } } class AzOpsScope { [string]$Scope [string]$Type [string]$Name [string]$StatePath [string]$ManagementGroup [string]$ManagementGroupDisplayName [string]$Subscription [string]$SubscriptionDisplayName [string]$ResourceGroup [string]$ResourceProvider [string]$Resource hidden [string]$StateRoot #region Internal Regex Helpers hidden [regex]$regex_tenant = '/$' hidden [regex]$regex_managementgroup = '(?i)^/providers/Microsoft.Management/managementgroups/[^/]+$' hidden [regex]$regex_managementgroupExtract = '(?i)^/providers/Microsoft.Management/managementgroups/' hidden [regex]$regex_subscription = '(?i)^/subscriptions/[^/]*$' hidden [regex]$regex_subscriptionExtract = '(?i)^/subscriptions/' hidden [regex]$regex_resourceGroup = '(?i)^/subscriptions/.*/resourcegroups/[^/]*$' hidden [regex]$regex_resourceGroupExtract = '(?i)^/subscriptions/.*/resourcegroups/' hidden [regex]$regex_managementgroupProvider = '(?i)^/providers/Microsoft.Management/managementgroups/[\s\S]*/providers' hidden [regex]$regex_subscriptionProvider = '(?i)^/subscriptions/.*/providers' hidden [regex]$regex_resourceGroupProvider = '(?i)^/subscriptions/.*/resourcegroups/[\s\S]*/providers' hidden [regex]$regex_managementgroupResource = '(?i)^/providers/Microsoft.Management/managementGroups/[\s\S]*/providers/[\s\S]*/[\s\S]*/' hidden [regex]$regex_subscriptionResource = '(?i)^/subscriptions/.*/providers/[\s\S]*/[\s\S]*/' hidden [regex]$regex_resourceGroupResource = '(?i)^/subscriptions/.*/resourcegroups/[\s\S]*/providers/[\s\S]*/[\s\S]*/' #endregion Internal Regex Helpers #region Constructors AzOpsScope ([string]$Scope, [string]$StateRoot) { <# .SYNOPSIS Creates an AzOpsScope based on the specified resource ID or File System Path .DESCRIPTION Creates an AzOpsScope based on the specified resource ID or File System Path .PARAMETER Scope Scope == ResourceID or File System Path .INPUTS None. You cannot pipe objects to Add-Extension. .OUTPUTS System.String. Add-Extension returns a string with the extension or file name. .EXAMPLE New-AzOpsScope -Scope "/providers/Microsoft.Management/managementGroups/3fc1081d-6105-4e19-b60c-1ec1252cf560" Creates an AzOpsScope based on the specified resource ID #> Write-PSFMessage -Level Verbose -String 'AzOpsScope.Constructor' -StringValues $scope -FunctionName AzOpsScope -ModuleName AzOps $this.StateRoot = $StateRoot if (Test-Path -Path $scope) { if ((Get-Item $scope).GetType().ToString() -eq 'System.IO.FileInfo') { #Strong confidence based on content - file Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile' -StringValues $scope -FunctionName AzOpsScope -ModuleName AzOps $this.InitializeMemberVariablesFromFile($Scope) } else { # Weak confidence based on metadata at scope - directory Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory' -StringValues $scope -FunctionName AzOpsScope -ModuleName AzOps $this.InitializeMemberVariablesFromDirectory($Scope) } } else { Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariables' -StringValues $scope -FunctionName AzOpsScope -ModuleName AzOps $this.InitializeMemberVariables($Scope) } } # Overloaded constructors - repeat member assignments in each constructor definition #AzOpsScope ([System.IO.DirectoryInfo]$Path, [string]$StateRoot) { hidden [void] InitializeMemberVariablesFromDirectory([System.IO.DirectoryInfo]$Path) { $managementGroupFileName = "microsoft.management_managementGroups-*$(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')" $subscriptionFileName = "microsoft.subscription_subscriptions-*$(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')" $resourceGroupFileName = "microsoft.resources_resourceGroups-*$(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')" if ($Path.FullName -eq (Get-Item $this.StateRoot).FullName) { # Root tenant path Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory.RootTenant' -StringValues $Path -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps $this.InitializeMemberVariables("/") return } # Always look into AutoGeneratedTemplateFolderPath folder regardless of path specified if ($Path.FullName -notlike "*$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')") { $Path = Join-Path $Path -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')" Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory.AutoGeneratedFolderPath' -StringValues $Path -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps } if ($managementGroupScopeFile = (Get-ChildItem -Force -Path $Path -File | Where-Object Name -like $managementGroupFileName)) { [string] $managementGroupID = $managementGroupScopeFile.Name.Replace('microsoft.management_managementgroups-', '').Replace('.parameters', '').Replace($(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'), '') Write-PSFMessage -Level Verbose -String 'AzOpsScope.Input.FromFileName.ManagementGroup' -StringValues $managementGroupID -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps $this.InitializeMemberVariables("/providers/Microsoft.Management/managementGroups/$managementGroupID") } elseif ($subscriptionScopeFileName = (Get-ChildItem -Force -Path $Path -File | Where-Object Name -like $subscriptionFileName)) { [string] $subscriptionID = $subscriptionScopeFileName.Name.Replace('microsoft.subscription_subscriptions-', '').Replace('.parameters', '').Replace($(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'), '') Write-PSFMessage -Level Verbose -String 'AzOpsScope.Input.FromFileName.Subscription' -StringValues $subscriptionID -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps $this.InitializeMemberVariables("/subscriptions/$subscriptionID") } elseif ((Get-ChildItem -Force -Path $Path -File | Where-Object Name -like $resourceGroupFileName) -or ((Get-ChildItem -Force -Path $Path.Parent -File | Where-Object Name -like $subscriptionFileName)) ) { Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory.ParentSubscription' -StringValues $Path.Parent -FunctionName InitializeMemberVariablesFromDirectory -ModuleName AzOps $parent = (New-AzOpsScope -Path $Path.Parent) $this.InitializeMemberVariables($("/subscriptions/{0}/resourceGroups/{1}" -f $parent.Subscription, $Path.Name)) } else { #Error Write-PSFMessage -Level Warning -Tag error -String 'AzOpsScope.Input.BadData.UnknownType' -StringValues $Path -FunctionName AzOpsScope -ModuleName AzOps throw "Invalid File Structure! Cannot find Management Group / Subscription / Resource Group files in $Path!" } } #AzOpsScope ([System.IO.FileInfo]$Path, [string]$StateRoot) { hidden [void] InitializeMemberVariablesFromFile([System.IO.FileInfo]$Path) { if (-not $Path.Exists) { throw 'Invalid Input!' } if ($Path.Extension -ne '.json') { # Try to determine based on directory Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.NotJson' -StringValues $Path -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps $this.InitializeMemberVariablesFromDirectory($Path.Directory) return } else { $resourcePath = Get-Content $Path | ConvertFrom-Json -AsHashtable if (!$resourcePath) { # Empty file with .json is not valid JSON file. Empty Json should've minimum file content '{}' # However, due to bug that is combination of Get-Content and ConvertFrom-Json when empty file with .json (that is valid file but not valid Json), # switch statement is failing to handle $null value unless assigned explicitly. $resourcePath = $null } switch ($resourcePath) { { $_.parameters.input.value.Keys -contains "ResourceId" } { # Parameter Files - resource from parameters file Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.ResourceId' -StringValues $($resourcePath.parameters.input.value.ResourceId) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps $this.InitializeMemberVariables($resourcePath.parameters.input.value.ResourceId) break } { $_.parameters.input.value.Keys -contains "Id" } { # Parameter Files - ManagementGroup and Subscription Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.Id' -StringValues $($resourcePath.parameters.input.value.Id) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps $this.InitializeMemberVariables($resourcePath.parameters.input.value.Id) break } { $_.parameters.input.value.Keys -contains "Type" } { # Parameter Files - Determine Resource Type and Name (Management group) # Management group resource id do contain '/provider' Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.Type' -StringValues ("$($resourcePath.parameters.input.value.Type)/$($resourcePath.parameters.input.value.Name)") -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps $this.InitializeMemberVariables("$($resourcePath.parameters.input.value.Type)/$($resourcePath.parameters.input.value.Name)") break } { $_.parameters.input.value.Keys -contains "ResourceType" } { # Parameter Files - Determine Resource Type and Name (Any ResourceType except management group) Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.ResourceType' -StringValues ($resourcePath.parameters.input.value.ResourceType) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps $currentScope = New-AzOpsScope -Path ($Path.Directory) # Creating Resource Id based on current scope, resource Type and Name of the resource $this.InitializeMemberVariables("$($currentScope.scope)/providers/$($resourcePath.parameters.input.value.ResourceType)/$($resourcePath.parameters.input.value.Name)") break } { $_.resources -and $_.resources[0].type -eq 'Microsoft.Management/managementGroups' } { # Template - Management Group Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.managementgroups' -StringValues ($_.resources[0].name) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps $currentScope = New-AzOpsScope -Path ($Path.Directory) $this.InitializeMemberVariables("$($currentScope.scope)") break } { $_.resources -and $_.resources[0].type -eq 'Microsoft.Management/managementGroups/subscriptions' } { # Template - Subscription Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.subscriptions' -StringValues ($_.resources[0].name) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps $currentScope = New-AzOpsScope -Path ($Path.Directory.Parent) $this.InitializeMemberVariables("$($currentScope.scope)") break } { $_.resources -and $_.resources[0].type -eq 'Microsoft.Resources/resourceGroups' } { Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromFile.resourceGroups' -StringValues ($_.resources[0].name) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps $currentScope = New-AzOpsScope -Path ($Path.Directory) $this.InitializeMemberVariables("$($currentScope.scope)") break } { $_.resources } { # Template - 1st resource Write-PSFMessage -Level Verbose -String 'Az OpsScope.InitializeMemberVariablesFromFile.resource' -StringValues ($_.resources[0].type), ($_.resources[0].name) -FunctionName InitializeMemberVariablesFromFile -ModuleName AzOps $currentScope = New-AzOpsScope -Path ($Path.Directory) $this.InitializeMemberVariables("$($currentScope.scope)/providers/$($_.resources[0].type)/$($_.resources[0].name)") break } Default { Write-PSFMessage -Level Warning -String 'AzOpsScope.Input.BadData.TemplateParameterFile' -StringValues $Path -FunctionName AzOpsScope -ModuleName AzOps Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariablesFromDirectory' -StringValues $Path -FunctionName AzOpsScope -ModuleName AzOps $this.InitializeMemberVariablesFromDirectory($Path.Directory) } } } } #endregion Constructors hidden [void] InitializeMemberVariables([string]$Scope) { Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariables.Start' -StringValues ($scope) -FunctionName InitializeMemberVariables -ModuleName AzOps $this.Scope = $Scope if ($this.IsResource()) { $this.Type = "resource" $this.Name = $this.IsResource() $this.Subscription = $this.GetSubscription() $this.SubscriptionDisplayName = $this.GetSubscriptionDisplayName() $this.ManagementGroup = $this.GetManagementGroup() $this.ManagementGroupDisplayName = $this.GetManagementGroupName() $this.ResourceGroup = $this.GetResourceGroup() $this.ResourceProvider = $this.IsResourceProvider() $this.Resource = $this.GetResource() if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) { $this.StatePath = $this.GetAzOpsResourcePath() + ".json" } else { if ( (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix') -notcontains 'parameters.json' -and ("$($this.ResourceProvider)/$($this.Resource)" -eq 'Microsoft.Authorization/policyDefinitions') ) { $this.StatePath = ($this.GetAzOpsResourcePath() + '.parameters' + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')) } else { $this.StatePath = ($this.GetAzOpsResourcePath() + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')) } } } elseif ($this.IsResourceGroup()) { $this.Type = "resourcegroups" $this.ResourceProvider = "Microsoft.Resources" $this.Resource = "resourceGroups" $this.Name = $this.IsResourceGroup() $this.Subscription = $this.GetSubscription() $this.SubscriptionDisplayName = $this.GetSubscriptionDisplayName() $this.ManagementGroup = $this.GetManagementGroup() $this.ManagementGroupDisplayName = $this.GetManagementGroupName() $this.ResourceGroup = $this.GetResourceGroup() if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) { $this.StatePath = (Join-Path $this.GetAzOpsResourceGroupPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.resources_resourcegroups-$($this.ResourceGroup).json").ToLower() ) } else { $this.StatePath = (Join-Path $this.GetAzOpsResourceGroupPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.resources_resourcegroups-$($this.ResourceGroup)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')).ToLower()) } } elseif ($this.IsSubscription()) { $this.Type = "subscriptions" $this.ResourceProvider = "Microsoft.Management" $this.Resource = "managementGroups/subscriptions" $this.Name = $this.IsSubscription() $this.Subscription = $this.GetSubscription() $this.SubscriptionDisplayName = $this.GetSubscriptionDisplayName() if ($script:AzOpsAzManagementGroup) { $this.ManagementGroup = $this.GetManagementGroup() $this.ManagementGroupDisplayName = $this.GetManagementGroupName() } if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) { $this.StatePath = (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.subscription_subscriptions-$($this.Subscription).json").ToLower()) } else { $this.StatePath = (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath (("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.subscription_subscriptions-$($this.Subscription)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))).ToLower()) } } elseif ($this.IsManagementGroup()) { $this.Type = "managementGroups" $this.ResourceProvider = "Microsoft.Management" $this.Resource = "managementGroups" $this.Name = $this.GetManagementGroup() $this.ManagementGroup = ($this.GetManagementGroup()).Trim() $this.ManagementGroupDisplayName = ($this.GetManagementGroupName()).Trim() if (Get-PSFConfigValue -FullName AzOps.Core.ExportRawTemplate) { $this.StatePath = (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath ("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.management_managementgroups-$($this.ManagementGroup).json").ToLower()) } else { $this.StatePath = (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath (("$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')\microsoft.management_managementgroups-$($this.ManagementGroup)" + $(Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'))).ToLower()) } } elseif ($this.IsRoot()) { $this.Type = "root" $this.Name = "/" $this.StatePath = $this.StateRoot.ToLower() } Write-PSFMessage -Level Verbose -String 'AzOpsScope.InitializeMemberVariables.End' -StringValues ($scope) -FunctionName InitializeMemberVariables -ModuleName AzOps } #endregion Initializers [String] ToString() { return $this.Scope } #region Validators [bool] IsRoot() { if (($this.Scope -match $this.regex_tenant)) { return $true } return $false } [bool] IsManagementGroup() { if (($this.Scope -match $this.regex_managementgroup)) { return $true } return $false } [string] IsSubscription() { if (($this.Scope -match $this.regex_subscription)) { return ($this.Scope.Split('/')[2]) } return $null } [string] IsResourceGroup() { if (($this.Scope -match $this.regex_resourceGroup)) { return ($this.Scope.Split('/')[4]) } return $null } [string] IsResourceProvider() { if ($this.Scope -match $this.regex_managementgroupProvider) { return (($this.regex_managementgroupProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[1] } if ($this.Scope -match $this.regex_subscriptionProvider) { return (($this.regex_subscriptionProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[1] } if ($this.Scope -match $this.regex_resourceGroupProvider) { return (($this.regex_resourceGroupProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[1] } return $null } [string] IsResource() { if ($this.Scope -match $this.regex_managementgroupResource) { return ($this.regex_managementgroupResource.Split($this.Scope) | Select-Object -last 1) } if ($this.Scope -match $this.regex_subscriptionResource) { return ($this.regex_subscriptionResource.Split($this.Scope) | Select-Object -last 1) } if ($this.Scope -match $this.regex_resourceGroupResource) { return ($this.regex_resourceGroupResource.Split($this.Scope) | Select-Object -last 1) } return $null } #endregion Validators #region Data Accessors <# Should Return Management Group Name #> [string] GetManagementGroup() { if ($this.GetManagementGroupName()) { foreach ($mgmt in $script:AzOpsAzManagementGroup) { if ($mgmt.DisplayName -eq $this.GetManagementGroupName()) { return $mgmt.Name } } } if ($this.Subscription) { foreach ($mgmt in $script:AzOpsAzManagementGroup) { foreach ($child in $mgmt.Children) { if ($child.DisplayName -eq $this.subscriptionDisplayName) { return $mgmt.Name } } } } return $null } [string] GetAzOpsManagementGroupPath([string]$managementgroupName) { if ($groupObject = $script:AzOpsAzManagementGroup | Where-Object Name -eq $managementgroupName) { $parentMgName = $groupObject.parentId -split "/" | Select-Object -Last 1 $parentObject = $script:AzOpsAzManagementGroup | Where-Object Name -eq $parentMgName if ($groupObject.parentId -and $parentObject) { $parentPath = $this.GetAzOpsManagementGroupPath($parentMgName) $childPath = "{0} ({1})" -f $groupObject.DisplayName, $groupObject.Name return Join-Path $parentPath -ChildPath ($childPath.ToLower()) } else { $childPath = "{0} ({1})" -f $groupObject.DisplayName, $groupObject.Name return Join-Path $this.StateRoot -ChildPath ($childPath.ToLower()) } } else { Write-PSFMessage -Level Warning -Tag error -String 'AzOpsScope.GetAzOpsManagementGroupPath.NotFound' -StringValues $managementgroupName -FunctionName AzOpsScope -ModuleName AzOps throw "Management Group not found: $managementgroupName" } } <# Should Return Management Group Display Name #> [string] GetManagementGroupName() { if ($this.Scope -match $this.regex_managementgroupExtract) { $mgId = $this.Scope -split $this.regex_managementgroupExtract -split '/' | Where-Object { $_ } | Select-Object -First 1 if ($mgId) { $mgDisplayName = ($script:AzOpsAzManagementGroup | Where-Object Name -eq $mgId).DisplayName if ($mgDisplayName) { #Write-PSFMessage -Level Debug -String 'AzOpsScope.GetManagementGroupName.Found.Azure' -StringValues $mgDisplayName -FunctionName AzOpsScope -ModuleName AzOps return $mgDisplayName } else { Write-PSFMessage -Level Debug -String 'AzOpsScope.GetManagementGroupName.NotFound' -StringValues $mgId -FunctionName AzOpsScope -ModuleName AzOps return $mgId } } } if ($this.Subscription) { foreach ($managementGroup in $script:AzOpsAzManagementGroup) { foreach ($child in $managementGroup.Children) { if ($child.DisplayName -eq $this.subscriptionDisplayName) { return $managementGroup.DisplayName } } } } return $null } [string] GetAzOpsSubscriptionPath() { $childpath = "{0} ({1})" -f $this.SubscriptionDisplayName, $this.Subscription if ($script:AzOpsAzManagementGroup) { return (Join-Path $this.GetAzOpsManagementGroupPath($this.ManagementGroup) -ChildPath ($childpath).ToLower()) } else { return (Join-Path $this.StateRoot -ChildPath ($childpath).ToLower()) } } [string] GetAzOpsResourceGroupPath() { return (Join-Path $this.GetAzOpsSubscriptionPath() -ChildPath ($this.ResourceGroup).ToLower()) } [string] GetSubscription() { if ($this.Scope -match $this.regex_subscriptionExtract) { $subId = $this.Scope -split $this.regex_subscriptionExtract -split '/' | Where-Object { $_ } | Select-Object -First 1 $sub = $script:AzOpsSubscriptions | Where-Object subscriptionId -eq $subId if ($sub) { Write-PSFMessage -Level Debug -String 'AzOpsScope.GetSubscription.Found' -StringValues $sub.Id -FunctionName AzOpsScope -ModuleName AzOps return $sub.subscriptionId } else { Write-PSFMessage -Level Debug -String 'AzOpsScope.GetSubscription.NotFound' -StringValues $subId -FunctionName AzOpsScope -ModuleName AzOps return $subId } } return $null } [string] GetSubscriptionDisplayName() { if ($this.Scope -match $this.regex_subscriptionExtract) { $subId = $this.Scope -split $this.regex_subscriptionExtract -split '/' | Where-Object { $_ } | Select-Object -First 1 $sub = $script:AzOpsSubscriptions | Where-Object subscriptionId -eq $subId if ($sub) { Write-PSFMessage -Level Debug -String 'AzOpsScope.GetSubscriptionDisplayName.Found' -StringValues $sub.displayName -FunctionName AzOpsScope -ModuleName AzOps return $sub.displayName } else { Write-PSFMessage -Level Debug -String 'AzOpsScope.GetSubscriptionDisplayName.NotFound' -StringValues $subId -FunctionName AzOpsScope -ModuleName AzOps return $subId } } return $null } [string] GetResourceGroup() { if ($this.Scope -match $this.regex_resourceGroupExtract) { return ($this.Scope -split $this.regex_resourceGroupExtract -split '/' | Where-Object { $_ } | Select-Object -First 1) } return $null } [string] GetResource() { if ($this.Scope -match $this.regex_managementgroupProvider) { return (($this.regex_managementgroupProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[2] } if ($this.Scope -match $this.regex_subscriptionProvider) { return (($this.regex_subscriptionProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[2] } if ($this.Scope -match $this.regex_resourceGroupProvider) { return (($this.regex_resourceGroupProvider.Split($this.Scope) | Select-Object -last 1) -split '/')[2] } return $null } [string] GetAzOpsResourcePath() { Write-PSFMessage -Level Debug -String 'AzOpsScope.GetAzOpsResourcePath.Retrieving' -StringValues $this.Scope -FunctionName AzOpsScope -ModuleName AzOps if ($this.Scope -match $this.regex_resourceGroupResource) { $rgpath = $this.GetAzOpsResourceGroupPath() return (Join-Path (Join-Path $rgpath -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')") -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $this.Name).ToLower()) } elseif ($this.Scope -match $this.regex_subscriptionResource) { $subpath = $this.GetAzOpsSubscriptionPath() return (Join-Path (Join-Path $subpath -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')") -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $this.Name).ToLower()) } elseif ($this.Scope -match $this.regex_managementgroupResource) { $mgmtPath = $this.GetAzOpsManagementGroupPath($this.ManagementGroup) return (Join-Path (Join-Path $mgmtPath -ChildPath "$(Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath')") -ChildPath ($this.ResourceProvider + "_" + $this.Resource + "-" + $this.Name).ToLower()) } Write-PSFMessage -Level Warning -Tag error -String 'AzOpsScope.GetAzOpsResourcePath.NotFound' -StringValues $this.Scope -FunctionName AzOpsScope -ModuleName AzOps throw "Unable to determine Resource Scope for: $($this.Scope)" } #endregion Data Accessors } function Assert-AzOpsInitialization { <# .SYNOPSIS Asserts AzOps has been correctly prepare for execution. .DESCRIPTION Asserts AzOps has been correctly prepare for execution. This boils down to Initialize-AzOpsEnvironment having been executed successfully. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command. .PARAMETER StatePath Path to where the AzOps processing state / repository is located at. .EXAMPLE > Assert-AzOpsInitialization -Cmdlet $PSCmdlet -Statepath $StatePath Asserts AzOps has been correctly prepare for execution. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $Cmdlet, [Parameter(Mandatory = $true)] [string] $StatePath ) begin { $strings = Get-PSFLocalizedString -Module AzOps $invalidPathPattern = [System.IO.Path]::GetInvalidPathChars() -replace '\|', '\|' -join "|" } process { $stateGood = $StatePath -and $StatePath -notmatch $invalidPathPattern if (-not $stateGood) { Write-PSFMessage -Level Warning -String 'Assert-AzOpsInitialization.StateError' -Tag error $exception = [System.InvalidOperationException]::new($strings.'Assert-AzOpsInitialization.StateError') $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "BadData", 'InvalidData', $null) } $cacheBuilt = $script:AzOpsSubscriptions -or $script:AzOpsAzManagementGroup if (-not $cacheBuilt) { Write-PSFMessage -Level Warning -String 'Assert-AzOpsInitialization.NoCache' -Tag error $exception = [System.InvalidOperationException]::new($strings.'Assert-AzOpsInitialization.NoCache') $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "NoCache", 'InvalidData', $null) } if (-not $stateGood -or -not $cacheBuilt) { $Cmdlet.ThrowTerminatingError($errorRecord) } } } function Assert-AzOpsJQDependency { <# .SYNOPSIS Asserts that - if jq is installed and in current path .DESCRIPTION Asserts that - if jq is installed and in current path .PARAMETER Cmdlet The $PSCmdlet variable of the calling command. .EXAMPLE > Assert-AzOpsJQDependency -Cmdlet $PSCmdlet #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] $Cmdlet ) process { Write-PSFMessage -Level InternalComment -String 'Assert-AzOpsJQDependency.Validating' $result = (Invoke-AzOpsNativeCommand -ScriptBlock { jq --version } -IgnoreExitcode) -as [bool] if ($result) { Write-PSFMessage -Level InternalComment -String 'Assert-AzOpsJQDependency.Success' return } $exception = [System.InvalidOperationException]::new('JQ is not in current path') $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "ConfigurationError", 'InvalidOperation', $null) Write-PSFMessage -Level Warning -String 'Assert-AzOpsJQDependency.Failed' -Tag error $Cmdlet.ThrowTerminatingError($errorRecord) } } function Assert-AzOpsWindowsLongPath { <# .SYNOPSIS Asserts that - if on windows - long paths have been enabled. .DESCRIPTION Asserts that - if on windows - long paths have been enabled. .PARAMETER Cmdlet The $PSCmdlet variable of the calling command. .EXAMPLE > Assert-AzOpsWindowsLongPath -Cmdlet $PSCmdlet Asserts that - if on windows - long paths have been enabled. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $Cmdlet ) process { if (-not $IsWindows) { return } Write-PSFMessage -Level InternalComment -String 'Assert-AzOpsWindowsLongPath.Validating' $hasRegKey = 1 -eq (Get-ItemPropertyValue -Path HKLM:SYSTEM\CurrentControlSet\Control\FileSystem -Name LongPathsEnabled) $hasGitConfig = (Invoke-AzOpsNativeCommand -ScriptBlock { git config --system -l } -IgnoreExitcode | Select-String 'core.longpaths=true') -as [bool] if ($hasGitConfig -and $hasRegKey) { return } if (-not $hasRegKey) { Write-PSFMessage -Level Warning -String 'Assert-AzOpsWindowsLongPath.No.Registry' } if (-not $hasGitConfig) { Write-PSFMessage -Level Warning -String 'Assert-AzOpsWindowsLongPath.No.GitCfg' } $exception = [System.InvalidOperationException]::new('Windows not configured for long paths. Please follow instructions for "Enabling long paths on Windows" on https://aka.ms/es/quickstart.') $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, "ConfigurationError", 'InvalidOperation', $null) Write-PSFMessage -Level Warning -String 'Assert-AzOpsWindowsLongPath.Failed' -Tag error $Cmdlet.ThrowTerminatingError($errorRecord) } } function ConvertTo-AzOpsState { <# .SYNOPSIS The cmdlet converts Azure resources (Resources/ResourceGroups/Policy/PolicySet/PolicyAssignments/RoleAssignment/Definition) to the AzOps state format and exports them to the file structure. .DESCRIPTION The cmdlet converts Azure resources (Resources/ResourceGroups/Policy/PolicySet/PolicyAssignments/RoleAssignment/Definition) to the AzOps state format and exports them to the file structure. It is normally executed and orchestrated through the Initialize-AzOpsRepository cmdlet. As most of the AzOps-cmdlets, it is dependant on the AzOpsAzManagementGroup and AzOpsSubscriptions variables. Cmdlet will look into jq filter is template directory for the specific one before using the generic one at the root of the module .PARAMETER Resource Object with resource as input .PARAMETER ExportPath ExportPath is used if resource needs to be exported to other path than the AzOpsScope path .PARAMETER ReturnObject Used if to return object in pipeline instead of exporting file .PARAMETER ExportRawTemplate Used in cases you want to return the template without the custom parameters json schema .PARAMETER StatePath The root path to where the entire state is being built in. .EXAMPLE Initialize-AzOpsEnvironment $policy = Get-AzPolicyDefinition -Custom | Select-Object -Last 1 ConvertTo-AzOpsState -Resource $policy Export custom policy definition to the AzOps StatePath .EXAMPLE Initialize-AzOpsEnvironment $policy = Get-AzPolicyDefinition -Custom | Select-Object -Last 1 ConvertTo-AzOpsState -Resource $policy -ReturnObject Name Value ---- ----- $schema http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json# contentVersion 1.0.0.0 parameters {input} Serialize custom policy definition to the AzOps format, return object instead of export file .INPUTS Resource .OUTPUTS Resource in AzOpsState json format or object returned as [PSCustomObject] depending on parameters used #> [CmdletBinding()] [OutputType([PSCustomObject])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [Alias('MG', 'Role', 'Assignment', 'CustomObject', 'ResourceGroup')] $Resource, [string] $ExportPath, [switch] $ReturnObject, [switch] $ExportRawTemplate, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string] $StatePath, [string] $JqTemplatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.JqTemplatePath') ) begin { Write-PSFMessage -Level Debug -String 'ConvertTo-AzOpsState.Starting' } process { Write-PSFMessage -Level Debug -String 'ConvertTo-AzOpsState.Processing' -StringValues $Resource if (-not $ExportPath) { if ($Resource.Id) { $objectFilePath = (New-AzOpsScope -scope $Resource.id -StatePath $StatePath).statepath } elseif ($Resource.ResourceId) { $objectFilePath = (New-AzOpsScope -scope $Resource.ResourceId -StatePath $StatePath).statepath } else { Write-PSFMessage -Level Error -String "ConvertTo-AzOpsState.NoExportPath" -StringValues $Resource.GetType() } } else { $objectFilePath = $ExportPath } # Create folder structure if it doesn't exist if (-not (Test-Path -Path $objectFilePath)) { Write-PSFMessage -String 'ConvertTo-AzOpsState.File.Create' -StringValues $objectFilePath $null = New-Item -Path $objectFilePath -ItemType "file" -Force } else { Write-PSFMessage -String 'ConvertTo-AzOpsState.File.UseExisting' -StringValues $objectFilePath } # if the export file path ends with parameter $generateTemplateParameter = $objectFilePath.EndsWith('.parameters.json') ? $true : $false Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplateParameter' -StringValues "$generateTemplateParameter" -FunctionName 'ConvertTo-AzOpsState' $resourceType = $null switch ($Resource) { { $_.ResourceType } { Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.ResourceType' -StringValues "$($Resource.ResourceType)" -FunctionName 'ConvertTo-AzOpsState' $resourceType = $_.ResourceType break } # Management Groups { $_ -is [Microsoft.Azure.Commands.Resources.Models.ManagementGroups.PSManagementGroup] -or $_ -is [Microsoft.Azure.Commands.Resources.Models.ManagementGroups.PSManagementGroupChildInfo] } { Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState' if ($_.Type -eq "/subscriptions") { $resourceType = 'Microsoft.Management/managementGroups/subscriptions' break } else { $resourceType = 'Microsoft.Management/managementGroups' break } } # Subscriptions { $_ -is [Microsoft.Azure.Commands.Profile.Models.PSAzureSubscription] } { Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState' $resourceType = 'Microsoft.Subscription/subscriptions' break } # Resource Groups { $_ -is [Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResourceGroup] } { Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState' $resourceType = 'Microsoft.Resources/resourceGroups' break } # Resources - Controlled group for raw objects { $_ -is [Microsoft.Azure.Commands.Profile.Models.PSAzureTenant] } { Write-PSFMessage -String 'ConvertTo-AzOpsState.ObjectType.Resolved.PSObject' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState' break } Default { Write-PSFMessage -Level Warning -String 'ConvertTo-AzOpsState.ObjectType.Resolved.Generic' -StringValues "$($_.GetType())" -FunctionName 'ConvertTo-AzOpsState' break } } if ($resourceType) { $providerNamespace = ($resourceType -split '/' | Select-Object -First 1) Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ProviderNamespace' -StringValues $providerNamespace -FunctionName 'ConvertTo-AzOpsState' if (($resourceType -split '/').Count -eq 2) { $resourceTypeName = (($resourceType -split '/', 2) | Select-Object -Last 1) Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ResourceTypeName' -StringValues $resourceTypeName -FunctionName 'ConvertTo-AzOpsState' $resourceApiTypeName = (($resourceType -split '/', 2) | Select-Object -Last 1) Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ResourceApiTypeName' -StringValues $resourceApiTypeName -FunctionName 'ConvertTo-AzOpsState' } if (($resourceType -split '/').Count -eq 3) { $resourceTypeName = ((($resourceType -split '/', 3) | Select-Object -Last 2) -join '/') Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ResourceTypeName' -StringValues $resourceTypeName -FunctionName 'ConvertTo-AzOpsState' $resourceApiTypeName = (($resourceType -split '/', 3) | Select-Object -Index 1) Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ResourceApiTypeName' -StringValues $resourceApiTypeName -FunctionName 'ConvertTo-AzOpsState' } $jqRemoveTemplate = ( (Test-Path (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.jq"))) ? (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.jq")): (Join-Path $JqTemplatePath -ChildPath "generic.jq") ) Write-PSFMessage -String 'ConvertTo-AzOpsState.Jq.Remove' -StringValues $jqRemoveTemplate -FunctionName 'ConvertTo-AzOpsState' # If we were able to determine resourceType, apply filter and write template or template parameter files based on output filename. $object = $Resource | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r -f $jqRemoveTemplate | ConvertFrom-Json if ($ReturnObject) { return $object } else { if ($generateTemplateParameter) { #region Generating Template Parameter Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplateParameter' -FunctionName 'ConvertTo-AzOpsState' $jqJsonTemplate = (Test-Path (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.parameters.jq"))) ? (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.parameters.jq")): (Join-Path $JqTemplatePath -ChildPath "template.parameters.jq") Write-PSFMessage -String 'ConvertTo-AzOpsState.Jq.Template' -StringValues $jqJsonTemplate -FunctionName 'ConvertTo-AzOpsState' $object = ($object | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r -f $jqJsonTemplate | ConvertFrom-Json) #endregion } else { #region Generating Template Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate' -StringValues "$true" -FunctionName 'ConvertTo-AzOpsState' $jqJsonTemplate = (Test-Path (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.template.jq"))) ? (Join-Path $JqTemplatePath -ChildPath (Join-Path $providerNamespace -ChildPath "$resourceTypeName.template.jq")): (Join-Path $JqTemplatePath -ChildPath "template.jq") Write-PSFMessage -String 'ConvertTo-AzOpsState.Jq.Template' -StringValues $jqJsonTemplate -FunctionName 'ConvertTo-AzOpsState' $object = ($object | ConvertTo-Json -Depth 100 -EnumsAsStrings | jq -r -f $jqJsonTemplate | ConvertFrom-Json) #endregion #region Replace Resource Type and API Version if ( ($Script:AzOpsResourceProvider | Where-Object { $_.ProviderNamespace -eq $providerNamespace }) -and (($Script:AzOpsResourceProvider | Where-Object { $_.ProviderNamespace -eq $providerNamespace }).ResourceTypes | Where-Object { $_.ResourceTypeName -eq $resourceApiTypeName }) ) { $apiVersions = (($Script:AzOpsResourceProvider | Where-Object { $_.ProviderNamespace -eq $providerNamespace }).ResourceTypes | Where-Object { $_.ResourceTypeName -eq $resourceApiTypeName }).ApiVersions[0] Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ApiVersion' -StringValues $resourceType, $apiVersions -FunctionName 'ConvertTo-AzOpsState' $object.resources[0].apiVersion = $apiVersions $object.resources[0].type = $resourceType } else { Write-PSFMessage -Level Warning -String 'ConvertTo-AzOpsState.GenerateTemplate.NoApiVersion' -StringValues $resourceType -FunctionName 'ConvertTo-AzOpsState' } #endregion #region Append Name for child resource # [Patch] Temporary until mangementGroup() is fully implemented if ($resourceType -eq "Microsoft.Management/managementGroups/subscriptions") { $resourceName = (((New-AzOpsScope -Scope $Resource.Id).ManagementGroup) + "/" + $Resource.Name) $object.resources[0].name = $resourceName Write-PSFMessage -String 'ConvertTo-AzOpsState.GenerateTemplate.ChildResource' -StringValues $resourceName -FunctionName 'ConvertTo-AzOpsState' } #endregion } Write-PSFMessage -String 'ConvertTo-AzOpsState.Exporting' -StringValues $objectFilePath -FunctionName 'ConvertTo-AzOpsState' ConvertTo-Json -InputObject $object -Depth 100 -EnumsAsStrings | Set-Content -Path ([WildcardPattern]::Escape($objectFilePath)) -Encoding UTF8 -Force } } else { Write-PSFMessage -String 'ConvertTo-AzOpsState.Exporting.Default' -StringValues $objectFilePath -FunctionName 'ConvertTo-AzOpsState' if ($ReturnObject) { return $Resource } else { ConvertTo-Json -InputObject $Resource -Depth 100 -EnumsAsStrings | Set-Content -Path ([WildcardPattern]::Escape($objectFilePath)) -Encoding UTF8 -Force } } } } function Get-AzOpsManagementGroups { <# .SYNOPSIS The cmdlet will recursively enumerates a management group and returns all children .DESCRIPTION The cmdlet will recursively enumerates a management group and returns all children mgs. If the -PartialDiscovery parameter has been used, it will add all MG's where discovery should initiate to the AzOpsPartialRoot variable. .PARAMETER ManagementGroup Name of the management group to enumerate .PARAMETER PartialDiscovery Whether to recursively grab all Management Groups and add them to the partial root cache .EXAMPLE Get-AzOpsManagementGroups -ManagementGroup Tailspin Id : /providers/Microsoft.Management/managementGroups/Tailspin Type : /providers/Microsoft.Management/managementGroups Name : Tailspin TenantId : d4c7591d-9b0c-49a4-9670-5f0349b227f1 DisplayName : Tailspin UpdatedTime : 0001-01-01 00:00:00 UpdatedBy : ParentId : /providers/Microsoft.Management/managementGroups/d4c7591d-9b0c-49a4-9670-5f0349b227f1 ParentName : d4c7591d-9b0c-49a4-9670-5f0349b227f1 ParentDisplayName : Tenant Root Group .INPUTS ManagementGroupName .OUTPUTS Management Group Object #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateScript( { Get-AzManagementGroup -GroupId $_ -WarningAction SilentlyContinue })] $ManagementGroup, [switch] $PartialDiscovery ) process { $groupObject = Get-AzManagementGroup -GroupId $ManagementGroup -Expand -WarningAction SilentlyContinue if ($PartialDiscovery) { if ($groupObject.ParentId -and -not (Get-AzManagementGroup -GroupId $groupObject.ParentName -ErrorAction Ignore -WarningAction SilentlyContinue)) { $script:AzOpsPartialRoot += $groupObject } if ($groupObject.Children) { $groupObject.Children | Where-Object Type -eq "/providers/Microsoft.Management/managementGroups" | Foreach-Object -Process { Get-AzOpsManagementGroups -ManagementGroup $_.Name -PartialDiscovery:$PartialDiscovery } } } $groupObject } } function Get-AzOpsPolicyAssignment { <# .SYNOPSIS Discover all custom policy assignments at the provided scope (Management Groups, subscriptions or resource groups) .DESCRIPTION Discover all custom policy assignments at the provided scope (Management Groups, subscriptions or resource groups) .PARAMETER ScopeObject The scope object representing the azure entity to retrieve policyset definitions for. .EXAMPLE > Get-AzOpsPolicyAssignment -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath) Discover all custom policy assignments deployed at Management Group scope #> [OutputType([Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.Policy.PsPolicyAssignment])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [AzOpsScope] $ScopeObject ) process { #TODO: Discuss dropping resourcegroups, as no action is taken ever if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { return } switch ($ScopeObject.Type) { managementGroups { Write-PSFMessage -String 'Get-AzOpsPolicyAssignment.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject } subscriptions { Write-PSFMessage -String 'Get-AzOpsPolicyAssignment.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject } resourcegroups { Write-PSFMessage -String 'Get-AzOpsPolicyAssignment.ResourceGroup' -StringValues $ScopeObject.ResourceGroup -Target $ScopeObject } } Get-AzPolicyAssignment -Scope $ScopeObject.Scope -WarningAction SilentlyContinue | Where-Object PolicyAssignmentId -match $ScopeObject.scope } } function Get-AzOpsPolicyDefinition { <# .SYNOPSIS Discover all custom policy definitions at the provided scope (Management Groups, subscriptions or resource groups) .DESCRIPTION Discover all custom policy definitions at the provided scope (Management Groups, subscriptions or resource groups) .PARAMETER ScopeObject The scope object representing the azure entity to retrieve policy definitions for. .EXAMPLE > Get-AzOpsPolicyDefinition -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath) Discover all custom policy definitions deployed at Management Group scope #> [OutputType([Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.Policy.PsPolicyDefinition])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [AzOpsScope] $ScopeObject ) process { #TODO: Discuss dropping resourcegroups, as no action is taken ever if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { return } switch ($ScopeObject.Type) { managementGroups { Write-PSFMessage -String 'Get-AzOpsPolicyDefinition.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject Get-AzPolicyDefinition -Custom -ManagementGroupName $ScopeObject.Name | Where-Object ResourceId -match $ScopeObject.Scope } subscriptions { Write-PSFMessage -String 'Get-AzOpsPolicyDefinition.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject Get-AzPolicyDefinition -Custom -SubscriptionId $ScopeObject.Scope.Split('/')[2] | Where-Object SubscriptionId -eq $ScopeObject.Name } } } } function Get-AzOpsPolicySetDefinition { <# .SYNOPSIS Discover all custom policyset definitions at the provided scope (Management Groups, subscriptions or resource groups) .DESCRIPTION Discover all custom policyset definitions at the provided scope (Management Groups, subscriptions or resource groups) .PARAMETER ScopeObject The scope object representing the azure entity to retrieve policyset definitions for. .EXAMPLE > Get-AzOpsPolicySetDefinition -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath) Discover all custom policyset definitions deployed at Management Group scope #> [OutputType([Microsoft.Azure.Commands.ResourceManager.Cmdlets.Implementation.Policy.PsPolicySetDefinition])] [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [AzOpsScope] $ScopeObject ) process { #TODO: Discuss dropping resourcegroups, as no action is taken ever if ($ScopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { return } switch ($ScopeObject.Type) { managementGroups { Write-PSFMessage -String 'Get-AzOpsPolicySetDefinition.ManagementGroup' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup -Target $ScopeObject Get-AzPolicySetDefinition -Custom -ManagementGroupName $ScopeObject.Name | Where-Object ResourceId -match $ScopeObject.Scope } subscriptions { Write-PSFMessage -String 'Get-AzOpsPolicySetDefinition.Subscription' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -Target $ScopeObject Get-AzPolicySetDefinition -Custom -SubscriptionId $ScopeObject.Scope.Split('/')[2] | Where-Object SubscriptionId -eq $ScopeObject.Name } } } } function Get-AzOpsResourceDefinition { <# .SYNOPSIS This cmdlet recursively discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Policies, Role Assignments) from the provided input scope. .DESCRIPTION This cmdlet recursively discovers resources (Management Groups, Subscriptions, Resource Groups, Resources, Policies, Role Assignments) from the provided input scope. .PARAMETER Scope Discovery Scope .PARAMETER SkipPolicy Skip discovery of policies for better performance. .PARAMETER SkipRole Skip discovery of roles for better performance. .PARAMETER SkipResourceGroup Skip discovery of resource groups. .PARAMETER SkipResource Skip discovery of resources inside resource groups. .PARAMETER ExportRawTemplate Export generic templates without embedding them in the parameter block. .PARAMETER StatePath The root folder under which to write the resource json. .EXAMPLE $TenantRootId = '/providers/Microsoft.Management/managementGroups/{0}' -f (Get-AzTenant).Id Get-AzOpsResourceDefinition -scope $TenantRootId -Verbose Discover all resources from root Management Group .EXAMPLE Get-AzOpsResourceDefinition -scope /providers/Microsoft.Management/managementGroups/landingzones -SkipPolicy -SkipResourceGroup Discover all resources from child Management Group, skip discovery of policies and resource groups .EXAMPLE Get-AzOpsResourceDefinition -scope /subscriptions/623625ae-cfb0-4d55-b8ab-0bab99cbf45c Discover all resources from Subscription level .EXAMPLE Get-AzOpsResourceDefinition -scope /subscriptions/623625ae-cfb0-4d55-b8ab-0bab99cbf45c/resourceGroups/myresourcegroup Discover all resources from resource group level .EXAMPLE Get-AzOpsResourceDefinition -scope /subscriptions/623625ae-cfb0-4d55-b8ab-0bab99cbf45c/resourceGroups/contoso-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.database.windows.net Discover a single resource #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $Scope, [switch] $SkipPolicy = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipPolicy'), [switch] $SkipRole = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipRole'), [switch] $SkipResourceGroup = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipResourceGroup'), [switch] $SkipResource = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipResource'), [switch] $ExportRawTemplate = (Get-PSFConfigValue -FullName 'AzOps.Core.ExportRawTemplate'), [Parameter(Mandatory = $false)] [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State') ) begin { #region Utility Functions function ConvertFrom-TypeResource { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [AzOpsScope] $ScopeObject, [string] $StatePath, [switch] $ExportRawTemplate ) process { $common = @{ FunctionName = 'Get-AzOpsResourceDefinition' Target = $ScopeObject } Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Resource.Processing' -StringValues $ScopeObject.Resource, $ScopeObject.ResourceGroup try { $resource = Get-AzResource -ResourceId $ScopeObject.scope -ErrorAction Stop ConvertTo-AzOpsState -Resource $resource -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate } catch { Write-PSFMessage @common -Level Warning -String 'Get-AzOpsResourceDefinition.Resource.Processing.Failed' -StringValues $ScopeObject.Resource, $ScopeObject.ResourceGroup -ErrorRecord $_ } } } function ConvertFrom-TypeResourceGroup { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [AzOpsScope] $ScopeObject, [switch] $SkipResource, [string] $StatePath, [switch] $ExportRawTemplate, $Context, [string] $OdataFilter ) process { $common = @{ FunctionName = 'Get-AzOpsResourceDefinition' Target = $ScopeObject } Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing' -StringValues $ScopeObject.Resourcegroup, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription try { $resourceGroup = Get-AzResourceGroup -Name $ScopeObject.ResourceGroup -DefaultProfile $Context -ErrorAction Stop } catch { Write-PSFMessage @common -Level Warning -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Error' -StringValues $ScopeObject.Resourcegroup, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription -ErrorRecord $_ return } if ($resourceGroup.ManagedBy) { Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.ResourceGroup.Processing.Owned' -StringValues $resourceGroup.ResourceGroupName, $resourceGroup.ManagedBy return } ConvertTo-AzOpsState -Resource $resourceGroup -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath # Get all resources in resource groups $paramGetAzResource = @{ DefaultProfile = $Context ResourceGroupName = $resourceGroup.ResourceGroupName ODataQuery = $OdataFilter ExpandProperties = $true } Get-AzResource @paramGetAzResource | ForEach-Object { New-AzOpsScope -Scope $_.ResourceId } | ConvertFrom-TypeResource -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate } } function ConvertFrom-TypeSubscription { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [AzOpsScope] $ScopeObject, [string] $StatePath, $Context, [switch] $ExportRawTemplate, [switch] $SkipResourceGroup, [switch] $SkipResource, [string] $ODataFilter ) begin { # Set variables for retry with exponential backoff $backoffMultiplier = 2 $maxRetryCount = 6 } process { $common = @{ FunctionName = 'Get-AzOpsResourceDefinition' Target = $ScopeObject } Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Subscription.Processing' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription # Skip discovery of resource groups if SkipResourceGroup switch have been used # Separate discovery of resource groups in subscriptions to support parallel discovery if ($SkipResourceGroup) { Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Subscription.SkippingResourceGroup' } else { # Get all Resource Groups in Subscription # Retry loop with exponential back off implemented to catch errors # Introduced due to error "Your Azure Credentials have not been set up or expired" # https://github.com/Azure/azure-powershell/issues/9448 # Define variables used by script if ( (((Get-PSFConfigValue -FullName 'AzOps.Core.SubscriptionsToIncludeResourceGroups') | Foreach-Object { $scopeObject.Subscription -like $_ }) -contains $true) -or (((Get-PSFConfigValue -FullName 'AzOps.Core.SubscriptionsToIncludeResourceGroups') | Foreach-Object { $scopeObject.SubscriptionDisplayName -like $_ }) -contains $true) ) { $resourceGroups = Invoke-AzOpsScriptBlock -ArgumentList $Context -ScriptBlock { param ($Context) Get-AzResourceGroup -DefaultProfile ($Context | Write-Output) -ErrorAction Stop | Where-Object { -not $_.ManagedBy } } -RetryCount $maxRetryCount -RetryWait $backoffMultiplier -RetryType Exponential if (-not $resourceGroups) { Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Subscription.NoResourceGroup' -StringValues $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription } #region Prepare Input Data for parallel processing $runspaceData = @{ AzOpsPath = "$($script:ModuleRoot)\AzOps.psd1" StatePath = $StatePath ScopeObject = $ScopeObject ODataFilter = $ODataFilter SkipResource = $SkipResource MaxRetryCount = $maxRetryCount BackoffMultiplier = $backoffMultiplier ExportRawTemplate = $ExportRawTemplate runspace_AzOpsAzManagementGroup = $script:AzOpsAzManagementGroup runspace_AzOpsSubscriptions = $script:AzOpsSubscriptions runspace_AzOpsPartialRoot = $script:AzOpsPartialRoot runspace_AzOpsResourceProvider = $script:AzOpsResourceProvider } #endregion Prepare Input Data for parallel processing #region Discover all resource groups in parallel $resourceGroups | Foreach-Object -ThrottleLimit (Get-PSFConfigValue -FullName 'AzOps.Core.ThrottleLimit') -Parallel { $resourceGroup = $_ $runspaceData = $using:runspaceData $msgCommon = @{ FunctionName = 'Get-AzOpsResourceDefinition' ModuleName = 'AzOps' } # region Importing module # We need to import all required modules and declare variables again because of the parallel runspaces # https://devblogs.microsoft.com/powershell/powershell-foreach-object-parallel-feature/ Import-Module "$([PSFramework.PSFCore.PSFCoreHost]::ModuleRoot)/PSFramework.psd1" $azOps = Import-Module $runspaceData.AzOpsPath -Force -PassThru # endregion Importing module & $azOps { $script:AzOpsAzManagementGroup = $runspaceData.runspace_AzOpsAzManagementGroup $script:AzOpsSubscriptions = $runspaceData.runspace_AzOpsSubscriptions $script:AzOpsPartialRoot = $runspaceData.runspace_AzOpsPartialRoot $script:AzOpsResourceProvider = $runspaceData.runspace_AzOpsResourceProvider } $context = Get-AzContext -ListAvailable | Where-Object { $_.Subscription.id -eq $runspaceData.ScopeObject.Subscription } Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.ResourceGroup' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup & $azOps { ConvertTo-AzOpsState -Resource $resourceGroup -ExportRawTemplate:$runspaceData.ExportRawTemplate -StatePath $runspaceData.Statepath } if (-not $using:SkipResource) { Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.ResourceGroup.Resources' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup $resources = & $azOps { $parameters = @{ DefaultProfile = $Context | Select-Object -First 1 ODataQuery = $runspaceData.ODataFilter } if ($resourceGroup.ResourceGroupName) { $parameters.ResourceGroupName = $resourceGroup.ResourceGroupName } Invoke-AzOpsScriptBlock -ArgumentList $parameters -ScriptBlock { param ( $Parameters ) $param = $Parameters | Write-Output Get-AzResource @param -ExpandProperties -ErrorAction Stop } -RetryCount $runspaceData.MaxRetryCount -RetryWait $runspaceData.BackoffMultiplier -RetryType Exponential } if (-not $resources) { Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.ResourceGroup.NoResources' -StringValues $resourceGroup.ResourceGroupName -Target $resourceGroup } # Loop through resources and convert them to AzOpsState foreach ($resource in $resources) { # Convert resources to AzOpsState Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.SubScription.Processing.Resource' -StringValues $resource.Name, $resourceGroup.ResourceGroupName -Target $resource & $azOps { ConvertTo-AzOpsState -Resource $resource -ExportRawTemplate:$runspaceData.ExportRawTemplate -StatePath $runspaceData.Statepath } } } else { Write-PSFMessage @msgCommon -String 'Get-AzOpsResourceDefinition.Subscription.SkippingResources' } } #endregion Discover all resource groups in parallel } else { Write-PSFMessage @common -String 'Get-AzOpsResourceDefinition.Subscription.ExcludeResourceGroup' } } if ($subscriptionItem = $script:AzOpsAzManagementGroup.children | Where-Object Name -eq $ScopeObject.name) { ConvertTo-AzOpsState -Resource $subscriptionItem -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath } } } function ConvertFrom-TypeManagementGroup { [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $true)] [AzOpsScope] $ScopeObject, [switch] $SkipPolicy, [switch] $SkipRole, [switch] $SkipResourceGroup, [switch] $SkipResource, [switch] $ExportRawTemplate, [string] $StatePath ) begin { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Exclude ScopeObject } process { $common = @{ FunctionName = 'Get-AzOpsResourceDefinition' Target = $ScopeObject } Write-PSFMessage -String 'Get-AzOpsResourceDefinition.ManagementGroup.Processing' -StringValues $ScopeObject.ManagementGroupDisplayName, $ScopeObject.ManagementGroup $childOfManagementGroups = ($script:AzOpsAzManagementGroup | Where-Object Name -eq $ScopeObject.ManagementGroup).Children foreach ($child in $childOfManagementGroups) { if ($child.Type -eq '/subscriptions') { if ($script:AzOpsSubscriptions.id -contains $child.Id) { Get-AzOpsResourceDefinition -Scope $child.Id @parameters } else { Write-PSFMessage -String 'Get-AzOpsResourceDefinition.ManagementGroup.Subscription.NotFound' -StringValues $child.Name } } else { Get-AzOpsResourceDefinition -Scope $child.Id @parameters } } ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup | Where-Object Name -eq $ScopeObject.ManagementGroup) -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath } } #endregion Utility Functions } process { Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing' -StringValues $Scope try { $scopeObject = New-AzOpsScope -Scope $Scope -StatePath $StatePath -ErrorAction Stop } catch { Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.NotFound' -StringValues $Scope return } if ($scopeObject.Subscription) { Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Subscription.Found' -StringValues $scopeObject.subscriptionDisplayName, $scopeObject.subscription $context = Get-AzContext -ListAvailable | Where-Object { $_.Subscription.id -eq $scopeObject.Subscription } $odataFilter = "`$filter=subscriptionId eq '$($scopeObject.subscription)'" Write-PSFMessage -Level Debug -String 'Get-AzOpsResourceDefinition.Subscription.OdataFilter' -StringValues $odataFilter } switch ($scopeObject.Type) { resource { ConvertFrom-TypeResource -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate } resourcegroups { ConvertFrom-TypeResourceGroup -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate -Context $context -SkipResource:$SkipResource -OdataFilter $odataFilter } subscriptions { ConvertFrom-TypeSubscription -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate -Context $context -SkipResourceGroup:$SkipResourceGroup -SkipResource:$SkipResource -ODataFilter $odataFilter } managementGroups { ConvertFrom-TypeManagementGroup -ScopeObject $scopeObject -StatePath $StatePath -ExportRawTemplate:$ExportRawTemplate -SkipPolicy:$SkipPolicy -SkipRole:$SkipRole -SkipResourceGroup:$SkipResourceGroup -SkipResource:$SkipResource } } if ($scopeObject.Type -notin 'resourcegroups', 'subscriptions', 'managementGroups') { Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope return } $serializedPolicyDefinitionsInAzure = @() $serializedPolicySetDefinitionsInAzure = @() $serializedPolicyAssignmentsInAzure = @() $serializedRoleDefinitionsInAzure = @() $serializedRoleAssignmentInAzure = @() #region Process Policies if (-not $SkipPolicy) { # Process policy definitions Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Policy Definitions', $scopeObject.Scope $policyDefinitions = Get-AzOpsPolicyDefinition -ScopeObject $scopeObject $policyDefinitions | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath $serializedPolicyDefinitionsInAzure = $policyDefinitions | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject # Process policyset definitions (initiatives)) Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'PolicySet Definitions', $scopeObject.Scope $policySetDefinitions = Get-AzOpsPolicySetDefinition -ScopeObject $scopeObject $policySetDefinitions | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath $serializedPolicySetDefinitionsInAzure = $policySetDefinitions | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject # Process policy assignments Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Policy Assignments', $scopeObject.Scope $policyAssignments = Get-AzOpsPolicyAssignment -ScopeObject $scopeObject $policyAssignments | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath $serializedPolicyAssignmentsInAzure = $policyAssignments | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject } #endregion Process Policies #region Process Roles if (-not $SkipRole) { # Process role definitions Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Role Definitions', $scopeObject.Scope $roleDefinitions = Get-AzOpsRoleDefinition -ScopeObject $scopeObject $roleDefinitions | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath $serializedRoleDefinitionsInAzure = $roleDefinitions | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject # Process role assignments Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Processing.Detail' -StringValues 'Role Assignments', $scopeObject.Scope $roleAssignments = Get-AzOpsRoleAssignment -ScopeObject $scopeObject $roleAssignments | ConvertTo-AzOpsState -ExportRawTemplate:$ExportRawTemplate -StatePath $StatePath $serializedRoleAssignmentInAzure = $roleAssignments | ConvertTo-AzOpsState -ExportRawTemplate -StatePath $StatePath -ReturnObject } #endregion Process Roles if ($scopeObject.Type -notin 'subscriptions', 'managementGroups') { Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope return } if (Get-PSFConfigValue -FullName 'AzOps.Core.AllInOneTemplate') { #region Add accumulated policy and role data # Get statefile from scope $parametersJson = Get-Content -Path $scopeObject.StatePath | ConvertFrom-Json -Depth 100 # Create property bag and add resources at scope $propertyBag = [ordered]@{ 'policyDefinitions' = @($serializedPolicyDefinitionsInAzure) 'policySetDefinitions' = @($serializedPolicySetDefinitionsInAzure) 'policyAssignments' = @($serializedPolicyAssignmentsInAzure) 'roleDefinitions' = @($serializedRoleDefinitionsInAzure) 'roleAssignments' = @($serializedRoleAssignmentInAzure) } # Add property bag to parameters json $parametersJson.parameters.input.value | Add-Member -Name 'properties' -MemberType NoteProperty -Value $propertyBag -force # Export state file with properties at scope ConvertTo-AzOpsState -Resource $parametersJson -ExportPath $scopeObject.StatePath -ExportRawTemplate -StatePath $StatePath #endregion Add accumulated policy and role data } Write-PSFMessage -String 'Get-AzOpsResourceDefinition.Finished' -StringValues $scopeObject.Scope } } function Get-AzOpsRoleAssignment { <# .SYNOPSIS Discovers all custom Role Assignment at the provided scope (Management Groups, subscriptions or resource groups) .DESCRIPTION Discovers all custom Role Assignment at the provided scope (Management Groups, subscriptions or resource groups) .PARAMETER ScopeObject The scope object representing the azure entity to retrieve role assignments for. .EXAMPLE > Get-AzOpsRoleAssignment -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath) Discover all custom role assignments deployed at Management Group scope #> [OutputType([AzOpsRoleAssignment])] [CmdletBinding()] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] [AzOpsScope] $ScopeObject ) process { Write-PSFMessage -String 'Get-AzOpsRoleAssignment.Processing' -StringValues $ScopeObject -Target $ScopeObject foreach ($roleAssignment in Get-AzRoleAssignment -Scope $ScopeObject.Scope | Where-Object Scope -eq $ScopeObject.Scope) { Write-PSFMessage -String 'Get-AzOpsRoleAssignment.Assignment' -StringValues $roleAssignment.DisplayName, $roleAssignment.RoleDefinitionName -Target $ScopeObject [AzOpsRoleAssignment]::new($roleAssignment) } } } function Get-AzOpsRoleDefinition { <# .SYNOPSIS Discover all custom Role Definition at the provided scope (Management Groups, subscriptions or resource groups) .DESCRIPTION Discover all custom Role Definition at the provided scope (Management Groups, subscriptions or resource groups) .PARAMETER ScopeObject The scope object representing the azure entity to retrieve role definitions for. .EXAMPLE > Get-AzOpsRoleDefinition -ScopeObject (New-AzOpsScope -Scope /providers/Microsoft.Management/managementGroups/contoso -StatePath $StatePath) Discover all custom role definitions deployed at Management Group scope #> [OutputType([AzOpsRoleDefinition])] [CmdletBinding()] param ( [parameter(Mandatory = $true, ValueFromPipeline = $true)] [AzOpsScope] $ScopeObject ) process { Write-PSFMessage -String 'Get-AzOpsRoleDefinition.Processing' -StringValues $ScopeObject -Target $ScopeObject foreach ($roleDefinition in Get-AzRoleDefinition -Custom -Scope $ScopeObject.Scope -WarningAction SilentlyContinue) { #removing trailing '/' if it exists in assignable scopes if (($roledefinition.AssignableScopes[0] -replace "[/]$" -replace '') -eq $ScopeObject.Scope) { [AzOpsRoleDefinition]::new($roleDefinition) } else { Write-PSFMessage -String 'Get-AzOpsRoleDefinition.NonAuthorative' -StringValues $roledefinition,Id, $ScopeObject.Scope, $roledefinition.AssignableScopes[0] -Target $ScopeObject } } } } function Get-AzOpsSubscription { <# .SYNOPSIS Returns a list of applicable subscriptions. .DESCRIPTION Returns a list of applicable subscriptions. "Applicable" generally refers to active, non-trial subscriptions. .PARAMETER ExcludedOffers Specific offers to exclude (e.g. specific trial offerings) .PARAMETER ExcludedStates Specific subscription states to ignore (e.g. expired subscriptions) .PARAMETER TenantId ID of the tenant to search in. Must be a connected tenant. .PARAMETER ApiVersion What version of the AZ Api to communicate with. .EXAMPLE > Get-AzOpsSubscription -TenantId $TenantId Returns active, non-trial subscriptions of the specified tenant. #> [CmdletBinding()] param ( [string[]] $ExcludedOffers = (Get-PSFConfigValue -FullName 'AzOps.Core.ExcludedSubOffer'), [string[]] $ExcludedStates = (Get-PSFConfigValue -FullName 'AzOps.Core.ExcludedSubState'), [Parameter(Mandatory = $true)] [ValidateScript({ $_ -in (Get-AzContext).Tenant.Id })] [guid] $TenantId, [string] $ApiVersion = '2020-01-01' ) process { Write-PSFMessage -String 'Get-AzOpsSubscription.Excluded.States' -StringValues ($ExcludedStates -join ',') Write-PSFMessage -String 'Get-AzOpsSubscription.Excluded.Offers' -StringValues ($ExcludedOffers -join ',') $nextLink = "/subscriptions?api-version=$ApiVersion" $allSubscriptionsResults = do { $allSubscriptionsJson = ((Invoke-AzRestMethod -Path $nextLink -Method GET).Content | ConvertFrom-Json -Depth 100) $allSubscriptionsJson.value | Where-Object tenantId -eq $TenantId $nextLink = $allSubscriptionsJson.nextLink -replace 'https://management\.azure\.com' } while ($nextLink) $includedSubscriptions = $allSubscriptionsResults | Where-Object { $_.state -notin $ExcludedStates -and $_.subscriptionPolicies.quotaId -notin $ExcludedOffers } if (-not $includedSubscriptions) { Write-PSFMessage -Level Warning -String 'Get-AzOpsSubscription.NoSubscriptions' -Tag failed return } Write-PSFMessage -String 'Get-AzOpsSubscription.Subscriptions.Found' -StringValues $allSubscriptionsResults.Count if ($allSubscriptionsResults.Count -gt $includedSubscriptions.Count) { Write-PSFMessage -String 'Get-AzOpsSubscription.Subscriptions.Excluded' -StringValues ($allSubscriptionsResults.Count - $includedSubscriptions.Count) } if ($includedSubscriptions | Where-Object State -EQ PastDue) { Write-PSFMessage -String 'Get-AzOpsSubscription.Subscriptions.PastDue' -StringValues ($includedSubscriptions | Where-Object State -EQ PastDue).Count } Write-PSFMessage -String 'Get-AzOpsSubscription.Subscriptions.Included' -StringValues $includedSubscriptions.Count $includedSubscriptions } } function Invoke-AzOpsChange { <# .SYNOPSIS Applies a change to Azure from the AzOps configuration. .DESCRIPTION Applies a change to Azure from the AzOps configuration. .PARAMETER ChangeSet Set of changes from the last execution that need to be applied. .PARAMETER StatePath The root path to where the entire state is being built in. .PARAMETER AzOpsMainTemplate Path to the main template used by AzOps .EXAMPLE > Invoke-AzOpsChange -ChangeSet changeSet -StatePath $StatePath -AzOpsMainTemplate $templatePath Applies a change to Azure from the AzOps configuration. #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string[]] $ChangeSet, [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State'), [string] $AzOpsMainTemplate = (Get-PSFConfigValue -FullName 'AzOps.Core.MainTemplate') ) begin { #region Utility Functions function Resolve-ArmFileAssociation { [CmdletBinding()] param ( [AzOpsScope] $ScopeObject, [string] $FilePath, [string] $AzOpsMainTemplate ) #region Initialization Prep $common = @{ Level = 'Host' Tag = 'pwsh' FunctionName = 'Invoke-AzOpsChange' Target = $ScopeObject } $result = [PSCustomObject] @{ TemplateFilePath = $null TemplateParameterFilePath = $null DeploymentName = $null ScopeObject = $ScopeObject Scope = $ScopeObject.Scope } $fileItem = Get-Item -Path $FilePath if ($fileItem.Extension -notin '.json' , '.bicep') { Write-PSFMessage -Level Warning -String 'Invoke-AzOpsChange.Resolve.NoJson' -StringValues $fileItem.FullName -Tag pwsh -FunctionName 'Invoke-AzOpsChange' -Target $ScopeObject return } #endregion Initialization Prep #region Case: Parameters File if ($fileItem.Name.EndsWith('.parameters.json')) { $result.TemplateParameterFilePath = $fileItem.FullName $deploymentName = $fileItem.Name -replace (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix'), '' if ($deploymentName.Length -gt 58) { $deploymentName = $deploymentName.SubString(0, 58) } $result.DeploymentName = "AzOps-$deploymentName" #region Directly Associated Template file exists $templatePath = $fileItem.FullName -replace '.parameters.json', (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix') if (Test-Path $templatePath) { Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.FoundTemplate' -StringValues $FilePath, $templatePath $result.TemplateFilePath = $templatePath return $result } #endregion Directly Associated Template file exists #region Directly Associated bicep template exists $bicepTemplatePath = $fileItem.FullName -replace '.parameters.json', '.bicep' if (Test-Path $bicepTemplatePath) { Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.FoundBicepTemplate' -StringValues $FilePath, $bicepTemplatePath $result.TemplateFilePath = $bicepTemplatePath return $result } #endregion Directly Associated bicep template exists #region Check in the main template file for a match Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.NotFoundTemplate' -StringValues $FilePath, $templatePath $mainTemplateItem = Get-Item $AzOpsMainTemplate Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.FromMainTemplate' -StringValues $mainTemplateItem.FullName # Determine Resource Type in Parameter file $templateParameterFileHashtable = Get-Content -Path $fileItem.FullName | ConvertFrom-Json -AsHashtable $effectiveResourceType = $null if ($templateParameterFileHashtable.Keys -contains "`$schema") { if ($templateParameterFileHashtable.parameters.input.value.Keys -contains "Type") { # ManagementGroup and Subscription $effectiveResourceType = $templateParameterFileHashtable.parameters.input.value.Type } elseif ($templateParameterFileHashtable.parameters.input.value.Keys -contains "ResourceType") { # Resource $effectiveResourceType = $templateParameterFileHashtable.parameters.input.value.ResourceType } } # Check if generic template is supporting the resource type for the deployment. if ($effectiveResourceType -and (Get-Content $mainTemplateItem.FullName | ConvertFrom-Json -AsHashtable).variables.apiVersionLookup.Keys -contains $effectiveResourceType) { Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.MainTemplate.Supported' -StringValues $effectiveResourceType, $AzOpsMainTemplate.FullName $result.TemplateFilePath = $mainTemplateItem.FullName return $result } Write-PSFMessage -Level Warning -String 'Invoke-AzOpsChange.Resolve.MainTemplate.NotSupported' -StringValues $effectiveResourceType, $AzOpsMainTemplate.FullName -Tag pwsh -FunctionName 'Invoke-AzOpsChange' -Target $ScopeObject return #endregion Check in the main template file for a match # All Code paths end the command } #endregion Case: Parameters File #region Case: Template File $result.TemplateFilePath = $fileItem.FullName $parameterPath = Join-Path $fileItem.Directory.FullName -ChildPath ($fileItem.BaseName + '.parameters' + (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix')) if (Test-Path -Path $parameterPath) { Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.ParameterFound' -StringValues $FilePath, $parameterPath $result.TemplateParameterFilePath = $parameterPath } else { Write-PSFMessage @common -String 'Invoke-AzOpsChange.Resolve.ParameterNotFound' -StringValues $FilePath, $parameterPath } $deploymentName = $fileItem.BaseName -replace '\.json$' -replace ' ', '_' if ($deploymentName.Length -gt 58) { $deploymentName = $deploymentName.SubString(0, 58) } $result.DeploymentName = "AzOps-$deploymentName" $result #endregion Case: Template File } #endregion Utility Functions $common = @{ Level = 'Host' Tag = 'git' } } process { if (-not $ChangeSet) { return } #region Categorize Input Write-PSFMessage @common -String 'Invoke-AzOpsChange.Deployment.Required' $deleteSet = @() $addModifySet = foreach ($change in $ChangeSet) { $operation, $filename = ($change -split "`t")[0, -1] if ($operation -eq 'D') { $deleteSet += $filename continue } if ($operation -in 'A', 'M', 'R') { $filename } } if ($deleteSet) { $deleteSet = $deleteSet | Sort-Object } if ($addModifySet) { $addModifySet = $addModifySet | Sort-Object } #TODO: Clarify what happens with the deletes - not used after reporting them Write-PSFMessage @common -String 'Invoke-AzOpsChange.Change.AddModify' foreach ($item in $addModifySet) { Write-PSFMessage @common -String 'Invoke-AzOpsChange.Change.AddModify.File' -StringValues $item } Write-PSFMessage @common -String 'Invoke-AzOpsChange.Change.Delete' foreach ($item in $deleteSet) { Write-PSFMessage @common -String 'Invoke-AzOpsChange.Change.Delete.File' -StringValues $item } #endregion Categorize Input #region Deploy State $common.Tag = 'pwsh' # Nested Pipeline allows economizing on New-AzOpsStateDeployment having to run its "begin" block once only $newStateDeploymentCmd = { New-AzOpsStateDeployment -StatePath $StatePath }.GetSteppablePipeline() $newStateDeploymentCmd.Begin($true) foreach ($addition in $addModifySet) { if ($addition -notmatch '/*.subscription.json$') { continue } Write-PSFMessage @common -String 'Invoke-AzOpsChange.Deploy.Subscription' -StringValues $addition -Target $addition $newStateDeploymentCmd.Process($addition) } foreach ($addition in $addModifySet) { if ($addition -notmatch '/*.providerfeatures.json$') { continue } Write-PSFMessage @common -String 'Invoke-AzOpsChange.Deploy.ProviderFeature' -StringValues $addition -Target $addition $newStateDeploymentCmd.Process($addition) } foreach ($addition in $addModifySet) { if ($addition -notmatch '/*.resourceproviders.json$') { continue } Write-PSFMessage @common -String 'Invoke-AzOpsChange.Deploy.ResourceProvider' -StringValues $addition -Target $addition $newStateDeploymentCmd.Process($addition) } $newStateDeploymentCmd.End() #endregion Deploy State $deploymentList = foreach ($addition in $addModifySet | Where-Object { $_ -match ((Get-Item $StatePath).Name) }) { try { $scopeObject = New-AzOpsScope -Path $addition -StatePath $StatePath -ErrorAction Stop } catch { Write-PSFMessage @common -String 'Invoke-AzOpsChange.Scope.Failed' -StringValues $addition, $StatePath -Target $addition -ErrorRecord $_ continue } if (-not $scopeObject) { Write-PSFMessage @common -String 'Invoke-AzOpsChange.Scope.NotFound' -StringValues $addition, $StatePath -Target $addition continue } Resolve-ArmFileAssociation -ScopeObject $scopeObject -FilePath $addition -AzOpsMainTemplate $AzOpsMainTemplate } #Starting Tenant Deployment $uniqueProperties = 'Scope', 'DeploymentName', 'TemplateFilePath', 'TemplateParameterFilePath' $deploymentList | Select-Object $uniqueProperties -Unique | Sort-Object -Property TemplateParameterFilePath | New-AzOpsDeployment -WhatIf:$WhatIfPreference } } function Invoke-AzOpsNativeCommand { <# .SYNOPSIS Executes a native command. .DESCRIPTION Executes a native command. .PARAMETER ScriptBlock The scriptblock containing the native command to execute. Note: Specifying a scriptblock WITHOUT any native command may cause erroneous LASTEXITCODE detection. .PARAMETER IgnoreExitcode Whether to ignore exitcodes. .PARAMETER Quiet Quiet mode disables printing error output of a native command. .EXAMPLE > Invoke-AzOpsNativeCommand -Scriptblock { git config --system -l } Executes "git config --system -l" #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock, [switch] $IgnoreExitcode, [switch] $Quiet ) try { if ($Quiet) { $output = & $ScriptBlock 2>&1 } else { $output = & $ScriptBlock } if (-not $Quiet -and $output) { $output | Out-String | ForEach-Object { Write-PSFMessage -Level Debug -Message $_ } $output } } catch { if (-not $IgnoreExitcode) { $caller = Get-PSCallStack -ErrorAction SilentlyContinue if ($caller) { Stop-PSFFunction -String 'Invoke-AzOpsNativeCommand.Failed.WithCallstack' -StringValues $ScriptBlock, $caller[1].ScriptName, $caller[1].ScriptLineNumber, $LASTEXITCODE -Cmdlet $PSCmdlet -EnableException $true } Stop-PSFFunction -String 'Invoke-AzOpsNativeCommand.Failed.NoCallstack' -StringValues $ScriptBlock, $LASTEXITCODE -Cmdlet $PSCmdlet -EnableException $true } $output } } function Invoke-AzOpsScriptBlock { <# .SYNOPSIS Execute a scriptblock, retry if it fails. .DESCRIPTION Execute a scriptblock, retry if it fails. .PARAMETER ScriptBlock The scriptblock to execute. .PARAMETER ArgumentList Any arguments to pass to the scriptblock. .PARAMETER RetryCount How often to try again before giving up. Default: 0 .PARAMETER RetryWait How long to wait between retries in seconds. Default: 3 .PARAMETER RetryType How to wait for a retry? Either always the exact time specified in RetryWait as seconds, or exponentially increase the time between waits. Assuming a wait time of 2 seconds and three retries, this will result in the following waits between attempts: Linear (default): 2, 2, 2 Exponential: 2, 4, 8 .EXAMPLE > Invoke-AzOpsScriptBlock -ScriptBlock { 1 / 0 } Will attempt once to divide by zero. Hint: This is unlikely to succeede. Ever. .EXAMPLE > Invoke-AzOpsScriptBlock -ScriptBlock { 1 / 0 } -RetryCount 3 Will attempt to divide by zero, retrying up to 3 additional times (for a total of 4 attempts). Hint: Trying to divide by zero more than once does not increase your chance of success. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ScriptBlock] $ScriptBlock, [object[]] $ArgumentList, [int] $RetryCount = 0, [int] $RetryWait = 3, [ValidateSet('Linear','Exponential')] [string] $RetryType = 'Linear' ) begin { $count = 0 } process { $data = @{ ScriptBlock = $ScriptBlock ArgumentList = $ArgumentList } while ($count -le $RetryCount) { $count++ try { if (Test-PSFParameterBinding -ParameterName ArgumentList) { & $ScriptBlock $ArgumentList } else { & $ScriptBlock } break } catch { if ($count -lt $RetryCount) { Write-PSFMessage -Level Debug -String 'Invoke-AzOpsScriptBlock.Failed.WillRetry' -StringValues $count, $RetryCount -ErrorRecord $_ -Data $data switch ($RetryType) { Linear { Start-Sleep -Seconds $RetryWait } Exponential { Start-Sleep -Seconds ([math]::Pow($RetryWait, $count)) } } continue } Write-PSFMessage -Level Warning -String 'Invoke-AzOpsScriptBlock.Failed.GivingUp' -StringValues $count, $RetryCount -ErrorRecord $_ -Data $data throw } } } } function New-AzOpsDeployment { <# .SYNOPSIS Deploys a full state into azure. .DESCRIPTION Deploys a full state into azure. .PARAMETER DeploymentName Name under which to deploy the state. .PARAMETER TemplateFilePath Path where the ARM templates can be found. .PARAMETER TemplateParameterFilePath Path where the parameters of the ARM templates can be found. .PARAMETER Mode Mode in which to process the templates. Defaults to incremental. TODO: Clarify use .PARAMETER StatePath The root folder under which to find the resource json. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE > $AzOpsDeploymentList | Select-Object $uniqueProperties -Unique | Sort-Object -Property TemplateParameterFilePath | New-Deployment Deploy all unique deployments provided from $AzOpsDeploymentList #> [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(ValueFromPipelineByPropertyName = $true)] [string] $DeploymentName = "azops-template-deployment", [Parameter(ValueFromPipelineByPropertyName = $true)] [string] $TemplateFilePath = (Get-PSFConfigValue -FullName 'AzOps.Core.MainTemplate'), [Parameter(ValueFromPipelineByPropertyName = $true)] [AllowEmptyString()] [AllowNull()] [string] $TemplateParameterFilePath, [string] $Mode = "Incremental", [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State') ) process { Write-PSFMessage -String 'New-AzOpsDeployment.Processing' -StringValues $DeploymentName, $TemplateFilePath, $TemplateParameterFilePath, $Mode -Target $TemplateFilePath #region Resolve Scope try { if ($TemplateParameterFilePath) { $scopeObject = New-AzOpsScope -Path $TemplateParameterFilePath -StatePath $StatePath -ErrorAction Stop } else { $scopeObject = New-AzOpsScope -Path $TemplateFilePath -StatePath $StatePath -ErrorAction Stop } } catch { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.Scope.Failed' -Target $TemplateFilePath -StringValues $TemplateFilePath, $TemplateParameterFilePath -ErrorRecord $_ return } if (-not $scopeObject) { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.Scope.Empty' -Target $TemplateFilePath -StringValues $TemplateFilePath, $TemplateParameterFilePath return } #endregion Resolve Scope #region Process Scope #region Resource Group if ($scopeObject.resourcegroup) { Write-PSFMessage -String 'New-AzOpsDeployment.ResourceGroup.Processing' -StringValues $scopeObject -Target $scopeObject Set-AzOpsContext -ScopeObject $scopeObject if ($scopeObject.ResourceProvider -eq 'Microsoft.Resources' -and $scopeObject.Resource -eq 'resourceGroups') { # Since this is a deployment for resource group, it must be invoked at subscription scope $defaultDeploymentRegion = Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion' Write-PSFMessage -String 'New-AzOpsDeployment.Subscription.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject $parameters = @{ 'TemplateFile' = $TemplateFilePath 'Location' = $defaultDeploymentRegion 'SkipTemplateParameterPrompt' = $true } if ($TemplateParameterFilePath) { $parameters.TemplateParameterFile = $TemplateParameterFilePath } if ((Get-AzContext).Subscription.Id -ne $scopeObject.subscription) { Set-AzOpsContext -ScopeObject $scopeObject } # Validate Template $results = Get-AzSubscriptionDeploymentWhatIfResult @parameters if ($results.Error) { Write-PSFMessage -Level Error -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject return } Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | out-string) -Target $scopeObject $parameters.Name = $DeploymentName if ($PSCmdlet.ShouldProcess("Start Subscription Deployment?")) { New-AzSubscriptionDeployment @parameters } else { # Exit deployment Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf' } } else { $parameters = @{ 'TemplateFile' = $TemplateFilePath 'SkipTemplateParameterPrompt' = $true 'ResourceGroupName' = $scopeObject.resourcegroup } if ($TemplateParameterFilePath) { $parameters.TemplateParameterFile = $TemplateParameterFilePath } $results = Get-AzResourceGroupDeploymentWhatIfResult @parameters -ErrorAction Continue -ErrorVariable resultsError if ($resultsError) { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -StringValues $resultsError.Exception.Message -Target $scopeObject } elseif ($results.Error) { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject return } else { Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject } $parameters.Name = $DeploymentName if ($PSCmdlet.ShouldProcess("Start ResourceGroup Deployment?")) { New-AzResourceGroupDeployment @parameters } else { # Exit deployment Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf' } } } #endregion Resource Group #region Subscription elseif ($scopeObject.subscription) { $defaultDeploymentRegion = Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion' Write-PSFMessage -String 'New-AzOpsDeployment.Subscription.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject if ((Get-AzContext).Subscription.Id -ne $scopeObject.subscription) { Set-AzOpsContext -ScopeObject $scopeObject } $parameters = @{ 'TemplateFile' = $TemplateFilePath 'Location' = $defaultDeploymentRegion 'SkipTemplateParameterPrompt' = $true } if ($TemplateParameterFilePath) { $parameters.TemplateParameterFile = $TemplateParameterFilePath } $results = Get-AzSubscriptionDeploymentWhatIfResult @parameters -ErrorAction Continue -ErrorVariable resultsError if ($resultsError) { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -StringValues $resultsError.Exception.Message -Target $scopeObject } elseif ($results.Error) { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject return } else { Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject } $parameters.Name = $DeploymentName if ($PSCmdlet.ShouldProcess("Start Subscription Deployment?")) { New-AzSubscriptionDeployment @parameters } else { # Exit deployment Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf' } } #endregion Subscription #region Management Group elseif ($scopeObject.managementGroup) { $defaultDeploymentRegion = Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion' Write-PSFMessage -String 'New-AzOpsDeployment.ManagementGroup.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject $parameters = @{ 'TemplateFile' = $TemplateFilePath 'Location' = $defaultDeploymentRegion 'ManagementGroupId' = $scopeObject.managementgroup 'SkipTemplateParameterPrompt' = $true } if ($TemplateParameterFilePath) { $parameters.TemplateParameterFile = $TemplateParameterFilePath } $results = Get-AzManagementGroupDeploymentWhatIfResult @parameters -ErrorAction Continue -ErrorVariable resultsError if ($resultsError) { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -StringValues $resultsError.Exception.Message -Target $scopeObject } elseif ($results.Error) { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject return } else { Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject } $parameters.Name = $DeploymentName if ($PSCmdlet.ShouldProcess("Start ManagementGroup Deployment?")) { New-AzManagementGroupDeployment @parameters } else { # Exit deployment Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf' } } #endregion Management Group #region Root elseif ($scopeObject.type -eq 'root' -and $scopeObject.scope -eq '/') { $defaultDeploymentRegion = Get-PSFConfigValue -FullName 'AzOps.Core.DefaultDeploymentRegion' Write-PSFMessage -String 'New-AzOpsDeployment.Root.Processing' -StringValues $defaultDeploymentRegion, $scopeObject -Target $scopeObject $parameters = @{ 'TemplateFile' = $TemplateFilePath 'location' = $defaultDeploymentRegion 'SkipTemplateParameterPrompt' = $true } if ($TemplateParameterFilePath) { $parameters.TemplateParameterFile = $TemplateParameterFilePath } $results = Get-AzTenantDeploymentWhatIfResult @parameters -ErrorAction Continue -ErrorVariable resultsError if ($resultsError) { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.WhatIfWarning' -StringValues $resultsError.Exception.Message -Target $scopeObject } elseif ($results.Error) { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.TemplateError' -StringValues $TemplateFilePath -Target $scopeObject return } else { Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.WhatIfResults' -StringValues ($results | Out-String) -Target $scopeObject } $parameters.Name = $DeploymentName if ($PSCmdlet.ShouldProcess("Start Tenant Deployment?")) { # Whatif Placeholder New-AzTenantDeployment @parameters } else { # Exit deployment Write-PSFMessage -Level Verbose -String 'New-AzOpsDeployment.SkipDueToWhatIf' } } #endregion Root #region Unidentified else { Write-PSFMessage -Level Warning -String 'New-AzOpsDeployment.Scope.Unidentified' -Target $scopeObject -StringValues $scopeObject } #endregion Unidentified #endregion Process Scope } } function New-AzOpsScope { <# .SYNOPSIS Returns an AzOpsScope for a path or for a scope .DESCRIPTION Returns an AzOpsScope for a path or for a scope .PARAMETER Scope The scope for which to return a scope object. .PARAMETER Path The path from which to build a scope. .PARAMETER StatePath The root path to where the entire state is being built in. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE > New-AzOpsScope -Scope "/providers/Microsoft.Management/managementGroups/3fc1081d-6105-4e19-b60c-1ec1252cf560" Return AzOpsScope for a root Management Group scope scope in Azure: scope : /providers/Microsoft.Management/managementGroups/3fc1081d-6105-4e19-b60c-1ec1252cf560 type : managementGroups name : 3fc1081d-6105-4e19-b60c-1ec1252cf560 statepath : C:\git\cet-northstar\azops\3fc1081d-6105-4e19-b60c-1ec1252cf560\.AzState\Microsoft.Management_managementGroups-3fc1081d-6105-4e19-b60c-1ec1252cf560.parameters.json managementgroup : 3fc1081d-6105-4e19-b60c-1ec1252cf560 managementgroupDisplayName : 3fc1081d-6105-4e19-b60c-1ec1252cf560 subscription : subscriptionDisplayName : resourcegroup : resourceprovider : resource : .EXAMPLE > New-AzOpsScope -path "C:\Users\jodahlbo\git\CET-NorthStar\azops\Tenant Root Group\Non-Production Subscriptions\Dalle MSDN MVP\365lab-dcs" Return AzOpsScope for a filepath .INPUTS Scope .INPUTS Path .OUTPUTS [AzOpsScope] #> #[OutputType([AzOpsScope])] [CmdletBinding(SupportsShouldProcess = $true)] param ( [Parameter(ParameterSetName = "scope")] [string] [ValidateScript( { $null -ne $script:AzOpsAzManagementGroup -or $script:AzOpsSubscription })] $Scope, [Parameter(ParameterSetName = "pathfile", ValueFromPipeline = $true)] [string] $Path, [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State') ) process { Write-PSFMessage -Level Debug -String 'New-AzOpsScope.Starting' switch ($PSCmdlet.ParameterSetName) { scope { Invoke-PSFProtectedCommand -ActionString 'New-AzOpsScope.Creating.FromScope' -ActionStringValues $Scope -Target $Scope -ScriptBlock { [AzOpsScope]::new($Scope, $StatePath) } -EnableException $true -PSCmdlet $PSCmdlet } pathfile { if (-not (Test-Path $Path)) { Stop-PSFFunction -String 'New-AzOpsScope.Path.NotFound' -StringValues $Path -EnableException $true -Cmdlet $PSCmdlet } $Path = Resolve-PSFPath -Path $Path -SingleItem -Provider FileSystem $StatePathValidator = Resolve-PSFPath -Path $StatePath -SingleItem -Provider FileSystem if (-not $Path.StartsWith($StatePathValidator)) { Stop-PSFFunction -String 'New-AzOpsScope.Path.InvalidRoot' -StringValues $Path, $StatePath -EnableException $true -Cmdlet $PSCmdlet } Invoke-PSFProtectedCommand -ActionString 'New-AzOpsScope.Creating.FromFile' -Target $Path -ScriptBlock { [AzOpsScope]::new($(Get-Item -Path $Path), $StatePath) } -EnableException $true -PSCmdlet $PSCmdlet } } } } function New-AzOpsStateDeployment { <# .SYNOPSIS Deploys a set of ARM templates into Azure. .DESCRIPTION Deploys a set of ARM templates into Azure. Define the state using Initialize-AzOpsRepository and maintain it via: - Invoke-AzOpsGitPull - Invoke-AzOpsGitPush .PARAMETER FileName Root path from which to deploy. .PARAMETER StatePath The overall path of the state to deploy. .EXAMPLE > New-StateDeployment -FileName $fileName -StatePath $StatePath Deploys the specified set of ARM templates into Azure. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [ValidateScript({ Test-Path $_ })] $FileName, [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State') ) begin { $subscriptions = Get-AzSubscription $enrollmentAccounts = Get-AzEnrollmentAccount } process { Write-PSFMessage -String 'New-AzOpsStateDeployment.Processing' -StringValues $FileName $scopeObject = New-AzOpsScope -Path (Get-Item -Path $FileName).FullName -StatePath $StatePath if (-not $scopeObject.Type) { Write-PSFMessage -Level Warning -String 'New-AzOpsStateDeployment.InvalidScope' -StringValues $FileName -Target $scopeObject return } #TODO: Clarify whether this exclusion was intentional if ($scopeObject.Type -ne 'subscriptions') { return } #region Process Subscriptions if ($FileName -match '/*.subscription.json$') { Write-PSFMessage -String 'New-AzOpsStateDeployment.Subscription' -StringValues $FileName -Target $scopeObject $subscription = $subscriptions | Where-Object Name -EQ $scopeObject.subscriptionDisplayName #region Subscription needs to be created if (-not $subscription) { Write-PSFMessage -String 'New-AzOpsStateDeployment.Subscription.New' -StringValues $FileName -Target $scopeObject if (-not $enrollmentAccounts) { Write-PSFMessage -Level Error -String 'New-AzOpsStateDeployment.NoEnrollmentAccount' -Target $scopeObject Write-PSFMessage -Level Error -String 'New-AzOpsStateDeployment.NoEnrollmentAccount.Solution' -Target $scopeObject return } if ($cfgEnrollmentAccount = Get-PSFConfigValue -FullName 'AzOps.Core.EnrollmentAccountPrincipalName') { Write-PSFMessage -String 'New-AzOpsStateDeployment.EnrollmentAccount.Selected' -StringValues $cfgEnrollmentAccount $enrollmentAccountObjectId = ($enrollmentAccounts | Where-Object PrincipalName -eq $cfgEnrollmentAccount).ObjectId } else { Write-PSFMessage -String 'New-AzOpsStateDeployment.EnrollmentAccount.First' -StringValues @($enrollmentAccounts)[0].PrincipalName $enrollmentAccountObjectId = @($enrollmentAccounts)[0].ObjectId } Invoke-PSFProtectedCommand -ActionString 'New-AzOpsStateDeployment.Subscription.Creating' -ActionStringValues $scopeObject.Name -ScriptBlock { $subscription = New-AzSubscription -Name $scopeObject.Name -OfferType (Get-PSFConfigValue -FullName 'AzOps.Core.OfferType') -EnrollmentAccountObjectId $enrollmentAccountObjectId -ErrorAction Stop $subscriptions = @($subscriptions) + @($subscription) } -Target $scopeObject -EnableException $true -PSCmdlet $PSCmdlet Invoke-PSFProtectedCommand -ActionString 'New-AzOpsStateDeployment.Subscription.AssignManagementGroup' -ActionStringValues $subscription.Name, $scopeObject.ManagementGroupDisplayName -ScriptBlock { New-AzManagementGroupSubscription -GroupName $scopeObject.ManagementGroup -SubscriptionId $subscription.SubscriptionId -ErrorAction Stop } -Target $scopeObject -EnableException $true -PSCmdlet $PSCmdlet } #endregion Subscription needs to be created #region Subscription exists already else { Write-PSFMessage -String 'New-AzOpsStateDeployment.Subscription.Exists' -StringValues $subscription.Name, $subscription.Id -Target $scopeObject Invoke-PSFProtectedCommand -ActionString 'New-AzOpsStateDeployment.Subscription.AssignManagementGroup' -ActionStringValues $subscription.Name, $scopeObject.ManagementGroupDisplayName -ScriptBlock { New-AzManagementGroupSubscription -GroupName $scopeObject.ManagementGroup -SubscriptionId $subscription.SubscriptionId -ErrorAction Stop } -Target $scopeObject -EnableException $true -PSCmdlet $PSCmdlet } #endregion Subscription exists already } if ($FileName -match '/*.providerfeatures.json$') { Register-AzOpsProviderFeature -FileName $FileName -ScopeObject $scopeObject } if ($FileName -match '/*.resourceproviders.json$') { Register-AzOpsResourceProvider -FileName $FileName -ScopeObject $scopeObject } #endregion Process Subscriptions } } function Register-AzOpsProviderFeature { <# .SYNOPSIS Registers a provider feature from ARM. .DESCRIPTION Registers a provider feature from ARM. .PARAMETER FileName Path to the ARM template file representing a provider feature. .PARAMETER ScopeObject The current AzOps scope. .EXAMPLE PS C:\> Register-ProviderFeature -FileName $file -ScopeObject $scopeObject Registers a provider feature from ARM. #> [CmdletBinding()] param ( [string] $FileName, [AzOpsScope] $ScopeObject ) process { #TODO: Clarify original function design intent # Get Subscription ID from scope (since Subscription ID is not available for Resource Groups and Resources) Write-PSFMessage -String 'Register-AzOpsProviderFeature.Processing' -StringValues $ScopeObject, $FileName -Target $ScopeObject $currentContext = Get-AzContext if ($ScopeObject.Subscription -and $currentContext.Subscription.Id -ne $ScopeObject.Subscription) { Write-PSFMessage -String 'Register-AzOpsProviderFeature.Context.Switching' -StringValues $currentContext.Subscription.Name, $CurrentAzContext.Subscription.Id, $ScopeObject.Subscription, $ScopeObject.Name -Target $ScopeObject try { $null = Set-AzContext -SubscriptionId $ScopeObject.Subscription -ErrorAction Stop } catch { Stop-PSFFunction -String 'Register-AzOpsProviderFeature.Context.Failed' -StringValues $ScopeObject.SubscriptionDisplayName -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet -Target $ScopeObject throw "Couldn't switch context $_" } } $providerFeatures = Get-Content $FileName | ConvertFrom-Json foreach ($providerFeature in $providerFeatures) { if ($ProviderFeature.FeatureName -and $ProviderFeature.ProviderName) { Write-PSFMessage -String 'Register-AzOpsProviderFeature.Provider.Feature' -StringValues $ProviderFeature.FeatureName, $ProviderFeature.ProviderName -Target $ScopeObject Register-AzProviderFeature -Confirm:$false -ProviderNamespace $ProviderFeature.ProviderName -FeatureName $ProviderFeature.FeatureName } } } } function Register-AzOpsResourceProvider { <# .SYNOPSIS Registers an azure resource provider. .DESCRIPTION Registers an azure resource provider. Assumes an ARM definition of a resource provider as input. .PARAMETER FileName The path to the file containing an ARM template defining a resource provider. .PARAMETER ScopeObject The current AzOps scope. .EXAMPLE PS C:\> Register-ResourceProvider -FileName $fileName -ScopeObject $scopeObject Registers an azure resource provider. #> [CmdletBinding()] param ( [string] $FileName, [AzOpsScope] $ScopeObject ) process { Write-PSFMessage -String 'Register-AzOpsResourceProvider.Processing' -StringValues $ScopeObject, $FileName -Target $ScopeObject $currentContext = Get-AzContext if ($ScopeObject.Subscription -and $currentContext.Subscription.Id -ne $ScopeObject.Subscription) { Write-PSFMessage -String 'Register-AzOpsResourceProvider.Context.Switching' -StringValues $currentContext.Subscription.Name, $CurrentAzContext.Subscription.Id, $ScopeObject.Subscription, $ScopeObject.Name -Target $ScopeObject try { $null = Set-AzContext -SubscriptionId $ScopeObject.Subscription -ErrorAction Stop } catch { Stop-PSFFunction -String 'Register-AzOpsResourceProvider.Context.Failed' -StringValues $ScopeObject.SubscriptionDisplayName -ErrorRecord $_ -EnableException $true -Cmdlet $PSCmdlet -Target $ScopeObject throw "Couldn't switch context $_" } } $resourceproviders = Get-Content $FileName | ConvertFrom-Json foreach ($resourceprovider in $resourceproviders | Where-Object RegistrationState -eq 'Registered') { if (-not $resourceprovider.ProviderNamespace) { continue } Write-PSFMessage -String 'Register-AzOpsResourceProvider.Provider.Register' -StringValues $resourceprovider.ProviderNamespace Write-AzOpsLog -Level Verbose -Topic "Register-AzOpsResourceProvider" -Message "Registering Provider $($resourceprovider.ProviderNamespace)" Register-AzResourceProvider -Confirm:$false -Pre -ProviderNamespace $resourceprovider.ProviderNamespace } } } function Save-AzOpsManagementGroupChildren { <# .SYNOPSIS Recursively build/change Management Group hierarchy in file system from provided scope. .DESCRIPTION Recursively build/change Management Group hierarchy in file system from provided scope. .PARAMETER Scope Scope to discover - assumes [AzOpsScope] object .PARAMETER StatePath The root path to where the entire state is being built in. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .EXAMPLE > Save-AzOpsManagementGroupChildren -Scope (New-AzOpsScope -scope /providers/Microsoft.Management/managementGroups/contoso) Discover Management Group hierarchy from scope .INPUTS AzOpsScope .OUTPUTS Management Group hierarchy in file system #> [CmdletBinding(SupportsShouldProcess = $true)] [OutputType()] param ( [Parameter(Mandatory = $true)] $Scope, [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State') ) process { Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Starting' Invoke-PSFProtectedCommand -ActionString 'Save-AzOpsManagementGroupChildren.Creating.Scope' -Target $Scope -ScriptBlock { $scopeObject = New-AzOpsScope -Scope $Scope -StatePath $StatePath -ErrorAction SilentlyContinue -Confirm:$false } -EnableException $true -PSCmdlet $PSCmdlet if (-not $scopeObject) { return } # In case -WhatIf is used Write-PSFMessage -String 'Save-AzOpsManagementGroupChildren.Processing' -StringValues $scopeObject.Scope # Construct all file paths for scope $scopeStatepath = $scopeObject.StatePath $statepathFileName = [IO.Path]::GetFileName($scopeStatepath) $statepathDirectory = [IO.Path]::GetDirectoryName($scopeStatepath) $statepathScopeDirectory = [IO.Directory]::GetParent($statepathDirectory).ToString() $statepathScopeDirectoryParent = [IO.Directory]::GetParent($statepathScopeDirectory).ToString() Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.StatePath' -StringValues $scopeStatepath Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.FileName' -StringValues $statepathFileName Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.Directory' -StringValues $statepathDirectory Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.ScopeDirectory' -StringValues $statepathScopeDirectory Write-PSFMessage -Level Debug -String 'Save-AzOpsManagementGroupChildren.Data.ScopeDirectoryParent' -StringValues $statepathScopeDirectoryParent # If file is found anywhere in "AzOps.Core.State", ensure that it is at the right scope or else it doesn't matter if (Get-ChildItem -Path $Statepath -File -Recurse -Force | Where-Object Name -eq $statepathFileName) { # If the file is found in AzOps State $exisitingScopePath = (Get-ChildItem -Path $Statepath -File -Recurse -Force | Where-Object Name -eq $statepathFileName).Directory # Looking at parent of parent if AutoGeneratedTemplateFolderPath is sub-directory, looking for parent (scope folder) of parent (actual parent in Azure) if ( ((Get-PSFConfigValue -FullName 'AzOps.Core.AutoGeneratedTemplateFolderPath') -notin '.') -and $exisitingScopePath.Parent.Parent.FullName -ne $statepathScopeDirectoryParent) { if ($exisitingScopePath.Parent.FullName -ne $statepathScopeDirectoryParent) { Write-PSFMessage -String 'Save-AzOpsManagementGroupChildren.Moving.Source' -StringValues $exisitingScopePath Move-Item -Path $exisitingScopePath.Parent -Destination $statepathScopeDirectoryParent -Force Write-PSFMessage -String 'Save-AzOpsManagementGroupChildren.Moving.Destination' -StringValues $statepathScopeDirectoryParent } } # Files might be at the right scope but not in right AutoGeneratedTemplateFolderPath e.g. when AutoGeneratedTemplateFolderPath is changed. if (-not (Test-Path $statepathDirectory)) { New-Item -Path $statepathDirectory -ItemType Directory -Force | out-null } # For all the files in AutoGeneratedTemplateFolderPath directory, only moving files that are auto generated Get-ChildItem -Path (Get-ChildItem -Path $Statepath -File -Recurse -Force | Where-Object Name -eq $statepathFileName).Directory -File -Filter 'Microsoft.*' | Move-Item -Destination $statepathDirectory -Force } switch ($scopeObject.Type) { managementGroups { ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup | Where-Object { $_.Name -eq $scopeObject.managementgroup }) -ExportPath $scopeObject.statepath -StatePath $StatePath foreach ($child in $script:AzOpsAzManagementGroup.Where{ $_.Name -eq $scopeObject.managementgroup }.Children) { if ($child.Type -eq '/subscriptions') { if ($script:AzOpsSubscriptions.id -contains $child.Id) { Save-AzOpsManagementGroupChildren -Scope $child.Id -StatePath $StatePath } else { Write-PSFMessage -String 'Save-AzOpsManagementGroupChildren.Subscription.NotFound' -StringValues $child.Name } } else { Save-AzOpsManagementGroupChildren -Scope $child.Id -StatePath $StatePath } } } subscriptions { ConvertTo-AzOpsState -Resource ($script:AzOpsAzManagementGroup.children | Where-Object Name -eq $scopeObject.name) -ExportPath $scopeObject.statepath -StatePath $StatePath } } } } function Set-AzOpsContext { <# .SYNOPSIS Changes the currently active azure context to the subscription of the specified scope object. .DESCRIPTION Changes the currently active azure context to the subscription of the specified scope object. .PARAMETER ScopeObject The scope object into which context to change. .EXAMPLE > Set-AzOpsContext -ScopeObject $scopeObject Changes the current context to the subscription of $scopeObject. #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [AzOpsScope] $ScopeObject ) begin { $context = Get-AzContext } process { if (-not $ScopeObject.Subscription) { return } if ($context.Subscription.Id -ne $ScopeObject.Subscription) { Write-PSFMessage -String 'Set-AzOpsContext.Change' -StringValues $context.Subscription.Name, $ScopeObject.SubscriptionDisplayName, $ScopeObject.Subscription Set-AzContext -SubscriptionId $scopeObject.Subscription } } } function Initialize-AzOpsEnvironment { <# .SYNOPSIS Initializes the execution context of the module. .DESCRIPTION Initializes the execution context of the module. This is used by all other commands. It prepares / caches tenant, subscription and management group data. .PARAMETER IgnoreContextCheck Whether it should validate the azure contexts available or not. .PARAMETER InvalidateCache If data was already cached from a previous execution, execute again anyway? .PARAMETER ExcludedSubOffer Subscription filter. Subscriptions from the listed offerings will be ignored. Generally used to prevent using trial subscriptions, but can be adapted for other limitations. .PARAMETER ExcludedSubState Subscription filter. Subscriptions in the listed states will be ignored. For example, by default, disabled subscriptions will not be processed. .PARAMETER PartialMgDiscovery Enable partial management group discovery. Necessary if current user does not have root access. Must be used in combination with -PartialMgDiscoveryRoot .PARAMETER PartialMgDiscoveryRoot Custom search roots under which to detect management groups. Used for partial management group discovery. Must be used in combination with -PartialMgDiscovery .EXAMPLE > Initialize-AzOpsEnvironment Initializes the default execution context of the module. #> [CmdletBinding()] param ( [switch] $IgnoreContextCheck = (Get-PSFConfigValue -FullName 'AzOps.Core.IgnoreContextCheck'), [switch] $InvalidateCache = (Get-PSFConfigValue -FullName 'AzOps.Core.InvalidateCache'), [string[]] $ExcludedSubOffer = (Get-PSFConfigValue -FullName 'AzOps.Core.ExcludedSubOffer'), [string[]] $ExcludedSubState = (Get-PSFConfigValue -FullName 'AzOps.Core.ExcludedSubState'), [switch] $PartialMgDiscovery = (Get-PSFConfigValue -FullName 'AzOps.Core.PartialMgDiscoveryRoot'), [string[]] $PartialMgDiscoveryRoot = (Get-PSFConfigValue -FullName 'AzOps.Core.PartialMgDiscoveryRoot') ) begin { Assert-AzOpsWindowsLongPath -Cmdlet $PSCmdlet Assert-AzOpsJQDependency -Cmdlet $PSCmdlet $allAzContext = Get-AzContext -ListAvailable if (-not $allAzContext) { Stop-PSFFunction -String 'Initialize-AzOpsEnvironment.AzureContext.No' -EnableException $true -Cmdlet $PSCmdlet } $azContextTenants = @($AllAzContext.Tenant.Id | Sort-Object -Unique) if (-not $IgnoreContextCheck -and $azContextTenants.Count -gt 1) { Stop-PSFFunction -String 'Initialize-AzOpsEnvironment.AzureContext.TooMany' -StringValues $azContextTenants.Count, ($azContextTenants -join ',') -EnableException $true -Cmdlet $PSCmdlet } } process { # If data exists and we don't want to rebuild the data cache, no point in continuing if (-not $InvalidateCache -and $script:AzOpsAzManagementGroup -and $script:AzOpsSubscriptions) { Write-PSFMessage -String 'Initialize-AzOpsEnvironment.UsingCache' return } #region Initialize & Prepare Write-PSFMessage -String 'Initialize-AzOpsEnvironment.Processing' $currentAzContext = Get-AzContext $tenantId = $currentAzContext.Tenant.Id $rootScope = '/providers/Microsoft.Management/managementGroups/{0}' -f $tenantId Write-PSFMessage -String 'Initialize-AzOpsEnvironment.Initializing' if (-not (Test-Path -Path (Get-PSFConfigValue -FullName 'AzOps.Core.State'))) { $null = New-Item -path (Get-PSFConfigValue -FullName 'AzOps.Core.State') -Force -ItemType directory } $script:AzOpsSubscriptions = Get-AzOpsSubscription -ExcludedOffers $ExcludedSubOffer -ExcludedStates $ExcludedSubState -TenantId $tenantId $script:AzOpsResourceProvider = Get-AzResourceProvider $script:AzOpsAzManagementGroup = @() $script:AzOpsPartialRoot = @() #endregion Initialize & Prepare #region Management Group Processing try { $managementGroups = Get-AzManagementGroup -ErrorAction Stop } catch { Write-PSFMessage -Level Warning -String 'Initialize-AzOpsEnvironment.ManagementGroup.NoManagementGroupAccess' return } #region Validate root '/' permissions if ($currentAzContext.Account.Type -eq "User") { $rootPermissions = Get-AzRoleAssignment -UserPrincipalName $currentAzContext.Account.Id -Scope "/" -ErrorAction SilentlyContinue } else { $rootPermissions = Get-AzRoleAssignment -ObjectId (Get-AzADServicePrincipal -ApplicationId $currentAzContext.Account.Id).Id -Scope "/" -ErrorAction SilentlyContinue } if (-not $rootPermissions) { Write-PSFMessage -Level Warning -String 'Initialize-AzOpsEnvironment.ManagementGroup.NoRootPermissions' -StringValues $currentAzContext.Account.Id $PartialMgDiscovery = $true } #endregion Validate root '/' permissions #region Partial Discovery if ($PartialMgDiscovery -and $PartialMgDiscoveryRoot) { Write-PSFMessage -Level Warning -String 'Initialize-AzOpsEnvironment.ManagementGroup.PartialDiscovery' $managementGroups = @() foreach ($managementRoot in $PartialMgDiscoveryRoot) { $managementGroups += [pscustomobject]@{ Name = $managementRoot } $script:AzOpsPartialRoot += Get-AzManagementGroup -GroupId $managementRoot -Recurse -Expand -WarningAction SilentlyContinue } } #endregion Partial Discovery #region Management Group Resolution Write-PSFMessage -String 'Initialize-AzOpsEnvironment.ManagementGroup.Resolution' -StringValues $managementGroups.Count $tempResolved = foreach ($mgmtGroup in $managementGroups) { Write-PSFMessage -String 'Initialize-AzOpsEnvironment.ManagementGroup.Expanding' -StringValues $mgmtGroup.Name Get-AzOpsManagementGroups -ManagementGroup $mgmtGroup.Name -PartialDiscovery:$PartialMgDiscovery } $script:AzOpsAzManagementGroup = $tempResolved | Sort-Object -Property Id -Unique #endregion Management Group Resolution #endregion Management Group Processing Write-PSFMessage -String 'Initialize-AzOpsEnvironment.Processing.Completed' } } function Initialize-AzOpsRepository { <# .SYNOPSIS Setup a repository for the AzOps workflow, based off templates and an existing Azure deployment. .DESCRIPTION Setup a repository for the AzOps workflow, based off templates and an existing Azure deployment. .PARAMETER SkipPolicy Skip discovery of policies for better performance. .PARAMETER SkipRole Skip discovery of role. .PARAMETER SkipResourceGroup Skip discovery of resource groups .PARAMETER SkipResource Skip discovery of resources inside resource groups. .PARAMETER InvalidateCache Invalidate cached subscriptions and Management Groups and do a full discovery. .PARAMETER GeneralizeTemplates Will generalize json templates (only used when generating azopsreference). .PARAMETER ExportRawTemplate Export generic templates without embedding them in the parameter block. .PARAMETER Rebuild Delete all AutoGeneratedTemplateFolderPath folders inside AzOpsState directory. .PARAMETER Force Delete $script:AzOpsState directory. .PARAMETER PartialMgDiscovery Accept working with only a subset of management groups in the entire hierarchy. Needed when lacking root access. .PARAMETER PartialMgDiscoveryRoot The subset of management groups in the entire hierarchy with which to work. Needed when lacking root access. .PARAMETER StatePath The root folder under which to write the resource json. .EXAMPLE > Initialize-AzOpsRepository Setup a repository for the AzOps workflow, based off templates and an existing Azure deployment. #> [CmdletBinding()] param ( [switch] $SkipPolicy = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipPolicy'), [switch] $SkipRole = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipRole'), [switch] $SkipResourceGroup = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipResourceGroup'), [switch] $SkipResource = (Get-PSFConfigValue -FullName 'AzOps.Core.SkipResource'), [switch] $InvalidateCache = (Get-PSFConfigValue -FullName 'AzOps.Core.InvalidateCache'), [switch] $GeneralizeTemplates = (Get-PSFConfigValue -FullName 'AzOps.Core.GeneralizeTemplates'), [switch] $ExportRawTemplate = (Get-PSFConfigValue -FullName 'AzOps.Core.ExportRawTemplate'), [switch] $Rebuild, [switch] $Force, [switch] $PartialMgDiscovery = (Get-PSFConfigValue -FullName 'AzOps.Core.PartialMgDiscoveryRoot'), [string[]] $PartialMgDiscoveryRoot = (Get-PSFConfigValue -FullName 'AzOps.Core.PartialMgDiscoveryRoot'), [string] $StatePath = (Get-PSFConfigValue -FullName 'AzOps.Core.State'), [string] $TemplateParameterFileSuffix = (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix') ) begin { #region Initialize & Prepare Write-PSFMessage -String 'Initialize-AzOpsRepository.Initialization.Starting' if (-not $SkipRole) { try { Write-PSFMessage -String 'Initialize-AzOpsRepository.Validating.UserRole' Get-AzADUser -First 1 -ErrorAction Stop Write-PSFMessage -String 'Initialize-AzOpsRepository.Validating.UserRole.Success' } catch { Write-PSFMessage -Level Warning -String 'Initialize-AzOpsRepository.Validating.UserRole.Failed' $SkipRole = $true } } $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include InvalidateCache, PartialMgDiscovery, PartialMgDiscoveryRoot Initialize-AzOpsEnvironment @parameters Assert-AzOpsInitialization -Cmdlet $PSCmdlet -StatePath $StatePath $tenantId = (Get-AzContext).Tenant.Id Write-PSFMessage -String 'Initialize-AzOpsRepository.Tenant' -StringValues $tenantId Write-PSFMessage -String 'Initialize-AzOpsRepository.TemplateParameterFileSuffix' -StringValues (Get-PSFConfigValue -FullName 'AzOps.Core.TemplateParameterFileSuffix') Write-PSFMessage -String 'Initialize-AzOpsRepository.Initialization.Completed' $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() #endregion Initialize & Prepare } process { #region Existing Content if (Test-Path $StatePath) { $migrationRequired = (Get-ChildItem -Recurse -Force -Path $StatePath -File | Where-Object { $_.Name -like $("Microsoft.Management_managementGroups-" + $tenantId + $TemplateParameterFileSuffix) } | Select-Object -ExpandProperty FullName -First 1) -notmatch '\((.*)\)' if ($migrationRequired) { Write-PSFMessage -String 'Initialize-AzOpsRepository.Migration.Required' } if ($Force -or $migrationRequired) { Invoke-PSFProtectedCommand -ActionString 'Initialize-AzOpsRepository.Deleting.State' -ActionStringValues $StatePath -Target $StatePath -ScriptBlock { Remove-Item -Path $StatePath -Recurse -Force -Confirm:$false -ErrorAction Stop } -EnableException $true -PSCmdlet $PSCmdlet } if ($Rebuild) { Invoke-PSFProtectedCommand -ActionString 'Initialize-AzOpsRepository.Rebuilding.State' -ActionStringValues $StatePath -Target $StatePath -ScriptBlock { Get-ChildItem -Path $StatePath -File -Recurse -Force -Filter 'Microsoft.*_*.json' | Remove-Item -Force -Recurse -Confirm:$false -ErrorAction Stop } -EnableException $true -PSCmdlet $PSCmdlet } } #endregion Existing Content #region Root Scopes $rootScope = '/providers/Microsoft.Management/managementGroups/{0}' -f $tenantId if ($script:AzOpsPartialRoot.id) { $rootScope = $script:AzOpsPartialRoot.id | Sort-Object -Unique } if ($rootScope -and $script:AzOpsAzManagementGroup) { foreach ($root in $rootScope) { # Create AzOpsState Structure recursively Save-AzOpsManagementGroupChildren -Scope $root -StatePath $StatePath # Discover Resource at scope recursively $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include SkipPolicy, SkipRole, SkipResourceGroup, SkipResource, ExportRawTemplate, StatePath Get-AzOpsResourceDefinition -Scope $root @parameters } } else { # If no management groups are found, iterate through each subscription foreach ($subscription in $script:AzOpsSubscriptions) { $parameters = $PSBoundParameters | ConvertTo-PSFHashtable -Inherit -Include SkipPolicy, SkipRole, SkipResourceGroup, SkipResource, ExportRawTemplate, StatePath Get-AzOpsResourceDefinition -Scope $subscription.id @parameters } } #endregion Root Scopes } end { $stopWatch.Stop() Write-PSFMessage -String 'Initialize-AzOpsRepository.Duration' -StringValues $stopWatch.Elapsed -Data @{ Elapsed = $stopWatch.Elapsed } } } Set-PSFConfig -Module AzOps -Name Core.AllInOneTemplate -Value $false -Initialize -Validation bool -Description '-' Set-PSFConfig -Module AzOps -Name Core.AutoGeneratedTemplateFolderPath -Value "." -Initialize -Validation string -Description 'Auto-Genereated Template Folder Path i.e. ./Az' Set-PSFConfig -Module AzOps -Name Core.AutoInitialize -Value $false -Initialize -Validation bool -Description '-' Set-PSFConfig -Module AzOps -Name Core.DefaultDeploymentRegion -Value northeurope -Initialize -Validation string -Description 'Default deployment region for state deployments (ARM region, not region where a resource is deployed)' Set-PSFConfig -Module AzOps -Name Core.EnrollmentAccountPrincipalName -Value '' -Initialize -Validation string -Description '-' Set-PSFConfig -Module AzOps -Name Core.ExcludedSubOffer -Value 'AzurePass_2014-09-01', 'FreeTrial_2014-09-01', 'AAD_2015-09-01' -Initialize -Validation stringarray -Description 'Excluded QuotaID' Set-PSFConfig -Module AzOps -Name Core.ExcludedSubState -Value 'Disabled', 'Deleted', 'Warned', 'Expired' -Initialize -Validation stringarray -Description 'Excluded subscription states' Set-PSFConfig -Module AzOps -Name Core.ExportRawTemplate -Value $false -Initialize -Validation bool -Description '-' Set-PSFConfig -Module AzOps -Name Core.GeneralizeTemplates -Value $false -Initialize -Validation bool -Description 'Invalidates cache and ensures that Management Groups and Subscriptions are re-discovered' Set-PSFConfig -Module AzOps -Name Core.IgnoreContextCheck -Value $false -Initialize -Validation bool -Description 'If set to $true, skip AAD tenant validation == 1' Set-PSFConfig -Module AzOps -Name Core.InvalidateCache -Value $true -Initialize -Validation bool -Description 'Invalidates cache and ensures that Management Groups and Subscriptions are re-discovered' Set-PSFConfig -Module AzOps -Name Core.JqTemplatePath -Value "$script:ModuleRoot\data\template" -Initialize -Validation string -Description 'default path to search for jq template' Set-PSFConfig -Module AzOps -Name Core.MainTemplate -Value "$script:ModuleRoot\data\template\template.json" -Initialize -Validation string -Description 'Main template json' Set-PSFConfig -Module AzOps -Name Core.OfferType -Value 'MS-AZR-0017P' -Initialize -Validation string -Description '-' Set-PSFConfig -Module AzOps -Name Core.PartialMgDiscoveryRoot -Value @() -Initialize -Validation stringarray -Description 'Used in combination with AZOPS_SUPPORT_PARTIAL_MG_DISCOVERY, example value: "Contoso","Tailspin","Management"' Set-PSFConfig -Module AzOps -Name Core.SkipPolicy -Value $false -Initialize -Validation bool -Description '-' Set-PSFConfig -Module AzOps -Name Core.SkipResource -Value $true -Initialize -Validation bool -Description 'Global flag to indicate whether resource should be discovered or not. Requires SkipResourceGroup to be false.' Set-PSFConfig -Module AzOps -Name Core.SkipResourceGroup -Value $false -Initialize -Validation bool -Description 'Global flag to indicate whether resource group should be discovered or not' Set-PSFConfig -Module AzOps -Name Core.SkipRole -Value $false -Initialize -Validation bool -Description '-' Set-PSFConfig -Module AzOps -Name Core.State -Value (Join-Path $pwd -ChildPath "root") -Initialize -Validation string -Description 'Folder to store AzOpsState artefact' Set-PSFConfig -Module AzOps -Name Core.SubscriptionsToIncludeResourceGroups -Value @('*') -Initialize -Validation stringarray -Description 'Requires SkipResourceGroup to be false. Subscription ID or Display Name that matches the filter. Powershell filter that matches with like operator is supported.' Set-PSFConfig -Module AzOps -Name Core.SupportPartialMgDiscovery -Value $false -Initialize -Validation bool -Description 'Enable partial discovery' Set-PSFConfig -Module AzOps -Name Core.TemplateParameterFileSuffix -Value '.json' -Initialize -Validation string -Description 'parameter file suffix to look for' Set-PSFConfig -Module AzOps -Name Core.ThrottleLimit -Value 10 -Initialize -Validation integer -Description 'Throttle limit used in Foreach-Object -Parallel for resource/subscription discovery' <# 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 'AzOps.ScriptBlockName' -Scriptblock { } #> <# # Example: Register-PSFTeppScriptblock -Name "AzOps.Test" -ScriptBlock { 'Test' } #> <# # Example: Register-PSFTeppArgumentCompleter -Command Get-Test -Parameter Type -Name AzOps.x #> # Module Cache for Subscriptions accessible for the current account $script:AzOpsSubscriptions = @() # Module Cache for Management Groups that are in scope for this module $script:AzOpsAzManagementGroup = @() # Module Cache for Management Group Roots that are in scope for this module, when accepting partial processing $script:AzOpsPartialRoot = @() # Module cache to load resource provider version $script:AzOpsResourceProvider = $null Set-PSFFeature -Name PSFramework.Stop-PSFFunction.ShowWarning -Value $true -ModuleName AzOps if (Get-PSFConfigValue -FullName AzOps.Core.AutoInitialize) { if ([runspace]::DefaultRunspace.Id -eq 1) { Initialize-AzOpsEnvironment } } New-PSFLicense -Product 'AzOps' -Manufacturer 'Friedrich Weinmann' -ProductVersion $script:ModuleVersion -ProductType Module -Name MIT -Version "1.0.0.0" -Date (Get-Date "2020-11-07") -Text @" Copyright (c) 2020 Friedrich Weinmann 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. "@ #endregion Load compiled code |