AZSBTools.psm1
function New-SBAZServicePrincipal { <# .SYNOPSIS Function to create Azure AD Service Principal .DESCRIPTION Function to create Azure AD Service Principal The use case intended for this function is to use the Service Principal to run PowerShell scripts against an Azure subscription .PARAMETER ServicePrincipalName One or more Service Principal Names .PARAMETER Environment Name of the Azure cloud. This parameter default to Azure Commercial cloud. As of 15 March 2018 that list is: AzureGermanCloud AzureCloud AzureUSGovernment AzureChinaCloud To see an updated list, use: (Get-AzureRMEnvironment).Name .PARAMETER Role This parameter is used to assign Role/Permissions for te Service Principal in the current subscription. The default value is 'Owner' role. As of 16 March 2018 the following default roles are defined: API Management Service Contributor Application Insights Component Contributor Automation Operator BizTalk Contributor Classic Network Contributor Classic Storage Account Contributor Classic Storage Account Key Operator Service Role Classic Virtual Machine Contributor ClearDB MySQL DB Contributor Contributor Cosmos DB Account Reader Role Data Factory Contributor Data Lake Analytics Developer DevTest Labs User DNS Zone Contributor DocumentDB Account Contributor Intelligent Systems Account Contributor Log Analytics Contributor Log Analytics Reader Network Contributor New Relic APM Account Contributor Owner Reader Redis Cache Contributor Scheduler Job Collections Contributor Search Service Contributor Security Manager SQL DB Contributor SQL Security Manager SQL Server Contributor Storage Account Contributor Storage Account Key Operator Service Role Traffic Manager Contributor User Access Administrator Virtual Machine Contributor Web Plan Contributor Website Contributor For more details on roles, type in: Get-AzureRmRoleDefinition | select name,description,actions | Out-GridView .EXAMPLE $SPList = New-SBAZServicePrincipal -ServicePrincipalName samtest1,sam1demo .EXAMPLE $SPN = New-SBAZServicePrincipal -ServicePrincipalName PowerShell05 -Environment AzureUSGovernment # The above line creates the SPN and gives it 'Owner' permission/role in the current subscription $SPN | Export-Csv .\PowerShell05-SPN.csv -NoTypeInformation # This line saves the $SPN to CSV (not the password) # To use the SPN in future automations: # $SPN = Import-Csv .\PowerShell05-SPN.csv # Login-AzureRmAccount -Credential (Get-SBCredential $SPN.ServicePrincipalName) -ServicePrincipal -TenantId $SPN.TenantID -Environment $SPN.Environment .OUTPUTS The function returns a PS Object for each input Service Principal Name containing the following properties: ServicePrincipalName TenantId Environment Role .LINK https://superwidgets.wordpress.com/2018/03/15/new-sbazserviceprincipal-cmdlet-to-create-new-azure-ad-service-principal-added-to-azsbtools-powershell-module/ .NOTES Function by Sam Boutros v0.1 - 14 March 2018 v0.2 - 15 March 2018 - Added 'Environment' parameter v0.3 - 16 March 2018 - Added 'Role' parameter, changed output to a custom PS Object #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String[]]$ServicePrincipalName, [Parameter(Mandatory=$false)][ValidateSet('AzureCloud','AzureUSGovernment','AzureGermanCloud','AzureChinaCloud')][String]$Environment = 'AzureCloud', [Parameter(Mandatory=$false)][String]$Role = 'Owner' ) Begin { $Subscription = Connect-AzureRmAccount -Environment $Environment } Process { if ($Subscription.Context.Subscription.Name) { Write-Log 'Identified',$Subscription.Context.Subscription.Name,'subscription in the',$Subscription.Context.Environment.Name,'cloud' Green,Cyan,Green,Cyan,Green $SPList = foreach ($AppName in $ServicePrincipalName) { $AppCred = Get-SBCredential -UserName $AppName #region Create/Validate Azure AD App Remove-Variable App -EA 0 if ($App = Get-AzureRmADApplication -DisplayName $AppName) { Write-Log 'Validated app:',$App.Displayname Green,Cyan } else { $App = New-AzureRmADApplication -DisplayName $AppName -IdentifierUris $AppName Write-Log 'Created app:',$App.Displayname Green,Cyan } #endregion #region Create/Validate Azure AD Service Principal Remove-Variable ServicePrincipal -EA 0 if ($ServicePrincipal = Get-AzureRmADServicePrincipal | where { $PSItem.ApplicationId -eq $App.ApplicationId.Guid }) { Write-Log 'Validated Service Principal:',($ServicePrincipal.SerVicePrincipalNames -join ', ') Green,Cyan } else { $ServicePrincipal = New-AzureRmADServicePrincipal -ApplicationId $App.ApplicationId.Guid -Password $AppCred.Password Write-Log 'Created Service Principal:',($ServicePrincipal.SerVicePrincipalNames -join ', ') Green,Cyan } #endregion #region Assign Role (Permissions) Write-Log 'Assigning role',$Role Green,Cyan -NoNewLine $Result = try { New-AzureRmRoleAssignment -ObjectId $ServicePrincipal.Id -RoleDefinitionName $Role -Scope "/subscriptions/$($Subscription.Context.Subscription.Id)" -EA 1 Write-Log 'done' Green } catch { Write-Log $PSItem.Exception.Message Yellow } #endregion [PSCustomObject][Ordered]@{ ServicePrincipalName = $AppName TenantId = (Get-AzureRmTenant).Id Environment = $Environment Role = $Role } } } else { Write-Log 'No subscriptions found for account',$Subscription.Context.Account.Id,'in the',$Subscription.Context.Environment.Name,'cloud' Magenta,Yellow,Magenta,Yellow,Magenta } } End { $SPList } } function Deploy-AzureARMVM { <# .SYNOPSIS Function to automate provisioning of Azure ARM VM(s) .DESCRIPTION Function to automate provisioning of Azure ARM VM(s) .PARAMETER SubscriptionName Name of existing Azure subscription .PARAMETER Location Name of Azure Data center/Location Example: 'eastus' To see location list use: Get-AzureRmLocation | sort Location | Select Location .PARAMETER ResourceGroup Name of Resource Group. Example: 'VMGroup17' The script will create it if it does not exist .PARAMETER AvailabilitySetName Example: 'Availability17' The script will create it if it does not exist .PARAMETER ConfirmShutdown This switch accepts $true or $False, and defaaults to $False If adding existing VMs to Availaibility set, the script must shut down the VMs .PARAMETER StorageAccountPrefix Only lower case letters and numbers, must be Azure (globally) unique .PARAMETER AdminName Example: 'myAdmin17' This will be the new VM local administrator .PARAMETER VMName Example: ('vm01','vm02') Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set .PARAMETER VMSize Example: 'Standard_A1_v2' To see available sizes in this Azure location use: (Get-AzureRoleSize).RoleSizeLabel .PARAMETER WinOSImage This defaults to '2012-R2-Datacenter' Available options: '2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server' To see current options in a given Azure Location use: (Get-AzureRMVMImageSku -Location usgovvirginia -Publisher MicrosoftWindowsServer -Offer WindowsServer).Skus For more information see https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage .PARAMETER vNetName Example: 'Seventeen' This will be the name of the virtual network to be created/updated if exist .PARAMETER vNetPrefix Example: '10.17.0.0/16' To be created/updated .PARAMETER SubnetName Example: 'vmSubnet' This will be the name of the subnet to be created/updated .PARAMETER SubnetPrefix Example: '10.17.0.0/24' Must be subset of vNetPrefix above - to be created/updated .PARAMETER LogFile' Path to log file where this scrit will log its commands and output Default is ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" .EXAMPLE Connect-AzureRmAccount -Environment AzureUSGovernment $myParamters = @{ SubscriptionName = 'Azure Government T1' Location = 'usgovvirginia' ResourceGroup = 'EncryptionTest01' AvailabilitySetName = 'AvailabilityTest01' ConfirmShutdown = $false StorageAccountPrefix = 'sam150318a' AdminName = 'myAdmin150318a' VMName = @('vm01','vm02','vm03') VMSize = 'Standard_A0' WinOSImage = '2016-Datacenter' vNetName = 'EncryptionTest01VNet' vNetPrefix = '10.3.0.0/16' SubnetName = 'vmSubnet' SubnetPrefix = '10.3.15.0/24' } Deploy-AzureARMVM @myParamters .LINK http://www.exigent.net/blog/microsoft-azure/provisioning-and-tearing-down-azure-virtual-machines/ .NOTES Function by Sam Boutros 3 January 2017 - v0.1 - Initial release 19 January 2017 - v0.2 Updated parameters - set to mandatory Updated Storage Account creation region, create a separate storage account for each VM Updated Initialize region; removing subscription login, adding input echo, adding error handling Added functionality to configure VMs in availability set 5 March 2018 - v0.3 Cosmetic updates #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$SubscriptionName , # Example: 'Sam Test 1' # Name of existing Azure subscription [Parameter(Mandatory=$true)][String]$Location , # Example: 'eastus' # Get-AzureRmLocation | sort Location | Select Location [Parameter(Mandatory=$true)][String]$ResourceGroup , # Example: 'VMGroup17' # To be created if not exist [Parameter(Mandatory=$false)][String]$AvailabilitySetName , # Example: 'Availability17' # To be created if not exist [Parameter(Mandatory=$false)][Switch]$ConfirmShutdown = $false, # If adding existing VMs to Availaibility set, the script must shut down the VMs [Parameter(Mandatory=$false)][String]$StorageAccountPrefix , # To be created if not exist, only lower case letters and numbers, must be Azure unique [Parameter(Mandatory=$true)][String]$AdminName , # Example: 'myAdmin17' # This will be the new VM local administrator [Parameter(Mandatory=$true)][String[]]$VMName , # Example: ('vm01','vm02') # Name(s) of VM(s) to be created. Each is 15 characters maximum. If VMs exist, they will be added to Availability Set [Parameter(Mandatory=$true)][String]$VMSize , # Example: 'Standard_A1_v2' # (Get-AzureRoleSize).RoleSizeLabel to see available sizes in this Azure location [Parameter(Mandatory=$false)][ValidateSet('2008-R2-SP1','2012-Datacenter','2012-R2-Datacenter','2016-Datacenter','2016-Datacenter-Server-Core','2016-Datacenter-with-Containers','2016-Nano-Server')] [String]$WinOSImage = '2012-R2-Datacenter' , # https://docs.microsoft.com/en-us/azure/virtual-machines/windows/cli-ps-findimage [Parameter(Mandatory=$true)][String]$vNetName , # Example: 'Seventeen' # This will be the name of the virtual network to be created/updated if exist [Parameter(Mandatory=$true)][String]$vNetPrefix , # Example: '10.17.0.0/16' # To be created/updated [Parameter(Mandatory=$true)][String]$SubnetName , # Example: 'vmSubnet' # This will be the name of the subnet to be created/updated [Parameter(Mandatory=$true)][String]$SubnetPrefix , # Example: '10.17.0.0/24' # Must be subset of vNetPrefix above - to be created/updated [Parameter(Mandatory=$false)][String]$LogFile = ".\Logs\Deploy-AzureARMVM-$($VMName -join '_')-$(Get-Date -Format 'ddMMMMyyyy_hh-mm-ss_tt').txt" ) Begin { #region Initialize if (!(Test-Path (Split-Path $LogFile))) { New-Item -Path (Split-Path $LogFile) -ItemType directory -Force | Out-Null } Write-Log 'Input received:' Green $LogFile write-log " SubscriptionName: $SubscriptionName" Cyan $LogFile write-log " Location: $Location" Cyan $LogFile write-log " ResourceGroup: $ResourceGroup" Cyan $LogFile write-log " AvailabilitySetName: $AvailabilitySetName" Cyan $LogFile write-log " ConfirmShutdown: $ConfirmShutdown" Cyan $LogFile write-log " StorageAccountPrefix: $StorageAccountPrefix" Cyan $LogFile write-log " AdminName: $AdminName" Cyan $LogFile write-log " VMName(s): $($VMName -join ', ')" Cyan $LogFile write-log " VMSize: $VMSize" Cyan $LogFile write-log " vNetName: $vNetName" Cyan $LogFile write-log " vNetPrefix: $vNetPrefix" Cyan $LogFile write-log " SubnetName: $SubnetName" Cyan $LogFile write-log " SubnetPrefix: $SubnetPrefix" Cyan $LogFile $Cred = Get-SBCredential -UserName $AdminName #endregion #region Connect to Azure subscription Write-Log 'Connecting to Azure subscription',$SubscriptionName Green,Cyan $LogFile -NoNewLine try { $Result = Get-AzureRmSubscription –SubscriptionName $SubscriptionName -ErrorAction Stop | Select-AzureRmSubscription Write-Log 'done' Green $LogFile Write-Log ($Result | Out-String).Trim() Cyan $LogFile } catch { throw "unable to get Azure Subscription '$SubscriptionName'" } #endregion #region Create/Update Resource group Write-Log 'Create/Update Resource group',$ResourceGroup Green,Cyan $LogFile -NoNewLine try { $Result = New-AzureRmResourceGroup -Name $ResourceGroup -Location $Location -Force -ErrorAction Stop Write-Log 'done' Green $LogFile Write-Log ($Result | Out-String).Trim() Cyan $LogFile } catch { throw "Failed to create Resource Group '$ResourceGroup'" } #endregion #region Create/Update Subnet and vNet Write-Log 'Creating/updating vNet',$vNetName,$vNetPrefix,'and subnet',$SubnetName,$SubnetPrefix Cyan,Green,DarkYellow,Cyan,Green,DarkYellow $LogFile -NoNewLine $Subnet = New-AzureRmVirtualNetworkSubnetConfig -Name $SubnetName -AddressPrefix $SubnetPrefix $vNet = New-AzureRmVirtualNetwork -Name $vNetName -ResourceGroupName $ResourceGroup -Location $Location -AddressPrefix $vNetPrefix -Subnet $Subnet -Force Write-Log 'done' Green #endregion } Process { foreach ($Name in $VMName) { # Provision Azure VM(s) #region Create Storage Account if it does not exist $StorageAccountName = "stor$($StorageAccountPrefix.ToLower())$($Name.ToLower())" if ($StorageAccountName.Length -gt 20) { Write-Log 'Storage account name',$StorageAccountName,'is too long, using first 20 characters only..' Green,Yellow,Green $LogFile $StorageAccountName = $StorageAccountName.Substring(0,19) } Write-Log 'Creating Storage Account',$StorageAccountName Green,Cyan $LogFile try { $StorageAccount = Get-AzureRmStorageAccount -Name $StorageAccountName -ResourceGroupName $ResourceGroup -ErrorAction Stop Write-Log 'Using existing storage account',$StorageAccountName Green,Cyan $LogFile } catch { $i=0 $DesiredStorageAccountName = $StorageAccountName while (!(Get-AzureRmStorageAccountNameAvailability $StorageAccountName).NameAvailable) { $i++ $StorageAccountName = "$StorageAccountName$i" } if ($DesiredStorageAccountName -ne $StorageAccountName ) { Write-Log 'Storage account',$DesiredStorageAccountName,'is taken, using',$StorageAccountName,'instead (available)' Greem,Yellow,Green,Cyan,Green $LogFile } try { $Splatt = @{ ResourceGroupName = $ResourceGroup Name = $StorageAccountName SkuName = 'Standard_LRS' Kind = 'Storage' Location = $Location ErrorAction = 'Stop' } $StorageAccount = New-AzureRmStorageAccount @Splatt Write-Log 'Created storage account',$StorageAccountName Green,Cyan $LogFile } catch { Write-Log 'Failed to create storage account',$StorageAccountName Magenta,Yellow $LogFile throw $PSItem.exception.message } } #endregion #region Create/validate Availability Set if ($AvailabilitySetName) { Write-Log 'Creating/verifying Availability Set',$AvailabilitySetName Green,Cyan $LogFile try { $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -ErrorAction Stop Write-Log 'Availability Set',$AvailabilitySetName,'already exists' Green,Yellow,Green $LogFile Write-Log ($AvailabilitySet | Out-String).Trim() Cyan $LogFile } catch { try { $AvailabilitySet = New-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName -Location $Location -ErrorAction Stop Write-Log 'Created Availability Set',$AvailabilitySetName Green,Cyan $LogFile } catch { Write-Log 'Failed to create Availability Set',$AvailabilitySetName Magenta,Yellow $LogFile throw $PSItem.exception.message } } if ($AvailabilitySet.Location -ne $Location) { Write-Log 'Unable to proceed, Availability set must be in the same location',$AvailabilitySet.Location,'as the desired VM location',$Location Magenta,Yellow,Magenta,Yellow $LogFile break } } #endregion try { $ExistingVM = Get-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -ErrorAction Stop Write-Log 'VM',$ExistingVM.Name,'already exists' Green,Yellow,Gree $LogFile if ($AvailabilitySetName) { if ($ConfirmShutdown) { Write-Log 'Shutting down VM',$Name,'to add it to Availability set',$AvailabilitySetName Green,Cayn,Green,Cyan $LogFile Stop-AzureRmVM -Name $Name -Force -StayProvisioned -ResourceGroupName $ResourceGroup -Confirm:$false # Remove current VM Remove-AzureRmVM -ResourceGroupName $ResourceGroup -Name $Name -Force -Confirm:$false # Prepare to recreate VM $VM = New-AzureRmVMConfig -VMName $ExistingVM.Name -VMSize $ExistingVM.HardwareProfile.VmSize -AvailabilitySetId $AvailabilitySet.Id Set-AzureRmVMOSDisk -VM $VM -VhdUri $ExistingVM.StorageProfile.OsDisk.Vhd.Uri -Name $ExistingVM.Name -CreateOption Attach -Windows #Add Data Disks foreach ($Disk in $ExistingVM.StorageProfile.DataDisks) { Add-AzureRmVMDataDisk -VM $VM -Name $Disk.Name -VhdUri $Disk.Vhd.Uri -Caching $Disk.Caching -Lun $Disk.Lun -CreateOption Attach -DiskSizeInGB $Disk.DiskSizeGB } #Add NIC(s) foreach ($NIC in $ExistingVM.NetworkInterfaceIDs) { Add-AzureRmVMNetworkInterface -VM $VM -Id $NIC } # Recreate the VM as part of the Availability Set New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $ExistingVM.Location -VM $VM -DisableBginfoExtension } else { Write-Log 'To add existing VM(s) to availability set, the VM(s) must be shut down. Use the','-ConfirmShutdown:$true','switch' Yellow,Cyan,Yellow $LogFile break } } } catch { Write-Log 'Preparing to create new VM',$Name Green,Cyan $LogFile Write-Log 'Requesting/updating public IP address assignment',"$Name-PublicIP" Green,Cyan $LogFile $PublicIp = New-AzureRmPublicIpAddress -Name "$Name-PublicIP" -ResourceGroupName $ResourceGroup -Location $Location -AllocationMethod Dynamic -Force Write-Log 'Provisining/updating vNIC',"$Name-vNIC" Green,Cyan $LogFile $vNIC = New-AzureRmNetworkInterface -Name "$Name-vNIC" -ResourceGroupName $ResourceGroup -Location $Location -SubnetId $vNet.Subnets[0].Id -PublicIpAddressId $PublicIp.Id -Force Write-Log 'Provisioning VM configuration object for VM',$Name Green,Cyan $LogFile if ($AvailabilitySetName) { $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize -AvailabilitySetId $AvailabilitySet.Id } else { $VM = New-AzureRmVMConfig -VMName $Name -VMSize $VMSize } Write-Log 'Configuring VM OS (Windows),',$Cred.UserName,'local admin' Green,Cyan,Green $LogFile $VM = Set-AzureRmVMOperatingSystem -VM $VM -Windows -ComputerName $Name -Credential $Cred -ProvisionVMAgent -EnableAutoUpdate Write-Log 'Selecting VM image - Latest',$WinOSImage Green,Cyan $LogFile $VM = Set-AzureRmVMSourceImage -VM $VM -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" -Skus $WinOSImage -Version "latest" Write-Log 'Adding vNIC' Green $LogFile $VM = Add-AzureRmVMNetworkInterface -VM $VM -Id $vNIC.Id $VhdUri = "$($StorageAccount.PrimaryEndpoints.Blob.ToString())vhds/$($Name)-OsDisk1.vhd" Write-Log 'Configuring OS Disk',$VhdUri Green,Cyan $LogFile $VM = Set-AzureRmVMOSDisk -VM $VM -Name 'OSDisk' -VhdUri $VhdUri -CreateOption FromImage Write-Log 'Creating VM..' Green -NoNewLine New-AzureRmVM -ResourceGroupName $ResourceGroup -Location $Location -VM $VM Write-Log 'done' Green $LogFile $DoneVM = Get-AzureRmVM | where { $_.Name -eq $Name } | FT -a Write-Log ($DoneVM | Out-String).Trim() cyan $LogFile } } } End { if ($AvailabilitySetName) { $AvailabilitySet = Get-AzureRmAvailabilitySet -ResourceGroupName $ResourceGroup -Name $AvailabilitySetName $VMDomains = $AvailabilitySet.VirtualMachinesReferences | foreach { $VM = Get-AzureRMVM -Name (Get-AzureRmResource -Id $_.id).Name -ResourceGroup $ResourceGroup -Status [PSCustomObject][Ordered]@{ Name = $VM.Name FaultDomain = $VM.PlatformFaultDomain UpdateDomain = $VM.PlatformUpdateDomain } } Write-Log ($VMDomains | sort Name | FT -a | Out-String).Trim() Cyan $LogFile } } } function Expand-Json { <# .SYNOPSIS Function to expand a custom PowerShell object in a more readable format .DESCRIPTION Function to expand a custom PowerShell object in a more readable format The ConvertFrom-Json cmdlet of the Microsoft.PowerShell.Utility module outputs a PS Custom Object that often contains sub objects and so on. This function expands all objects and displays the key/value pairs in a more humanly readable format - see the example .PARAMETER Json PS Custom Object, typically the output of ConvertFrom-Json cmdlet - see the example .PARAMETER Parent This is optional parameter used to show sub-objects when using the function recursively .EXAMPLE Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-Json where the contents of Storage1.json file are: { "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", "contentVersion": "1.0.0.0", "parameters": { "storageAccountType": { "type": "string", "defaultValue": "Standard_LRS", "allowedValues": [ "Standard_LRS", "Standard_GRS", "Standard_ZRS", "Premium_LRS" ], "metadata": { "description": "Storage Account type" } } }, "variables": { "storageAccountName": "[concat(uniquestring(resourceGroup().id), 'standardsa')]" }, "resources": [ { "type": "Microsoft.Storage/storageAccounts", "name": "[variables('storageAccountName')]", "apiVersion": "2016-01-01", "location": "[resourceGroup().location]", "sku": { "name": "[parameters('storageAccountType')]" }, "kind": "Storage", "properties": { } } ], "outputs": { "storageAccountName": { "type": "string", "value": "[variables('storageAccountName')]" } } } The output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json would look like: $schema : https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json# contentVersion : 1.0.0.0 parameters : @{storageAccountType=} variables : @{storageAccountName=[concat(uniquestring(resourceGroup().id), 'standardsa')]} resources : {@{type=Microsoft.Storage/storageAccounts; name=[variables('storageAccountName')]; apiVersion=2016-01-01; location=[resourceGroup().location]; sku=; kind=Storage; properties=}} outputs : @{storageAccountName=} which does not show sub-objects such as parameters.storageAccountType.allowedValues, parameters.storageAccountType.defaultValue, ... However, the output of Get-Content E:\Scripts\ARMTemplates\Storage1.json | ConvertFrom-Json | Expand-Json shows all objects, sub-objects, and their key/pair values: $schema: https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json# contentVersion: 1.0.0.0 outputs.storageAccountName.type: string outputs.storageAccountName.value: [variables('storageAccountName')] parameters.storageAccountType.allowedValues: Standard_LRS, Standard_GRS, Standard_ZRS, Premium_LRS parameters.storageAccountType.defaultValue: Standard_LRS parameters.storageAccountType.metadata.description: Storage Account type parameters.storageAccountType.type: string resources.apiVersion: 2016-01-01 resources.kind: Storage resources.location: [resourceGroup().location] resources.name: [variables('storageAccountName')] resources.sku.name: [parameters('storageAccountType')] resources.type: Microsoft.Storage/storageAccounts variables.storageAccountName: [concat(uniquestring(resourceGroup().id), 'standardsa')] .LINK https://superwidgets.wordpress.com/ .NOTES Function by Sam Boutros v0.1 - 28 March 2018 #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true,ValueFromPipeLine=$true,ValueFromPipeLineByPropertyName=$true)][PSCustomObject]$JSON, [Parameter(Mandatory=$false)][String[]]$Parent ) Begin { Write-Verbose "JSON: $($JSON | Out-String)" Write-Verbose "Parent: $($Parent -join '.')" } Process { foreach ($NoteProperty in ($JSON | Get-Member -MemberType NoteProperty)) { if ($NoteProperty.Definition -match 'PSCustomObject') { Expand-Json -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name) } else { if (($JSON.($NoteProperty.Name) -join '').Trim()) { Write-Log "$(($Parent + $NoteProperty.Name) -join '.'):",($JSON.($NoteProperty.Name) -join ', ') Green,Cyan } else { Expand-Json -JSON $JSON.($NoteProperty.Name) -Parent ($Parent + $NoteProperty.Name) -EA 0 } } } } End { } } function Write-Log { <# .SYNOPSIS Function to log input string to file and display it to screen .DESCRIPTION Function to log input string to file and display it to screen. Log entries in the log file are time stamped. Function allows for displaying text to screen in different colors. .PARAMETER String The string to be displayed to the screen and saved to the log file .PARAMETER Color The color in which to display the input string on the screen Default is White 16 valid options for [System.ConsoleColor] type are Black Blue Cyan DarkBlue DarkCyan DarkGray DarkGreen DarkMagenta DarkRed DarkYellow Gray Green Magenta Red White Yellow .PARAMETER LogFile Path to the file where the input string should be saved. Example: c:\log.txt If absent, the input string will be displayed to the screen only and not saved to log file .EXAMPLE Write-Log -String "Hello World" -Color Yellow -LogFile c:\log.txt This example displays the "Hello World" string to the console in yellow, and adds it as a new line to the file c:\log.txt If c:\log.txt does not exist it will be created. Log entries in the log file are time stamped. Sample output: 2014.08.06 06:52:17 AM: Hello World .EXAMPLE Write-Log "$((Get-Location).Path)" Cyan This example displays current path in Cyan, and does not log the displayed text to log file. .EXAMPLE "$((Get-Process | select -First 1).name) process ID is $((Get-Process | select -First 1).id)" | Write-Log -color DarkYellow Sample output of this example: "MDM process ID is 4492" in dark yellow .EXAMPLE Write-Log 'Found',(Get-ChildItem -Path .\ -File).Count,'files in folder',(Get-Item .\).FullName Green,Yellow,Green,Cyan .\mylog.txt Sample output will look like: Found 520 files in folder D:\Sandbox - and will have the listed foreground colors .EXAMPLE Write-Log (Get-Volume | sort DriveLetter | Out-String).Trim() Cyan .\mylog.txt Sample output will look like (in Cyan, and will also be written to .\mylog.txt): DriveLetter FriendlyName FileSystemType DriveType HealthStatus OperationalStatus SizeRemaining Size ----------- ------------ -------------- --------- ------------ ----------------- ------------- ---- Recovery NTFS Fixed Healthy OK 101.98 MB 450 MB C NTFS Fixed Healthy OK 7.23 GB 39.45 GB D Unknown CD-ROM Healthy Unknown 0 B 0 B E Data NTFS Fixed Healthy OK 26.13 GB 49.87 GB .LINK https://superwidgets.wordpress.com/2014/12/01/powershell-script-function-to-display-text-to-the-console-in-several-colors-and-save-it-to-log-with-timedate-stamp/ .NOTES Function by Sam Boutros v1.0 - 6 August 2014 v1.1 - 1 December 2014 - added multi-color display in the same line v1.2 - 8 August 2016 - updated date time stamp format, protect against bad LogFile name v1.3 - 22 September 2017 - Re-write: Error handling for no -String parameter, bad color(s), and bad -LogFile without errors Add Verbose messages #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [String[]]$String, [Parameter(Mandatory=$false,Position=1)][String[]]$Color, [Parameter(Mandatory=$false,Position=2)][String]$LogFile, [Parameter(Mandatory=$false,Position=3)][Switch]$NoNewLine ) if ($String) { $ConsoleColor = @('Black','Blue','Cyan','DarkBlue','DarkCyan','DarkGray','DarkGreen','DarkMagenta', 'DarkRed','DarkYellow','Gray','Green','Magenta','Red','White','Yellow') $ColorList = $Color | % { if ($_ -in $ConsoleColor) { $_ } else { 'White' } } if (!$ColorList) { $ColorList = 'Green' } if ($String.Count -gt 1) { $i=0 foreach ($item in $String) { if ($ColorList.Count -gt 1) { if ($ColorList[$i]) { $col = $ColorList[$i] } else { $col = 'White' } } else { if ($i -eq 0) { $col = $ColorList } else { $col = 'White' } } Write-Host "$item " -ForegroundColor $col -NoNewline $i++ } } else { # 1 String if ($ColorList.Count -gt 1) { $col = $ColorList[0] } else { $col = $ColorList } Write-Host "$String " -ForegroundColor $col -NoNewline } if (!$NoNewLine) { Write-Host ' ' } try { "$(Get-Date -format 'dd MMMM yyyy hh:mm:ss tt'): $($String -join ' ')" | Out-File -Filepath $Logfile -Append -ErrorAction Stop } catch { Write-Verbose 'Write-Log: Missing -LogFile parameter or bad LogFile name. Will not save input string(s) to log file..' } } else { Write-Verbose 'Write-Log: Missing -String parameter - nothing to write or log..' } } function Get-SBCredential { <# .SYNOPSIS Function to get AD credential, save encrypted password to file for future automation .DESCRIPTION Function to get AD credential, save encrypted password to file for future automation The function will use saved password if the password file exists The function will prompt for the password if the password file does not exist, or the -Refresh switch is used Note that the function does not validate whether the UserName exists in any directory, or that the password entered is valid. It merely creates a Credential object to be used securely for future automation, eleminating the need to type in the password everytime the function is needed, or the need to type in password in clear text in scripts. .PARAMETER UserName This can be in the format 'myusername' or 'domain\username' If not provided, the function assumes username under which the function is executed .PARAMETER Refresh This switch will force the function to prompt for the password and over-write the password file .OUTPUTS The function returns a PSCredential object that can be used with other cmdlets that use the -Credential parameter .EXAMPLE $MyCred = Get-SBCredential .EXAMPLE $Cred2 = Get-SBCredential -UserName 'sboutros' -Verbose -Refresh .EXAMPLE $Cred3 = 'domain2\ADSuperUser' | Get-SBCredential Disable-ADAccount -Identity 'Someone' -Server 'MyDomainController' -Credential $Cred3 This example obtains and saves credential of 'domain2\ADSuperUser' in $Cred3 varialble Second line uses that credential to disable an AD account of 'Someone' .NOTES Sam Boutros 5 August 2016 - v1.0 For more information see https://superwidgets.wordpress.com/2016/08/05/powershell-script-to-provide-a-ps-credential-object-saving-password-securely/ #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [String]$UserName = "$env:USERDOMAIN\$env:USERNAME", [Parameter(Mandatory=$false,Position=1)][Switch]$Refresh = $false ) $CredPath = "$env:Temp\$($UserName.Replace('\','_')).txt" if ($Refresh) { Remove-Item -Path $CredPath -Force -Confirm:$false -ErrorAction SilentlyContinue } if (!(Test-Path -Path $CredPath)) { Read-Host "Enter the pwd for $UserName" -AsSecureString | ConvertFrom-SecureString | Out-File $CredPath } $Pwd = Get-Content $CredPath | ConvertTo-SecureString New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $UserName, $Pwd } function Get-SBDisk { <# .SYNOPSIS Function to get disk information including block (allocation unit) size .DESCRIPTION Function to get disk information including block (allocation unit) size Function returns information on all fixed disks (Type 3) Function will fail to return computer disk information if: - Target computer is offline or name is misspelled - Function/script is run under an account with no read permission on the target computer - WMI services not running on the target computer - Target computer firewall or AntiVirus blocks WMI or RPC calls .PARAMETER ComputerName The name or IP address of computer(s) to collect disk information on Default value is local computer name .PARAMETER WMITimeOut Timeout in seconds. The default value is 20 .PARAMETER Cred PS Credential object .PARAMETER IncludeRecoveryVolume This parameter takes a $true or $false value, and is set to $false by default When set to $true the script will return information on Recovery Volume .EXAMPLE Get-SBDisk Returns fixed disk information of local computer .EXAMPLE Get-SBDisk computer1, 192.168.19.26, computer3 -Verbose Returns fixed disk information of the 3 listed computers The 'verbose' parameter will display a message if the target computer cannot be reached .OUTPUTS The script returns a PS Object with the following properties: ComputerName VolumeName DriveLetterOrMountPoint BlockSizeKB SizeGB FreeGB 'Free%' FileSystem Compressed .LINK https://superwidgets.wordpress.com/2017/01/09/powershell-script-to-get-disk-information-including-block-size/ .NOTES Function by Sam Boutros - v1.0 - 9 January 2017 v2.0 - 24 January 2017 Used WMI object Win32_Volume instead of Win32_LogicalDisk to capture mount points as well Added parameter to skip Recovery Volume Updated output object properties v3.0 - 12 July 2017 Updated output object to change data types to Int32 instead of the default String for BlockSizeKB,SizeGB,FreeGB,'Free%' v4.0 - 20 September 2017 - Used Get-SBWMI instead to take advanrage of the default 20 sec Timeout v4.1 - 22 September 2017 - Added WMITimeout parameter, removed -Filter parameter from Get-SBWMI call and filtered via updated if statement to speed processing by 200% #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$false, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)] [String[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory=$false)][Int32]$WMITimeOut = 20, [Parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Cred = (Get-SBCredential -UserName "$env:USERDOMAIN\$env:USERNAME"), [Parameter(Mandatory=$false)][Switch]$IncludeRecoveryVolume ) foreach ($Computer in $ComputerName) { try { Get-SBWMI -ComputerName $Computer -Class Win32_Volume -TimeOut $WMITimeOut -Cred $Cred -ErrorAction Stop | % { if ($_.DriveType -eq 3 -and ($_.Label-notlike'Recovery' -or $IncludeRecoveryVolume)) { [PSCustomObject][Ordered]@{ ComputerName = $Computer VolumeName = $_.Label DriveLetterOrMountPoint = $(if ($_.Name.Contains(':')) {$_.Name} else {'<Not mounted>'}) BlockSizeKB = [Int32]($_.Blocksize/1KB) SizeGB = [Math]::Round($_.Capacity/1GB,1) FreeGB = [Math]::Round($_.FreeSpace/1GB,1) 'Free%' = [Math]::Round($_.FreeSpace/$_.Capacity*100,1) FileSystem = $_.FileSystem Compressed = $_.Compressed Indexed = $_.IndexingEnabled Automount = $_.Automount QuotasEnabled = $_.QuotasEnabled PageFilePresent = $_.PageFilePresent BootVolume = $_.BootVolume SystemVolume = $_.SystemVolume } # PSCustomObject } # if } # Get-SBWMI } catch { Write-Verbose "Unable to read disk information from computer $Computer" } } } function ConvertTo-EnhancedHTML { <# .SYNOPSIS Provides an enhanced version of the ConvertTo-HTML command that includes inserting an embedded CSS style sheet, JQuery, and JQuery Data Tables for interactivity. Intended to be used with HTML fragments that are produced by ConvertTo-EnhancedHTMLFragment. This command does not accept pipeline input. .PARAMETER jQueryURI A Uniform Resource Indicator (URI) pointing to the location of the jQuery script file. You can download jQuery from www.jquery.com; you should host the script file on a local intranet Web server and provide a URI that starts with http:// or https://. Alternately, you can also provide a file system path to the script file, although this may create security issues for the Web browser in some configurations. Tested with v1.8.2. Defaults to http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.min.js, which will pull the file from Microsoft's ASP.NET Content Delivery Network. .PARAMETER jQueryDataTableURI A Uniform Resource Indicator (URI) pointing to the location of the jQuery Data Table script file. You can download this from www.datatables.net; you should host the script file on a local intranet Web server and provide a URI that starts with http:// or https://. Alternately, you can also provide a file system path to the script file, although this may create security issues for the Web browser in some configurations. Tested with jQuery DataTable v1.9.4 Defaults to http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.3/jquery.dataTables.min.js, which will pull the file from Microsoft's ASP.NET Content Delivery Network. .PARAMETER CssStyleSheet The CSS style sheet content - not a file name. If you have a CSS file, you can load it into this parameter as follows: -CSSStyleSheet (Get-Content MyCSSFile.css) Alternately, you may link to a Web server-hosted CSS file by using the -CssUri parameter. .PARAMETER CssUri A Uniform Resource Indicator (URI) to a Web server-hosted CSS file. Must start with either http:// or https://. If you omit this, you can still provide an embedded style sheet, which makes the resulting HTML page more standalone. To provide an embedded style sheet, use the -CSSStyleSheet parameter. .PARAMETER Title A plain-text title that will be displayed in the Web browser's window title bar. Note that not all browsers will display this. .PARAMETER PreContent Raw HTML to insert before all HTML fragments. Use this to specify a main title for the report: -PreContent "<H1>My HTML Report</H1>" .PARAMETER PostContent Raw HTML to insert after all HTML fragments. Use this to specify a report footer: -PostContent "Created on $(Get-Date)" .PARAMETER HTMLFragments One or more HTML fragments, as produced by ConvertTo-EnhancedHTMLFragment. -HTMLFragments $part1,$part2,$part3 .EXAMPLE The following is a complete example script showing how to use ConvertTo-EnhancedHTMLFragment and ConvertTo-EnhancedHTML. The example queries 6 pieces of information from the local computer and produces a report in C:\. This example uses most of the avaiable options. It relies on Internet connectivity to retrieve JavaScript from Microsoft's Content Delivery Network. This example uses an embedded stylesheet, which is defined as a here-string at the top of the script. $computername = 'localhost' $path = 'c:\' $style = @" <style> body { color:#333333; font-family:Calibri,Tahoma; font-size: 10pt; } h1 { text-align:center; } h2 { border-top:1px solid #666666; } th { font-weight:bold; color:#eeeeee; background-color:#333333; cursor:pointer; } .odd { background-color:#ffffff; } .even { background-color:#dddddd; } .paginate_enabled_next, .paginate_enabled_previous { cursor:pointer; border:1px solid #222222; background-color:#dddddd; padding:2px; margin:4px; border-radius:2px; } .paginate_disabled_previous, .paginate_disabled_next { color:#666666; cursor:pointer; background-color:#dddddd; padding:2px; margin:4px; border-radius:2px; } .dataTables_info { margin-bottom:4px; } .sectionheader { cursor:pointer; } .sectionheader:hover { color:red; } .grid { width:100% } .red { color:red; font-weight:bold; } </style> "@ function Get-InfoOS { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $os = Get-WmiObject -class Win32_OperatingSystem -ComputerName $ComputerName $props = @{'OSVersion'=$os.version; 'SPVersion'=$os.servicepackmajorversion; 'OSBuild'=$os.buildnumber} New-Object -TypeName PSObject -Property $props } function Get-InfoCompSystem { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $cs = Get-WmiObject -class Win32_ComputerSystem -ComputerName $ComputerName $props = @{'Model'=$cs.model; 'Manufacturer'=$cs.manufacturer; 'RAM (GB)'="{0:N2}" -f ($cs.totalphysicalmemory / 1GB); 'Sockets'=$cs.numberofprocessors; 'Cores'=$cs.numberoflogicalprocessors} New-Object -TypeName PSObject -Property $props } function Get-InfoBadService { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $svcs = Get-WmiObject -class Win32_Service -ComputerName $ComputerName ` -Filter "StartMode='Auto' AND State<>'Running'" foreach ($svc in $svcs) { $props = @{'ServiceName'=$svc.name; 'LogonAccount'=$svc.startname; 'DisplayName'=$svc.displayname} New-Object -TypeName PSObject -Property $props } } function Get-InfoProc { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $procs = Get-WmiObject -class Win32_Process -ComputerName $ComputerName foreach ($proc in $procs) { $props = @{'ProcName'=$proc.name; 'Executable'=$proc.ExecutablePath} New-Object -TypeName PSObject -Property $props } } function Get-InfoNIC { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $nics = Get-WmiObject -class Win32_NetworkAdapter -ComputerName $ComputerName ` -Filter "PhysicalAdapter=True" foreach ($nic in $nics) { $props = @{'NICName'=$nic.servicename; 'Speed'=$nic.speed / 1MB -as [int]; 'Manufacturer'=$nic.manufacturer; 'MACAddress'=$nic.macaddress} New-Object -TypeName PSObject -Property $props } } function Get-InfoDisk { [CmdletBinding()] param( [Parameter(Mandatory=$True)][string]$ComputerName ) $drives = Get-WmiObject -class Win32_LogicalDisk -ComputerName $ComputerName ` -Filter "DriveType=3" foreach ($drive in $drives) { $props = @{'Drive'=$drive.DeviceID; 'Size'=$drive.size / 1GB -as [int]; 'Free'="{0:N2}" -f ($drive.freespace / 1GB); 'FreePct'=$drive.freespace / $drive.size * 100 -as [int]} New-Object -TypeName PSObject -Property $props } } foreach ($computer in $computername) { try { $everything_ok = $true Write-Verbose "Checking connectivity to $computer" Get-WmiObject -class Win32_BIOS -ComputerName $Computer -EA Stop | Out-Null } catch { Write-Warning "$computer failed" $everything_ok = $false } if ($everything_ok) { $filepath = Join-Path -Path $Path -ChildPath "$computer.html" $params = @{'As'='List'; 'PreContent'='<h2>OS</h2>'} $html_os = Get-InfoOS -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='List'; 'PreContent'='<h2>Computer System</h2>'} $html_cs = Get-InfoCompSystem -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='Table'; 'PreContent'='<h2>♦ Local Disks</h2>'; 'EvenRowCssClass'='even'; 'OddRowCssClass'='odd'; 'MakeTableDynamic'=$true; 'TableCssClass'='grid'; 'Properties'='Drive', @{n='Size(GB)';e={$_.Size}}, @{n='Free(GB)';e={$_.Free};css={if ($_.FreePct -lt 80) { 'red' }}}, @{n='Free(%)';e={$_.FreePct};css={if ($_.FreeePct -lt 80) { 'red' }}}} $html_dr = Get-InfoDisk -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='Table'; 'PreContent'='<h2>♦ Processes</h2>'; 'MakeTableDynamic'=$true; 'TableCssClass'='grid'} $html_pr = Get-InfoProc -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='Table'; 'PreContent'='<h2>♦ Services to Check</h2>'; 'EvenRowCssClass'='even'; 'OddRowCssClass'='odd'; 'MakeHiddenSection'=$true; 'TableCssClass'='grid'} $html_sv = Get-InfoBadService -ComputerName $computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'As'='Table'; 'PreContent'='<h2>♦ NICs</h2>'; 'EvenRowCssClass'='even'; 'OddRowCssClass'='odd'; 'MakeHiddenSection'=$true; 'TableCssClass'='grid'} $html_na = Get-InfoNIC -ComputerName $Computer | ConvertTo-EnhancedHTMLFragment @params $params = @{'CssStyleSheet'=$style; 'Title'="System Report for $computer"; 'PreContent'="<h1>System Report for $computer</h1>"; 'HTMLFragments'=@($html_os,$html_cs,$html_dr,$html_pr,$html_sv,$html_na)} ConvertTo-EnhancedHTML @params | Out-File -FilePath $filepath } } .Notes Function by Don Jones Generated on: 9/10/2013 For more information see Powershell.org included in AZSBTools module with permission by Don Jones #> [CmdletBinding()] param( [string]$jQueryURI = 'http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.8.2.min.js', [string]$jQueryDataTableURI = 'http://ajax.aspnetcdn.com/ajax/jquery.dataTables/1.9.3/jquery.dataTables.min.js', [Parameter(ParameterSetName='CSSContent')][string[]]$CssStyleSheet, [Parameter(ParameterSetName='CSSURI')][string[]]$CssUri, [string]$Title = 'Report', [string]$PreContent, [string]$PostContent, [Parameter(Mandatory=$True)][string[]]$HTMLFragments ) <# Add CSS style sheet. If provided in -CssUri, add a <link> element. If provided in -CssStyleSheet, embed in the <head> section. Note that BOTH may be supplied - this is legitimate in HTML. #> Write-Verbose "Making CSS style sheet" $stylesheet = "" if ($PSBoundParameters.ContainsKey('CssUri')) { $stylesheet = "<link rel=`"stylesheet`" href=`"$CssUri`" type=`"text/css`" />" } if ($PSBoundParameters.ContainsKey('CssStyleSheet')) { $stylesheet = "<style>$CssStyleSheet</style>" | Out-String } <# Create the HTML tags for the page title, and for our main javascripts. #> Write-Verbose "Creating <TITLE> and <SCRIPT> tags" $titletag = "" if ($PSBoundParameters.ContainsKey('title')) { $titletag = "<title>$title</title>" } $script += "<script type=`"text/javascript`" src=`"$jQueryURI`"></script>`n<script type=`"text/javascript`" src=`"$jQueryDataTableURI`"></script>" <# Render supplied HTML fragments as one giant string #> Write-Verbose "Combining HTML fragments" $body = $HTMLFragments | Out-String <# If supplied, add pre- and post-content strings #> Write-Verbose "Adding Pre and Post content" if ($PSBoundParameters.ContainsKey('precontent')) { $body = "$PreContent`n$body" } if ($PSBoundParameters.ContainsKey('postcontent')) { $body = "$body`n$PostContent" } <# Add a final script that calls the datatable code We dynamic-ize all tables with the .enhancedhtml-dynamic-table class, which is added by ConvertTo-EnhancedHTMLFragment. #> Write-Verbose "Adding interactivity calls" $datatable = "" $datatable = "<script type=`"text/javascript`">" $datatable += '$(document).ready(function () {' $datatable += "`$('.enhancedhtml-dynamic-table').dataTable();" $datatable += '} );' $datatable += "</script>" <# Datatables expect a <thead> section containing the table header row; ConvertTo-HTML doesn't produce that so we have to fix it. #> Write-Verbose "Fixing table HTML" $body = $body -replace '<tr><th>','<thead><tr><th>' $body = $body -replace '</th></tr>','</th></tr></thead>' <# Produce the final HTML. We've more or less hand-made the <head> amd <body> sections, but we let ConvertTo-HTML produce the other bits of the page. #> Write-Verbose "Producing final HTML" ConvertTo-HTML -Head "$stylesheet`n$titletag`n$script`n$datatable" -Body $body Write-Debug "Finished producing final HTML" } function ConvertTo-EnhancedHTMLFragment { <# .SYNOPSIS Creates an HTML fragment (much like ConvertTo-HTML with the -Fragment switch that includes CSS class names for table rows, CSS class and ID names for the table, and wraps the table in a <DIV> tag that has a CSS class and ID name. .PARAMETER InputObject The object to be converted to HTML. You cannot select properties using this command; precede this command with Select-Object if you need a subset of the objects' properties. .PARAMETER EvenRowCssClass The CSS class name applied to even-numbered <TR> tags. Optional, but if you use it you must also include -OddRowCssClass. .PARAMETER OddRowCssClass The CSS class name applied to odd-numbered <TR> tags. Optional, but if you use it you must also include -EvenRowCssClass. .PARAMETER TableCssID Optional. The CSS ID name applied to the <TABLE> tag. .PARAMETER DivCssID Optional. The CSS ID name applied to the <DIV> tag which is wrapped around the table. .PARAMETER TableCssClass Optional. The CSS class name to apply to the <TABLE> tag. .PARAMETER DivCssClass Optional. The CSS class name to apply to the wrapping <DIV> tag. .PARAMETER As Must be 'List' or 'Table.' Defaults to Table. Actually produces an HTML table either way; with Table the output is a grid-like display. With List the output is a two-column table with properties in the left column and values in the right column. .PARAMETER Properties A comma-separated list of properties to include in the HTML fragment. This can be * (which is the default) to include all properties of the piped-in object(s). In addition to property names, you can also use a hashtable similar to that used with Select-Object. For example: Get-Process | ConvertTo-EnhancedHTMLFragment -As Table ` -Properties Name,ID,@{n='VM'; e={$_.VM}; css={if ($_.VM -gt 100) { 'red' } else { 'green' }}} This will create table cell rows with the calculated CSS class names. E.g., for a process with a VM greater than 100, you'd get: <TD class="red">475858</TD> You can use this feature to specify a CSS class for each table cell based upon the contents of that cell. Valid keys in the hashtable are: n, name, l, or label: The table column header e or expression: The table cell contents css or csslcass: The CSS class name to apply to the <TD> tag Another example: @{n='Free(MB)'; e={$_.FreeSpace / 1MB -as [int]}; css={ if ($_.FreeSpace -lt 100) { 'red' } else { 'blue' }} This example creates a column titled "Free(MB)". It will contain the input object's FreeSpace property, divided by 1MB and cast as a whole number (integer). If the value is less than 100, the table cell will be given the CSS class "red." If not, the table cell will be given the CSS class "blue." The supplied cascading style sheet must define ".red" and ".blue" for those to have any effect. .PARAMETER PreContent Raw HTML content to be placed before the wrapping <DIV> tag. For example: -PreContent "<h2>Section A</h2>" .PARAMETER PostContent Raw HTML content to be placed after the wrapping <DIV> tag. For example: -PostContent "<hr />" .PARAMETER MakeHiddenSection Used in conjunction with -PreContent. Adding this switch, which needs no value, turns your -PreContent into clickable report section header. The section will be hidden by default, and clicking the header will toggle its visibility. When using this parameter, consider adding a symbol to your -PreContent that helps indicate this is an expandable section. For example: -PreContent '<h2>♦ My Section</h2>' If you use -MakeHiddenSection, you MUST provide -PreContent also, or the hidden section will not have a section header and will not be visible. .PARAMETER MakeTableDynamic When using "-As Table", makes the table dynamic. Will be ignored if you use "-As List". Dynamic tables are sortable, searchable, and are paginated. You should not use even/odd styling with tables that are made dynamic. Dynamic tables automatically have their own even/odd styling. You can apply CSS classes named ".odd" and ".even" in your CSS to style the even/odd in a dynamic table. .EXAMPLE $fragment = Get-WmiObject -Class Win32_LogicalDisk | Select-Object -Property PSComputerName,DeviceID,FreeSpace,Size | ConvertTo-HTMLFragment -EvenRowClass 'even' ` -OddRowClass 'odd' ` -PreContent '<h2>Disk Report</h2>' ` -MakeHiddenSection ` -MakeTableDynamic You will usually save fragments to a variable, so that multiple fragments (each in its own variable) can be passed to ConvertTo-EnhancedHTML. .NOTES Consider adding the following to your CSS when using dynamic tables: .paginate_enabled_next, .paginate_enabled_previous { cursor:pointer; border:1px solid #222222; background-color:#dddddd; padding:2px; margin:4px; border-radius:2px; } .paginate_disabled_previous, .paginate_disabled_next { color:#666666; cursor:pointer; background-color:#dddddd; padding:2px; margin:4px; border-radius:2px; } .dataTables_info { margin-bottom:4px; } This applies appropriate coloring to the next/previous buttons, and applies a small amount of space after the dynamic table. If you choose to make sections hidden (meaning they can be shown and hidden by clicking on the section header), consider adding the following to your CSS: .sectionheader { cursor:pointer; } .sectionheader:hover { color:red; } This will apply a hover-over color, and change the cursor icon, to help visually indicate that the section can be toggled. .Notes Function by Don Jones Generated on: 9/10/2013 For more information see Powershell.org included in AZSBTools module with permission by Don Jones #> [CmdletBinding()] param( [Parameter(Mandatory=$True,ValueFromPipeline=$True)] [object[]]$InputObject, [string]$EvenRowCssClass, [string]$OddRowCssClass, [string]$TableCssID, [string]$DivCssID, [string]$DivCssClass, [string]$TableCssClass, [ValidateSet('List','Table')] [string]$As = 'Table', [object[]]$Properties = '*', [string]$PreContent, [switch]$MakeHiddenSection, [switch]$MakeTableDynamic, [string]$PostContent ) BEGIN { <# Accumulate output in a variable so that we don't produce an array of strings to the pipeline, but instead produce a single string. #> $out = '' <# Add the section header (pre-content). If asked to make this section of the report hidden, set the appropriate code on the section header to toggle the underlying table. Note that we generate a GUID to use as an additional ID on the <div>, so that we can uniquely refer to it without relying on the user supplying us with a unique ID. #> Write-Verbose "Precontent" if ($PSBoundParameters.ContainsKey('PreContent')) { if ($PSBoundParameters.ContainsKey('MakeHiddenSection')) { [string]$tempid = [System.Guid]::NewGuid() $out += "<span class=`"sectionheader`" onclick=`"`$('#$tempid').toggle(500);`">$PreContent</span>`n" } else { $out += $PreContent $tempid = '' } } <# The table will be wrapped in a <div> tag for styling purposes. Note that THIS, not the table per se, is what we hide for -MakeHiddenSection. So we will hide the section if asked to do so. #> Write-Verbose "DIV" if ($PSBoundParameters.ContainsKey('DivCSSClass')) { $temp = " class=`"$DivCSSClass`"" } else { $temp = "" } if ($PSBoundParameters.ContainsKey('MakeHiddenSection')) { $temp += " id=`"$tempid`" style=`"display:none;`"" } else { $tempid = '' } if ($PSBoundParameters.ContainsKey('DivCSSID')) { $temp += " id=`"$DivCSSID`"" } $out += "<div $temp>" <# Create the table header. If asked to make the table dynamic, we add the CSS style that ConvertTo-EnhancedHTML will look for to dynamic-ize tables. #> Write-Verbose "TABLE" $_TableCssClass = '' if ($PSBoundParameters.ContainsKey('MakeTableDynamic') -and $As -eq 'Table') { $_TableCssClass += 'enhancedhtml-dynamic-table ' } if ($PSBoundParameters.ContainsKey('TableCssClass')) { $_TableCssClass += $TableCssClass } if ($_TableCssClass -ne '') { $css = "class=`"$_TableCSSClass`"" } else { $css = "" } if ($PSBoundParameters.ContainsKey('TableCSSID')) { $css += "id=`"$TableCSSID`"" } else { if ($tempid -ne '') { $css += "id=`"$tempid`"" } } $out += "<table $css>" <# We're now setting up to run through our input objects and create the table rows #> $fragment = '' $wrote_first_line = $false $even_row = $false if ($properties -eq '*') { $all_properties = $true } else { $all_properties = $false } } PROCESS { foreach ($object in $inputobject) { Write-Verbose "Processing object" $datarow = '' $headerrow = '' <# Apply even/odd row class. Note that this will mess up the output if the table is made dynamic. That's noted in the help. #> if ($PSBoundParameters.ContainsKey('EvenRowCSSClass') -and $PSBoundParameters.ContainsKey('OddRowCssClass')) { if ($even_row) { $row_css = $OddRowCSSClass $even_row = $false Write-Verbose "Even row" } else { $row_css = $EvenRowCSSClass $even_row = $true Write-Verbose "Odd row" } } else { $row_css = '' Write-Verbose "No row CSS class" } <# If asked to include all object properties, get them. #> if ($all_properties) { $properties = $object | Get-Member -MemberType Properties | Select -ExpandProperty Name } <# We either have a list of all properties, or a hashtable of properties to play with. Process the list. #> foreach ($prop in $properties) { Write-Verbose "Processing property" $name = $null $value = $null $cell_css = '' <# $prop is a simple string if we are doing "all properties," otherwise it is a hashtable. If it's a string, then we can easily get the name (it's the string) and the value. #> if ($prop -is [string]) { Write-Verbose "Property $prop" $name = $Prop $value = $object.($prop) } elseif ($prop -is [hashtable]) { Write-Verbose "Property hashtable" <# For key "css" or "cssclass," execute the supplied script block. It's expected to output a class name; we embed that in the "class" attribute later. #> if ($prop.ContainsKey('cssclass')) { $cell_css = $Object | ForEach $prop['cssclass'] } if ($prop.ContainsKey('css')) { $cell_css = $Object | ForEach $prop['css'] } <# Get the current property name. #> if ($prop.ContainsKey('n')) { $name = $prop['n'] } if ($prop.ContainsKey('name')) { $name = $prop['name'] } if ($prop.ContainsKey('label')) { $name = $prop['label'] } if ($prop.ContainsKey('l')) { $name = $prop['l'] } <# Execute the "expression" or "e" key to get the value of the property. #> if ($prop.ContainsKey('e')) { $value = $Object | ForEach $prop['e'] } if ($prop.ContainsKey('expression')) { $value = $tObject | ForEach $prop['expression'] } <# Make sure we have a name and a value at this point. #> if ($name -eq $null -or $value -eq $null) { Write-Error "Hashtable missing Name and/or Expression key" } } else { <# We got a property list that wasn't strings and wasn't hashtables. Bad input. #> Write-Warning "Unhandled property $prop" } <# When constructing a table, we have to remember the property names so that we can build the table header. In a list, it's easier - we output the property name and the value at the same time, since they both live on the same row of the output. #> if ($As -eq 'table') { Write-Verbose "Adding $name to header and $value to row" $headerrow += "<th>$name</th>" $datarow += "<td$(if ($cell_css -ne '') { ' class="'+$cell_css+'"' })>$value</td>" } else { $wrote_first_line = $true $headerrow = "" $datarow = "<td$(if ($cell_css -ne '') { ' class="'+$cell_css+'"' })>$name :</td><td$(if ($cell_css -ne '') { ' class="'+$cell_css+'"' })>$value</td>" $out += "<tr$(if ($row_css -ne '') { ' class="'+$row_css+'"' })>$datarow</tr>" } } <# Write the table header, if we're doing a table. #> if (-not $wrote_first_line -and $as -eq 'Table') { Write-Verbose "Writing header row" $out += "<tr>$headerrow</tr><tbody>" $wrote_first_line = $true } <# In table mode, write the data row. #> if ($as -eq 'table') { Write-Verbose "Writing data row" $out += "<tr$(if ($row_css -ne '') { ' class="'+$row_css+'"' })>$datarow</tr>" } } } END { <# Finally, post-content code, the end of the table, the end of the <div>, and write the final string. #> Write-Verbose "PostContent" if ($PSBoundParameters.ContainsKey('PostContent')) { $out += "`n$PostContent" } Write-Verbose "Done" $out += "</tbody></table></div>" Write-Output $out } } Function Get-SBWMI { <# .SYNOPSIS Function query WMI with Timeout .DESCRIPTION Function query WMI with Timeout .PARAMETER Class Class name such as 'Win32_computerSystem' .PARAMETER Property Property name such as 'NumberofLogicalProcessors' .PARAMETER Filter In the format Property=Value such as DriveLetter=G: .PARAMETER ComputerName Computer name .PARAMETER NameSpace Default is 'root\cimv2' To see name spaces type: (Get-WmiObject -Namespace 'root' -Class '__Namespace').Name .PARAMETER Cred PS Credential object .PARAMETER TimeOut In seconds .EXAMPLE Get-SBWMI -Class Win32_computerSystem -Property NumberofLogicalProcessors .EXAMPLE Get-SBWMI -Class Win32_Volume -Filter 'DriveType=3' .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 20 September 2017 v0.2 - 29 September 2017 - Added parameter to use a different credential other than the one running the script Added error checking for failure to WMI connect #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true, ValueFromPipeLine=$true, ValueFromPipeLineByPropertyName=$true, Position=0)][string]$Class, [Parameter(Mandatory=$false)][String[]]$Property = '*', [Parameter(Mandatory=$false)][String]$Filter, [Parameter(Mandatory=$false)][String]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory=$false)][String]$NameSpace = 'root\cimv2', [Parameter(Mandatory=$false)][System.Management.Automation.PSCredential]$Cred = (Get-SBCredential -UserName "$env:USERDOMAIN\$env:USERNAME"), [Parameter(Mandatory=$false)][int]$TimeOut=20 ) Begin { if ($Filter) { if ($Filter -match '=') { $FilterProperty = $Filter.Split('=')[0].Trim() $FilterValue = $Filter.Split('=')[1].Trim() } else { Write-Log 'Get-SBWMI Input Error:','Filter',', supported syntax is','Property=Value','such as','DriveLetter=G' Magenta,Yellow,Magenta,Yellow,Magenta,Yellow Write-Log ' ignoring filter',$Filter Magenta,Yellow } } } Process{ $ConnOpt = New-Object System.Management.ConnectionOptions if ($ComputerName -ne $env:COMPUTERNAME) { # User credentials cannot be used for local connections $ConnOpt.EnablePrivileges = $true $ConnOpt.Username = $Cred.UserName $ConnOpt.SecurePassword = $Cred.Password } $Scope = New-Object System.Management.ManagementScope “\\$ComputerName\$NameSpace", $ConnOpt try { $Scope.Connect() } catch { $Message = $_.Exception.InnerException } if ($Scope.IsConnected) { $EnumOptions = New-Object System.Management.EnumerationOptions $EnumOptions.set_timeout((New-TimeSpan -seconds $TimeOut)) $Search = New-Object System.Management.ManagementObjectSearcher $Search.set_options($EnumOptions) $Search.Query = “SELECT $Property FROM $Class” $Search.Scope = $Scope $Result = $Search.get() } else { Write-Warning "Get-SBWMI: Error: $(($Message|Out-String).Trim())" } } End { if ($Result){ if ($Filter) { if ($FilterProperty -in ($Result | Get-Member -MemberType Property).Name) { $Result | where { $_.$FilterProperty -eq $FilterValue } } else { Write-Log 'Class',$Class,'doesn''t contain filter property',$FilterProperty Magenta,Yellow,Magenta,Yellow Write-Log 'Class',$Class,'has the following properties:' Cyan,Yellow,Cyan Write-Log (($Result | Get-Member -MemberType Property).Name | ? { $_ -notmatch '__' } | Out-String).Trim() Cyan } } else { $Result } } } } function Test-SBNetConnection { <# .SYNOPSIS Function to test open TCP ports .DESCRIPTION Function to test open TCP ports Compared to the Test-NetConnection native function of the NetTCPIP module, this command is much faster particularly when it comes across closed ports. In addition, the timeout value is adjustable by using the TimeoutSec parameter. .PARAMETER ComputerName This parameter accepts a computer name or IPv4 Address. If a computer name is provided, the function attempts to resolve it to an IP address .PARAMETER PortNumber This is one or more TCP port number(s) with valid values from 1 to 65535 It defaults to 111,135,22,3389,25,80,5985,5986 Ports 111,135 help identify the system as a Linux or Windows system respectively Ports 22,3389 are Linux/SSH and Windows/RDP ports Ports 25,80 are SMTP and HTTP ports Ports 5895,5986 are PowerShell/WinRM ports over HTTP and HTTPS respectively .PARAMETER TimeoutSec Time out in seconds This defaults to 1, and accepts valid values from 1 to 300 seconds. .OUTPUTS The script outputs a PS array of objects, one for each open port including the following properties/example: ComputerName RemotePort TcpTestSucceeded ------------ ---------- ---------------- 10.127.73.195 53 True 10.127.73.195 135 True 10.127.73.195 389 True 10.127.73.195 443 False 10.127.73.195 5723 False 10.127.73.195 5985 True 10.127.73.195 5986 True .EXAMPLE Test-SBNetConnection -ComputerName 10.127.73.195 .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 18 October 2017 v0.2 - 5 January 2018 - Fixed bug to account for computers that resolve to more than 1 IP #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$ComputerName, [Parameter(Mandatory=$false)][uInt16[]]$PortNumber = @(111,135,22,3389,25,80,5985,5986), [Parameter(Mandatory=$false)][ValidateRange(1,300)][Int16]$TimeoutSec = 1 ) Begin { } Process{ if (!($IPv4Address = $Computername -as 'IPAddress')) { try { [IPAddress[]]$IPv4Address = (Resolve-DnsName -Name $ComputerName -EA 1).IPAddress } catch { Write-Warning "Unable to resolve computer name '$ComputerName'" } } if ($IPv4Address) { foreach ($IP in $IPv4Address.IPAddressToString) { foreach ($Item in $PortNumber) { $TCP = New-Object System.Net.Sockets.TcpClient $AsyncResult = $TCP.BeginConnect("$IP","$Item",$null,$null) $PortOpen = $false if ($AsyncResult.AsyncWaitHandle.WaitOne($TimeoutSec*1000,$false)) { try { $TCP.EndConnect($AsyncResult) $PortOpen = $true } catch { Write-Warning $_.Exception.InnerException } } else { Write-Warning "TCP connect to $($IP):$Item timed out ($TimeoutSec sec)" } # if $AsyncResult $TCP.Close() [PSCustomObject][Ordered]@{ ComputerName = $IP RemotePort = $Item TcpTestSucceeded = $PortOpen } # PSCustomObject } # foreach port } # foreach IP } # if $IPv4Address } # Process End { } } function Tag-AzureVM { #Requires -Modules AzureRM #Requires -Version 5 <# .SYNOPSIS Function to apply one or more Azure tags to one or more VM and its related objects .DESCRIPTION Function to apply one or more Azure tags to one or more VM and its related objects including its: NICs Disks Extensions .PARAMETER LoginName The username required to authenticate to Azure Example: samb@mydomain.com .PARAMETER SubscriptionName The Azure subscription name such as 'My Dev EA subscription' .PARAMETER ResourceGroupName The name of the Azure Resource Group where the VM(s) reside .PARAMETER VMName The name(s) or one or more VMs .PARAMETER TagList One or more Azure tags to be applied. This is a hash table that takes key/value pairs in the format: @{ COMPANY = 'MyCompany' OWNER = 'Sam.Boutros' } .EXAMPLE $Splatt = @{ LoginName = 'sam.boutros@mydomain.com' SubscriptionName = 'My Enterprise subscription' ResourceGroupName = 'My RG1' VMName = @( 'VM01' 'VM02' ) TagList = @{ COMPANY = 'my company' OWNER = 'Sam.Boutros' } } Tag-AzureVM @Splatt .OUTPUTS None .LINK https://superwidgets.wordpress.com/category/powershell/ .NOTES Function by Sam Boutros v0.1 - 4 June 2018 - Initial release v0.2 - 14 June 2018 - Parameterized, added error handling and documentation #> [CmdletBinding(ConfirmImpact='Low')] Param( [Parameter(Mandatory=$true)][String]$LoginName, [Parameter(Mandatory=$true)][String]$SubscriptionName, [Parameter(Mandatory=$true)][String]$ResourceGroupName, [Parameter(Mandatory=$true)][String[]]$VMName, [Parameter(Mandatory=$false)][HashTable]$TagList = @{ COMPANY = 'MyCompany' OWNER = 'Sam.Boutros' } ) Begin { Login-AzureRmAccount -Credential (Get-SBCredential $LoginName) | Out-Null # -Environment AzureCloud try { Get-AzureRmSubscription -SubscriptionName $SubscriptionName -WA 0 -EA 1 | Set-AzureRmContext | Out-Null Write-Log 'Connected to Azure subscription',$SubscriptionName,'as',$LoginName Green,Cyan,Green,Cyan } catch { Write-Log $PSItem.Exception.Message Magenta break } } Process { foreach ($VM in $VMName) { Write-Log 'Processing VM',$VM Green,Cyan try { $VMObj = Get-AzureRmVM -Name $VM -ResourceGroupName $ResourceGroupName -WA 0 -EA 1 Write-Log 'Found VM',$VM,'in resource group',$ResourceGroupName Green,Cyan,Green,Cyan } catch { Write-Log 'VM',$VM,'not found in resource group',$ResourceGroupName Magenta,Yellow,Magenta,Yellow break } if ($VMObj) { $ObjList = @() $ObjList += $VMObj # Get VM NIC Objects foreach ($NicId in $VMObj.NetworkInterfaceIDs) { $NICObj = Get-AzureRmResource -ResourceId $NicId $ObjList += $NICObj Write-Log ' Identified VM NIC ',$NICObj.Name Green,Cyan } # Get VM Disk Objects foreach ($DiskName in $VMObj.DataDiskNames) { $DiskObj = Get-AzureRmDisk -ResourceGroupName $ResourceGroupName -DiskName $DiskName $ObjList += $DiskObj Write-Log ' Identified VM disk',$DiskObj.Name Green,Cyan } # Get VM Extension Objects foreach ($VMExtension in $VMObj.Extensions) { $VMExtensionObj = Get-AzureRmResource -ResourceId $VMExtension.Id $ObjList += $VMExtensionObj Write-Log ' Identified VM extension',$VMExtensionObj.Name Green,Cyan } } # Get VM related objects foreach ($Resource in $ObjList) { # The Microsoft team is being inconsistent the way they make these objects # For example, the resource Id is called Id on some objects but called ResourceId on others if (! $Resource.ResourceId) { $Resource | Add-Member -MemberType NoteProperty -Name ResourceId -Value $Resource.Id } if (! $Resource.ResourceType) { $Resource | Add-Member -MemberType NoteProperty -Name ResourceType -Value $Resource.Type } Write-Log 'Processing resource',$Resource.Name,"($($Resource.ResourceType))" Green,Cyan,Green if ($ResourceTagList = (Get-AzureRmResource -ResourceId $Resource.ResourceId).Tags) { $OK2Save = $false foreach ($key in $TagList.Keys) { if (-not($ResourceTagList.ContainsKey($key))) { Write-Log ' Tag',$key,'is not set for resource',$Resource.Name,'setting..' Green,Cyan,Yellow,Cyan,Green $ResourceTagList.Add($key, $TagList[$key]) $OK2Save = $true } elseif ($Resource.Tags[$key] -eq $TagList[$key]) { Write-Log ' Tag',$key,'is already set for resource',$Resource.Name,'value:',$Resource.Tags[$key],'skipping..' Green,Cyan,Green,Cyan,Green,Cyan,Green } else { Write-Log ' Tag',$key,'is already set for resource',$Resource.Name,'value:',$Resource.Tags[$key],'updating..' Green,Cyan,Green,Cyan,Green,Yellow,Green $Resource.Tags.$key = $TagList.$key # not working for some reason !? $OK2Save = $true } } if ($OK2Save) { Write-Log 'saving tags to resource..' DarkYellow -NoNewLine try { Set-AzureRmResource -Tag $ResourceTagList -ResourceId $Resource.ResourceId -Force -EA 1 | Out-Null } catch { Write-Log 'failed' Magenta Write-Log $PSItem.Exception.Message Yellow } } } else { Write-Log ' No tags configured for resource',$Resource.Name,'adding tags',($TagList.Keys -join ',') Green,Cyan,Green,Cyan try { Set-AzureRmResource -Tag $TagList -ResourceId $Resource.ResourceId -Force -EA 1 | Out-Null } catch { Write-Log 'failed' Magenta Write-Log $PSItem.Exception.Message Yellow } } } } } End {} } Export-ModuleMember -Function * -Variable * |