Powershell ForEach-Object: You cannot call a method on a null-valued expression - azure

I am trying to extract the tag values for SystemOwner and TechnicalOwner.
However I get the error "ForEach-Object: You cannot call a method on a null-valued expression"
I already know that the "$Tags = $RG.Tags" could be the issue because running this variable does not give me any results. Then how do I get the resource group tags?
$RGs = (Get-AzResourceGroup).ResourceGroupName
ForEach-Object $RG in $RGs
{
# Get email address value for each SystemOwner and TechnicalOwner Tag in the RG
$Tags = $RG.Tags
$TO = ($Tags.GetEnumerator() | Where-Object {$_.Key -eq "Technical*"}).value
If ($TO)
{
if ($TO.Value -ne $null) {
Write-Host "Technical Owner Found in the Resource Group, building an Array"
$contactemailadress += $TO.Value
}
}
$SO = $Tags.GetEnumerator() | Where-Object {$_.Key -eq "SystemOwner"} #looking for Resource Group tag Names that have a space in the name
If ($SO)
{
if ($SO.Value -ne $null) {
Write-Host "System Owner Found in the Resource Group, building an Array"
$contactemailadress += $SO.Value
}
}
}

I think you could do something like this:
$contactemailadress = #()
$RGs = Get-AzResourceGroup
$RGs | ForEach-Object {
# Get email address value for each SystemOwner and TechnicalOwner Tag in the RG
$Tags = $_.Tags
If ($Tags -and $Tags['TechnicalOwner'])
{
Write-Host "Technical Owner Found in the Resource Group, building an Array"
$contactemailadress += $Tags['TechnicalOwner']
}
If ($Tags -and $Tags['SystemOwner'])
{
Write-Host "System Owner Found in the Resource Group, building an Array"
$contactemailadress += $Tags['SystemOwner']
}
}
The main issue you have is that (Get-AzResourceGroup).ResourceGroupName returns the Resource Group Name - however, in order for the tags to be returned you would need the full object.
You can prove this by checking for Properties on the Get-AzResourceGroup command you are running:
(Get-AzResourceGroup -Name resource_group_name).ResourceGroupName | Get-Member -Type Property
TypeName: System.String
Name MemberType Definition
---- ---------- ----------
Length Property int Length {get;}
As you can see, this command only has one property - length, which is not useful for your scripts purpose.
If you run this instead, you get the full object with all properties:
Get-AzResourceGroup -Name resource_group_name | Get-Member -Type Property
TypeName: Microsoft.Azure.Commands.ResourceManager.Cmdlets.SdkModels.PSResourceGroup
Name MemberType Definition
---- ---------- ----------
Location Property string Location {get;set;}
ManagedBy Property string ManagedBy {get;set;}
ProvisioningState Property string ProvisioningState {get;set;}
ResourceGroupName Property string ResourceGroupName {get;set;}
ResourceId Property string ResourceId {get;set;}
Tags Property hashtable Tags {get;set;}
TagsTable Property string TagsTable {get;}
You can use foreach or ForEach-Object, both should work but when using the latter you can query the variable inside the loop using the $_ notation.
Lastly, in your script there seem to be some variables unnecessarily used (but I don't know if you would need them later on), such as $TO and two if-statements that more or less do the same (i.E. If ($SO) and if ($SO.Value -ne $null)).

Related

Powershell workflow with "foreach -parallel" and Invoke-AZVMRunCommand- how to get results out?

I have a bunch of VMs in Azure I want to run the same script against. Because of reasons, I can't use powershell 7, so I have to use powershell 5. Which means I can use "foreach -parallel", but NOT "foreach-parallel". So it can work, but requires a workflow.
I have the following workflow that will grab the list of servers in a specific tenant/environment, and then run a foreach -parallel to run a script against each of them at the same time.
The problem I'm running into is that it appears to work, but the result I get out is:
Microsoft.Azure.Commands.Compute.Automation.Models.PSRunCommandResult
blah
Which is not the actual result (in this it would be the time). Because it's within a workflow, getting the details out is... painful. How do I get the results? It appears to run, there's no errors, but I can't figure out the results. Thanks.
The get-member on $out gives:
Name MemberType Definition
---- ---------- ----------
GetType Method type GetType()
ToString Method string ToString(), string ToString(string format, System.IFormatProvider formatProvider), string IFormattable.ToString(string format, System.I...
PSComputerName NoteProperty string PSComputerName=localhost
PSShowComputerName NoteProperty bool PSShowComputerName=True
PSSourceJobInstanceId NoteProperty guid PSSourceJobInstanceId=0870d1ff-1234-5678-014e-2e123456c7d8
Capacity Property System.Int32 {get;set;}
Count Property System.Int32 {get;set;}
EndTime Property {get;set;}
Error Property {get;set;}
Item Property {get;set;}
Name Property {get;set;}
Output Property {get;set;}
StartTime Property {get;set;}
Status Property System.String {get;set;}
Value Property Deserialized.System.Collections.Generic.List`1[[Microsoft.Azure.Management.Compute.Models.InstanceViewStatus, Microsoft.Azure.Management.Compu...
Microsoft.Azure.Commands.Compute.Automation.Models.PSRunCommandResult
And here's the script I'm running:
#for some reason you need to disconnect from one and then connect to the other, can't run two at once.
disconnect-azaccount
connect-azaccount -Tenant "mytenantid"
#get all the dev Windows VM servers in the current tenant that are turned on.
$serverinfo = #()
Get-AzContext -ListAvailable |where-object {$_.Name -like "*Dev*"}| %{
$_|select-azcontext
$serverinfo += get-azvm -status | Where-Object {$_.PowerState -eq "VM running" -and $_.StorageProfile.OSDisk.OSType -eq "Windows"}
}
#honestly, for the test I filtered down to the jumpboxes.
$serverinfo2 = $serverinfo|where {$_.Name -like "*jumpbox*"}
#workflows are needed; can't run foreach -parallel otherwise.
Workflow TestParallel{
#need to pass in the details, it can't reach the $serverinfo2 otherwise.
param($listofservers) #take serverinfo2 and make it accessible from within the workflow.
$test=#()
#need to set the proper context
Get-AzContext -ListAvailable |where-object {$_.Name -like "*Dev*"}| select-azcontext
#now we run each of these at the same time. Foreach-parallel requires 7, but we can do foreach -parallel within powershell 4 workflows.
Foreach -parallel($server in $listofservers){
#now that the script is on there, run the command locally.
$out = Invoke-AzVMRunCommand `
-ResourceGroupName $server.ResourceGroupName `
-Name $server.name `
-CommandId 'RunPowerShellScript' `
-ScriptString "$dt = gwmi win32_localtime; New-Object DateTime $dt.year,$dt.month,$dt.day,$dt.hour,$dt.minute, $dt.second"
#scriptstring is a newer command, added in july 2022
#Formating the Output with the VM name. Value[0].Message contains the results from running the script.
#export it to a variable that will survive the foreach
#$WORKFLOW:test += $server.Name + " " + $out.Value[0].Message
$WORKFLOW:test += $out.Value[1].Message #appears to be 1 now, using " -scriptstring"
#writing it locally to see if it shows up
"$out"
}
[array]$resultsArray = #($test)
write-output ("blah " + $test[0])
$resultsArray
}
TestParallel $serverinfo2

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.

How to "parse" Azure resource id object with powershell

I am currently trying to pull data from an Azure policy for some automation with:
(Get-AzPolicyState -Filter "ComplianceState eq 'NonCompliant'" | Where-Object {$.PolicyDefinitionReferenceId -eq "azure backup should be enabled for virtual machines_1"} | Where-Object {$.SubscriptionId -eq "0000"}).ResourceId
Type Name: System.String
Example output
/subscriptions/00/resourcegroups/rg-test/providers/microsoft.compute/virtualmachines/vm-test
/subscriptions/00/resourcegroups/rg-test2/providers/microsoft.compute/virtualmachines/vm-test2
I am confused on how I can just pull the resource group name and virtual machine name only into a variable array.
The resource group name is always preceded by /resourcegroups/ and VM name is always preceded by /virtualmachines/.
Any guidance would be greatly appreciated. Thank you!
If you just want to extract your VM name and groupName from resourceID, just try the code below:
function parseGroupAndName{
param (
[string]$resourceID
)
$array = $resourceID.Split('/')
$indexG = 0..($array.Length -1) | where {$array[$_] -eq 'resourcegroups'}
$indexV = 0..($array.Length -1) | where {$array[$_] -eq 'virtualmachines'}
$result = $array.get($indexG+1),$array.get($indexV+1)
return $result
}
$resourceID = '/subscriptions/00/resourcegroups/rg-test/providers/microsoft.compute/virtualmachines/vm-test'
parseGroupAndName -resourceID $resourceID
Result:
more simple :
$resourceID = '/subscriptions/xxxx/resourceGroups/rrrrrrr/providers/aaaa/bbbb/nnnnn'
$cc=Get-AzResource -ResourceId
$cc.ResourceType
aaaaa/bbbb
$cc.ResourceGroupName
rrrrrr
$cc.Name
nnnnnnn

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;}

How do i get a list of Vm in a recourse group

Im trying to get all Vms in a resource group then send every element to a function
$a = Get-AzureRmVM -ResourceGroupName Test2 | ft Name
foreach($output in $a) {Stop-AzRmVM -ResourceGroupName "Test2" -Name $output}
Im getting this error message
Start-AzureRmVM : Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'Name'. Specified
method is not supported.
Anish K is correct about the Name parameter expecting a String, not an array of objects.
However, you should also remove the | ft Name because that is only for outputting stuff to console.
I'd use a ForEach-Object here like this (untested):
Get-AzureRmVM -ResourceGroupName 'Test2' | ForEach-Object {
$_ | Stop-AzRmVM
}
As per the error, you are passing an object[] instead of string. Your function Stop-AzRmVm expects string for Name parameter.
Changes your script like below:
$a = Get-AzureRmVM -ResourceGroupName Test2 | ft Name
foreach($output in $a) {Stop-AzRmVM -ResourceGroupName "Test2" -Name $output.Name}

Resources