Export-CSV showing System.Collections.Generic.Dictionary`2[System.String,System.String] for Azure Tags? - azure

My script using Get-AzDisk https://learn.microsoft.com/en-us/powershell/module/az.compute/get-azdisk?view=azps-5.9.0 like below
Get-AzDisk -ResourceGroupName $ResourceGroupName|Where-Object{$_.ManagedBy.Length -lt 1} | Select-Object Name, OsType, DiskSizeGB, ResourceGroupName, Location, Tags, Tier, ProvisioningState, TimeCreated | ogv
Cannot export the actual Tags values like in the Out-GridView does:
This is the .CSV column for Tags:
How to modify it so it can show the same data as in the OGV?

You could use a calculated property to format the object (which is a hashtable) to the desired output. Here is an example that uses an calculated property to serialize the tags as JSON:
Get-AzDisk -ResourceGroupName $ResourceGroupName
| Where-Object { $_.ManagedBy.Length -lt 1 }
| Select-Object Name, OsType, DiskSizeGB, ResourceGroupName, Location, #{e = {$_.Tags | ConvertTo-Json | Out-String}; l='Tags'} , Tier, ProvisioningState, TimeCreated
And here the corresponding output as CSV (after I pipe it to ConvertTo-CSV cmdlet):

Tags are a Hash.
$myTags = $vm.Tags
if($myTags) {
foreach ($key in $myTags.Keys) {
$curVal = $myTags[$key]
}
}
You can grab all the Keys and Values and write them to a string in the column.
Be aware the $curVal could itself be a multi-value system.collections.generic.dictionary and might require further manipulation.

Related

Optimize PowerShell code to avoid calling the cmdlet multiple times inside calculated properties?

I am looking to optimize the code below to avoid calling the same command twice under the calculated properties.
https://learn.microsoft.com/en-us/powershell/module/az.compute/get-azvm
https://learn.microsoft.com/en-us/powershell/module/az.compute/get-azvmsize
Get-AzVM | Select-Object-Object Name,
#{ l = 'osdiskingb'; e = { ($_.StorageProfile.OsDisk.DiskSizeGB) } }, `
#{ l = 'memory'; e = { $size = $_.HardwareProfile.VmSize; Get-AzVMSize -vmname $_.Name -ResourceGroupName $_.ResourceGroupName | Where-Object { $_.name -eq $size } | Select-Object -expand MemoryInMB } }, `
#{ l = 'cpu'; e = { $size = $_.HardwareProfile.VmSize; Get-AzVMSize -vmname $_.Name -ResourceGroupName $_.ResourceGroupName | Where-Object { $_.name -eq $size } | Select-Object -expand NumberOfCores } }, `
#{ l = 'nic'; e = { $_.NetworkProfile.NetworkInterfaces.id.split('/') | Select-Object -Last 1 } }, `
#{ l = 'ip'; e = { $nic = $_.NetworkProfile.NetworkInterfaces.id.split('/') | Select-Object -Last 1; Get-AzNetworkInterface -Name $nic | Select-Object -expand ipconfigurations | Select-Object -expand privateipaddress } }
The script above works for pulling various different Azure VMs.
What can I try next?
Note:
This answer addresses the question as asked, in the context of Select-Object and calculated properties.
For a ForEach-Object-based alternative that uses explicit construction of [pscustomobject] instances, see zett42's helpful answer.
While the script blocks of calculated properties are executed in sequence, for each input object, they each run in their own child scope relative to the caller, which complicates sharing state between them.
However, you can simply create a variable whose value you want to share in the parent scope, which in the simplest case inside a script is the $script: scope, as the following simplified example shows (which uses a call to Get-Date in lieu of a call to Azure cmdlet as an example of a call you do not want to repeat):
# Share the result of the `Get-Date` call between calculated properties.
'foo' | Select-Object `
#{ n='Month'; e = { $script:dt = Get-Date; $dt.Month } },
#{ n='Year'; e = { $dt.Year } }
Output:
Month Year
----- ----
8 2022
This proves that the $script:-scoped $dt variable was successfully used in the second calculated property.
If you want to reliably target the parent scope, which may differ from the $script: scope if you're running inside a nested function call, for instance, replace $script:dt = Get-Date with Set-Variable -Scope 1 dt (Get-Date)
Note:
That script blocks of calculated properties as well as delay-bind script blocks run in a child scope may be surprising, given that it contrasts with the behavior of script blocks passed to ForEach-Object and Where-Object, for instance - for a discussion, see GitHub issue #7157.
This might not exactly answer your original question, but you might consider dropping calculated properties when the code becomes too complicated. Instead, use a [pscustomobject]#{…} literal in a ForEach-Object script block. This way you can move common code out of the properties to the begin of the script block.
Get-AzVM | ForEach-Object {
$size = $_.HardwareProfile.VmSize
$vmsize = Get-AzVMSize -vmname $_.Name -ResourceGroupName $_.ResourceGroupName | Where-Object { $_.name -eq $size }
$nic = $_.NetworkProfile.NetworkInterfaces.id.split('/') | Select-Object -Last 1
# Implicitly outputs an object with the given properties
[pscustomobject]#{
Name = $_.Name
osdiskingb = $_.StorageProfile.OsDisk.DiskSizeGB
memory = $vmsize.MemoryInMB
cpu = $vmsize.NumberOfCores
nic = $nic
ip = (Get-AzNetworkInterface -Name $nic).ipconfigurations.privateipaddress
}
}
On a side note, SomeCommand | Select-Object -Expand PropertyName isn't very efficient and can be replaced by member access, as I did for the ip property. The key is to enclose the command in parentheses.

Powershell odd behaviour when outputting to csv

I'm having a problem when outputting my foreach loop to a csv file.
My Groups are set like this:
$Groups = "Group1", "Group2", "Group3"
My code is:
$results = ForEach ($Group in $Groups) {
$memberof = get-adgroup $Group | select -expandproperty distinguishedname
Write-Output $Group
Get-ADObject -Filter 'memberof -eq $memberof -and (ObjectClass -eq "user" -or ObjectClass -eq "contact")' -properties * | select name, Objectclass, mail
Write-Output ""
Write-Output ""
}
$results | Export-csv Contacts.csv -NoTypeInformation
The problem seems to be coming from the Write-Output lines but I have no clue why. When I run my code without writing to a csv file, I get the expected result, something like:
NameOfGroup1
name Objectclass mail
---- ----------- ----
User1 user User1#mail.com
User2 user User2#mail.com
#Spaces caused by write-output ""
NameOfGroup2
User1 user User1#mail.com
Contact1 contact Contact1#externalmail.com
Then again when I run my code to write to csv file and have the write-output $Group commented out I get a similar result.
But if I run my full code from the top of this page including the write-output $Group, it comes out like this:
I've figured out what these results represent but I haven't got clue why they do print out like this.
Eseentially the numbers refer to the length of the group name, so the first 17 would be a 17 character group name, and then the number of lines below is equal to the number of contacts and users that are inside that group. The 2 zeros at the end of each group are the length of the write-output "" lines.
What is causing this behavior?
The following code will closely output what you are attempting.
$results = ForEach ($Group in $Groups) {
$memberof = get-adgroup $Group | select -expandproperty distinguishedname
Get-ADUser -Filter "memberof -eq '$memberof' -and (ObjectClass -eq 'user' -or ObjectClass -eq 'contact')" -properties name,ObjectClass,Mail | Select-Object #{n='Group';e={$Group}},name, Objectclass, mail
[pscustomobject]"" | Select-Object Group,Name,ObjectClass,Mail
[pscustomobject]"" | Select-Object Group,Name,ObjectClass,Mail
}
$results | Export-csv Contacts.csv -NoTypeInformation
Explanation:
Export-Csv converts an object or array of objects with properties into a CSV file. You can see the same result in the console with ConvertTo-Csv. Properties are converted into columns and property values are placed under their associated columns. When you output a string as in Write-Output $Group, it has a property of Length. To fix this, you need to add $Group as a calculated property in your Select-Object. If you want to do blank lines in your CSV, then you should output another object with all of the property values as ''.
When you mix objects in your PowerShell outputs, you can see unexpected results. Your Get-ADObject outputs a custom object. Your Write-Output lines output a string. Those two object types do not share properties. So you only see the properties for the first object in your array, which is a string. If you put all of the Write-Output statements at the end of your loop, you will see more properties in your CSV. See below for an example that just by reversing the order of processed objects, you get a different result.
$str = "string"
$obj = [pscustomobject]#{property1 = "value1"; property2 = "value2"}
$str,$obj | convertto-csv -notype
"Length"
"6"
$obj,$str | convertto-csv -notype
"property1","property2"
"value1","value2"
,
Notice the properties available to the custom object $obj and the string $str.
$obj | get-member -Type Properties
TypeName: System.Management.Automation.PSCustomObject
Name MemberType Definition
---- ---------- ----------
property1 NoteProperty string property1=value1
property2 NoteProperty string property2=value2
$str | get-member -Type Properties
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
Length Property int Length {get;}

Trying to write a Powershell query that shows all resource groups and the tag values

I am trying to use Powershell to create a csv file that will provide a list of the Resource Groups and the Value of the Tags I have associated with them called "Cost Code" and "Environment"
I have tried the following commands which a mixture of the two provide the information I need I just need to join the two outputs.
(Get-AzureRmResourceGroup | Where ResourceGroupName -like rg*).Tags
(Get-AzureRmResourceGroup | Where ResourceGroupName -like rg*) | select ResourceGroupName, Tags | ft
The way I would like it to look is
ResourceGroup,Cost Code,Environment
RG1,1,Dev
RG2,2,Test
RG3,3,Prod
RG4,4,Dev
RG5,5,Test
RG6,6,Prod
RG7,7,Dev
RG8,8,Test
RG9,9,Prod
The following should output what you need:
Get-AzureRmResourceGroup | Where-Object { $_.ResourceGroupName -like "rg*"} |
Select-Object #{n='ResourceGroup';e={$_.ResourceGroupName}},
#{n='Cost Code';e={$_.Tags['Cost Code']}},
#{n='Environment';e={$_.Tags['Environment']}} |
ConvertTo-Csv -NoTypeInformation
Explanation:
Select-Object here is using calculated properties with three different hash tables. ResourceGroup is calculated only because you didn't specifically want the ResourceGroupName property provided by the standard output. Since .Tags stores a hash table, you can reference its values by named index (hashtable['tag_name']). The Select-Object results can be piped into any Object to CSV converter for the desired output. Here ConvertTo-Csv is used.

Azure Powershell - Script to obtain VM info across subscriptions

Trying to run a script that will connect to each subscription, and pull the
$azureSubs = Get-AzureRMSubscription
$azureSubs | ForEach-Object {Select-AzureRMSubscription $_ | Out-Null; Get-AzureRMVM | select resourcegroupname, name, licensetype -WarningAction SilentlyContinue}
This works, BUT I'd like to add two more pieces of information: the "OSType" and "VMSize"
If I do a GET-AZURERMVM, in the table for that subscription that the command is run in, the two pieces of information I need are there: VmSize and OsType
However, when I try to add them to the query, the columns are blank.
I believe the VmSize is in the HardwareProfile, and OsType is in the OsProfile, as if I run a "Get-AzureRMVM -name (name) -resourcegroupname (RGname)", then it shows "Hardware Profile: VMSize" and "OSProfile: ComputerName, AdminUsername windowsConfiguration, Secrets"
Ultimate goal is to get the script that will, for each subscription, print results like:
ResourceGroupName | Name | License Type | VMSize | OS Type
TEST_RG | Test_VM | Windows_Server | DS3_v2 | Windows
Test_RG | Test_VM2 | | DS3_v2 | Linux
etc.
Thankful for any help; sorry for such a noob question. Have spent so much time trying to figure this out...
Something like the following would work.
What you were missing mainly was calculated properties.
This is what allow you to perform a select of custom property.
Some notes:
In your code, you used -WarningAction SilentlyContinue on the Select statement. You need to put it on the Get-AzureRMVM CmdLet instead.
This is my opinion but unless you are writing one-liners on purposes, try aerating your code more. It will make it way easier to read, debug and maintain.
This is the code you wrote, modified to include the calculated properties and with the WarningAction parameter set to Get-AzureRMVM instead of the Select statement.
$azureSubs = Get-AzureRMSubscription
$Vms = $azureSubs | ForEach-Object {Select-AzureRMSubscription $_ | Out-Null; Get-AzureRMVM -WarningAction SilentlyContinue | select resourcegroupname, name, licensetype, #{Name="VMSize";Expression={$_.HardwareProfile.VmSize}},#{Name="OsType";Expression={$_.StorageProfile.OsDisk.OsType}}}
$Vms | ft
The same thing, with some progress indication without forcing everything on one line.
$azureSubs = Get-AzureRMSubscription
$Vms = New-Object 'System.Collections.Generic.List[PSObject]'
ForEach ($sub in $azureSubs) {
Select-AzureRMSubscription $sub | Out-Null
Write-Host "Processing Subscription $($sub.Name)".PadRight(50,' ') -ForegroundColor Cyan -NoNewline
[PsObject[]]$items = Get-AzureRMVM -WarningAction SilentlyContinue |
select resourcegroupname,
name,
licensetype,
#{Name="VMSize";Expression={$_.HardwareProfile.VmSize}},
#{Name="OsType";Expression={$_.StorageProfile.OsDisk.OsType}}
Write-Host "($($items.count) retrieved)"
if ($items -ne $null) {
$vms.AddRange($items)
}
}
$vms | Format-Table
You are looking for something like this on the select side
select resourcegroupname, name, licensetype, #{Name="VMSize";Expression={$_.HardwareProfile.VmSize}}, #{Name="OsType";Expression={$_.StorageProfile.OsDisk.OsType}}

How do I add another column to a System.Object using a list in Powershell?

I am making a script to query active directory via powershell and pull all computers that contain a username in the description field, then filter that list with only computers last logged in the past 14 days.
This is what I have so far:
$queryAD = Get-ADComputer -SearchBase 'OU=West Division,DC=cable,DC=comcast,DC=com' -Properties Name, Description -Filter {(Name -like "WA*") -and (Description -like $wildCard)} | Select-Object Name, Description
$lastLogon = $queryAD | Select-Object -ExpandProperty Description | %{$_.replace(("$NTname" + ";"),"").split(";")[0]} | %{get-date $_ -format d}
I'd like to add the list generated from $lastLogon to $queryAD, right now $queryAD is returning two columns with headers Name and Description. I need a third header added called Last Logon Date and contain the list in $lastLogon. Please advise.
You could assign the values to an array of objects to make your output cleaner (if this method is providing you the data you want) like so:
$queryAD = Get-ADComputer -SearchBase 'OU=West Division,DC=cable,DC=comcast,DC=com' -Properties Name, Description -Filter {(Name -like "WA*") -and (Description -like $wildCard)} | Select-Object Name, Description
$computer_list = #()
foreach($computer in $queryAD) {
$computer_info = New-Object PSObject -Property #{
Name = $computer.Name
Description = $computer.Description
LastLogonDate = $computer | Select-Object -ExpandProperty Description | %{$_.replace(("$NTname" + ";"),"").split(";")[0]} | %{get-date $_ -format d}
}
$computer_list += $computer_info
}
in which case $computer_list will contain all of the info you're gathering in tidy objects.
...but this method seems overcomplicated. Look into this blog entry by Matt Vogt for a better way to query for old machines in AD.

Resources