Checking several Hyper-V Hosts for a specific VM in Powershell - powershell

I am writing a script to administer Hyper-VMs using the PowerShell Management Library for Hyper-V.
Since we are using several Hyper-V Hosts and our VMs can change their host for performance reasons or other reasons I need a script that finds out which Host a VM runs on for the following functions.
This was my try at accomplishing this:
function IdentifyHost
{
param
(
[parameter(Position=0, Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$VM
)
[Array]$hosts=Get-VMHost
if ($hosts.count -eq 0)
{
Write-Warning "No valid hosts found."
}
for ([int]$i=0; $i -lt $hosts.count; $i++ )
{
try
{
$out = Get-VM -Name $VM -Server $hosts[$i] -ErrorAction Stop
}
catch [UnauthorizedAccessException]
{
Write-Warning "Access to $hosts[$i] denied."
}
if ($VM -is [String])
{
if ($out.VMElementName -eq $VM )
{
return $out.__SERVER
}
}
elseif ($VM.ElementName -ne $null)
{
if ($out.VMElementName -eq $VM.VMElementName)
{
return $out.__SERVER
}
}
}
Write-Warning "No Host found for $VM"
}
Get-VMHost returns an array of all available Hyper-V hosts in the local area network.
My problem is that my function always returns the first element of the $hosts array whenever there is an UnauthorizedAccessException for the first element.
The plan is as following:
If the VM exists on the Host he will return a WMI Object representing that VM whose VMElementName property is equal to the VMs name given as parameter.
If the VM is given a WMI Object representing a VM the VMElementName properties of the two objects are equal.
If the VM does not exist on the Host he returns nothing.
If there's an access issue it should be catched.
But somehow it doesn't work out.
My question is this: What am I doing wrong in the code? And how can I fix it?
EDIT: The output of the function is the access problem warning for the first element of the $hosts array and then the first element of $hosts itself.
EDIT2: I fixed this myself by changing the return from the fragile $hosts[$i] to $out.__Server

Okay so I found a possible way of solving this issue:
Instead of returning the $hosts[$i] which yields unfavorable results I return the __Server property of $out, assuming there is a valid $out that matches the conditions.
If any of you guys knows a better or cleaner way of doing this, please by my guest.

Related

ShouldProcess failing in PowerShell7

Environment: Windows Server 2022 21H2, Powershell 7.2 (running as administrator)
I have a script that implements ShouldProcess, which works fine in Windows PowerShell 5. However, in PowerShell 7, the script invariably throws the error Cannot find an overload for "ShouldProcess" and the argument count: "1". ShouldProcess at MSDoc says that the one-argument overload for $PSCmdlet.ShouldProcess() exists and should work.
It's failing, as above. Why?
The script in question is pasted below; it's in a script module:
function Remove-DomainUserProfile {
<#
#Comment-based help removed for space considerations
#>
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact="High")]
param(
[Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
[Parameter(ParameterSetName='SpecificProfile')]
[Parameter(ParameterSetName='ByAge')]
[Parameter(ParameterSetName='AllProfiles')]
[String[]]$ComputerName = $env:ComputerName,
[Parameter(Mandatory=$true,ParameterSetName='SpecificProfile')]
[Parameter(ParameterSetName='ByAge')]
[Alias("UserName","sAMAccountName")]
[String]$Identity,
[Parameter(ParameterSetName='ByAge')]
[Parameter(ParameterSetName='AllProfiles')]
[Switch]$DomainOnly,
[Parameter(ParameterSetName='SpecificProfile')]
[Parameter(ParameterSetName='ByAge')]
[Int]$Age,
[Parameter(Mandatory=$true,ParameterSetName='AllProfiles')]
[Switch]$All
)
BEGIN {
if (-NOT (Test-IsAdmin)) {
Write-Output "This function requires being run in an Administrator session! Please start a PowerShell
session with Run As Administrator and try running this command again."
return
}
$NoSystemAccounts = "SID!='S-1-5-18' AND SID!='S-1-5-19' AND SID!='S-1-5-20' AND NOT SID LIKE 'S-1-5-%-500' "
# Don't even bother with the system or administrator accounts.
if ($DomainOnly) {
$SIDQuery = "SID LIKE '$((Get-ADDomain).DomainSID)%' " # All domain account SIDs begin
with the domain SID
} elseif ($Identity.Length -ne 0) {
$SIDQuery = "SID LIKE '$(Get-UserSID -AccountName $Identity)' "
}
$CutoffDate = (Get-Date).AddDays(-$Age)
$Query = "SELECT * FROM Win32_UserProfile "
}
PROCESS{
ForEach ($Computer in $ComputerName) {
Write-Verbose "Processing Computer $Computer..."
if ($SIDQuery) {
$Query += "WHERE " + $SIDQuery
} else {
$Query += "WHERE " + $NoSystemAccounts
}
if ($All) {
Write-Verbose "Querying WMI using '$Query'"
$UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query
} else {
Write-Verbose "Querying WMI using '$Query' and filtering for profiles last used before $CutoffDate ..."
$UserProfiles = Get-WMIObject -ComputerName $Computer -Query $Query | Where-Object {
[Management.ManagementDateTimeConverter]::ToDateTime($_.LastUseTime) -lt $CutoffDate }
}
ForEach ($UserProfile in $UserProfiles) {
if ($PSCmdlet.ShouldProcess($UserProfile)) {
Write-Verbose "Deleting profile object $UserProfile ($(Get-SIDUser $UserProfile.SID))..."
$UserProfile.Delete()
}
}
}
}
END {}
}
To complement Santiago Squarzon's excellent analysis:
The behavior, present up to at least PowerShell 7.2.1, should be considered a bug, because any object should be auto-convertible to a string in a .NET method call.
There is no reason for [pscustomobject] a.k.a [psobject] instances to act differently than instances of any other type (irrespective of whether implicit stringification makes sense in a given situation); to give a simple example:
If (42).ToString((Get-Item /)) works, ...
... there's no reason why (42).ToString(([pscustomobject] #{ foo=1 })) shouldn't.
Note that implicit stringification in the context of cmdlets / functions / script is not affected; e.g., Get-Date -Format ([pscustomobject] #{ foo=1 }) doesn't cause an error.
See GitHub issue #16988.
The reason that the serialization infrastructure is involved at all is that the obsolete WMI cmdlets such as Get-WmiObject aren't natively available in PowerShell (Core) v6+ anymore, and using them implicitly makes use of the Windows PowerShell Compatibility feature:
This entails using a hidden powershell.exe child process, communication with which requires use of serialization, during which most non-primitive types lose their type identity and are emulated with method-less [psobject] instances that contain copies of the original object's properties.
In PowerShell v3 and above, and especially in PowerShell (Core) v6+, use the CIM cmdlets instead, such as Get-CimInstance, instead:
While similar to the WMI cmdlets in many respects, an important difference is that objects returned from CIM cmdlets have no methods; instead, methods must be called via Invoke-CimMethod.
See this answer for more information.
For reference, this error can be reproduced on both PowerShell versions 5.1 and Core. The steps to reproduce is passing a System.Management.Automation.PSObject as argument to the .ShouldProcess(String) overload. It makes sense, by looking at your comment mentioning a serialized object. In below example, if the System.Diagnostics.Process object is not serialized it works properly on both versions.
function Test-ShouldProcess {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = "High")]
param()
$obj = [System.Management.Automation.PSSerializer]::Deserialize(
[System.Management.Automation.PSSerializer]::Serialize((Get-Process)[0])
)
# will throw
if ($PSCmdlet.ShouldProcess($obj)) { 'hello' }
}
Test-ShouldProcess

PowerShell function to check the health of remote disks not actually remoting

I am trying to use a script to check the health of physical disks on Lenovo computers utilizing the storcli tool. I found this and have tried to modify it to be a function and allow the input of remote computers and eventually use Get-Content to input a server list. For whatever reason it takes the computer input from -ComputerName but does not actually run the commands on the remote computer. It seems to just read the disks on the local machine and always reports back "Healthy" while I know there are bad disks on the remote machine. Also I have run the script on the machine with the bad disks and it does work and reports the disk as failed. Could anyone offer any insight into what I am missing for this to actually check the remote machines? Remoting is enabled as I can run other scripts without issue. Thank you in advance.
Function Get-DriveStatus {
[cmdletbinding()]
param(
[string[]]$computername = $env:computername,
[string]$StorCLILocation = "C:\LenovoToolkit\StorCli64.exe",
[string]$StorCliCommand = "/c0/eall/sall show j"
)
foreach ($computer in $computername) {
try {
$ExecuteStoreCLI = & $StorCliLocation $StorCliCommand | out-string
$ArrayStorCLI= ConvertFrom-Json $ExecuteStoreCLI
}catch{
$ScriptError = "StorCli Command has Failed: $($_.Exception.Message)"
exit
}
foreach($PhysicalDrive in $ArrayStorCLI.Controllers.'Response Data'.'Drive Information'){
if(($($PhysicalDrive.State) -ne "Onln") -and ($($PhysicalDrive.State -ne "GHS"))) {
$RAIDStatus += "Physical Drive $($PhysicalDrive.'DID') With Size $($PhysicalDrive.'Size') is $($PhysicalDrive.State)`n"
}
}
#If the variables are not set, We’re setting them to a “Healthy” state as our final action.
if (!$RAIDStatus) { $RAIDStatus = "Healthy" }
if (!$ScriptError) { $ScriptError = "Healthy" }
if ($ScriptError -eq "Healthy")
{
Write-Host $computer $RAIDStatus
}
else
{
Write-Host $computer "Error: ".$ScriptError
}
}#End foreach $computer
}#End function
$RAIDStatus = $null
$ScriptError = $null

Foreach loop in powershell with hasharray

I'm writing a powershell script to ping all the servers and check which are offline. but i have a bug. By name it works perfectly. But when i do test-connection with an IP it seems to work BUT i cant output the name of the IP in the hashlist. Could someone help me figure this out? Thanks!!
System.Collections.Hashtable.keys Is online/available, This is what it outputs. But i want it to say "Servername is online/available"
#Creating IP Array list
$ip_array = #{
Server = [ipaddress] "192.168.1.1"
sws = [ipaddress] "192.168.1.1"
}
Foreach ($ip in $ip_array)
{
if((Test-Connection -IPAddress $ip.values.ipaddresstostring -quiet -count 1 ) -eq $false)
{
write-output("$ip.keys Is offline/unavailable, please troubleshoot connection, script is terminating") | Red
}
else
{
$ping = $true
write-output("$ip.keys Is online/available") | Green
}
}
PowerShell's default pipeline semantics (any collection that can be enumerated and unraveled will be) makes dictionaries a pain to work with - piping them anywhere would result in a list of disjoint key-value-pairs, dictionary itself lost.
For this reason, PowerShell refuses to automatically enumerate dictionaries, and you must manually obtain an enumerator in order to loop over the entries in it:
foreach($entry in $ip_hash.GetEnumerator()){
# reference `$entry.Key` or `$entry.Name` for the key (eg. Server)
# reference `$entry.Value` for the value (eg. 192.168.1.1)
}
If you really intend to use a Hashtable for this, combining IP addresses with computernames, change to something like this:
# creating IP Hashtable
$ip_hash = #{
'192.168.1.1' = 'Server1'
'192.168.1.2' = 'Server2'
# etcetera
}
# loop through the hash, key-by-key
foreach ($ip in $ip_hash.Keys) {
$ping = Test-Connection -ComputerName $ip -Quiet -Count 1 -ErrorAction SilentlyContinue
if(!$ping) {
Write-Host "Server $($ip_hash[$ip]) is offline/unavailable, please troubleshoot connection, script is terminating" -ForegroundColor Red
}
else {
Write-Host "Server $($ip_hash[$ip]) is online/available" -ForegroundColor Green
}
}
Output would look like:
The Keys in the hash must all have unique values

How to register the result of a PowerShell expression in a variable for a DSC?

I am trying to configure an Azure VM with Azure Automation DSC. One of the resources I want to set is DNS on the client workstation with xDnsServerAddress from xNetworking module.
The problem is that it requires an interface alias and interface aliases change on Azure VMs vary depending on deployment (mainly VMs seem to get either Ethernet or Ethernet 2).
I can query the interface name locally using the following cmdlet expression:
$Interface=Get-NetAdapter|Where Name -Like "Ethernet*"|Select-Object -First 1
$InterfaceAlias=$($Interface.Name)
I don't know, however, how to use it inside the DSC.
My DSC configuration is as below (only the relevant part):
Configuration MyDscConfig
{
Import-DscResource -ModuleName xNetworking
# place-1
Node $AllNodes.where{$_.Role -eq "Workstation"}.NodeName
{
# place-2
xDnsServerAddress DnsServerAddressSetToDc1
{
Address = '10.0.0.4'
InterfaceAlias = $InterfaceAlias
AddressFamily = 'IPv4'
Validate = $true
}
}
}
The problem is that if I place the cmdlet expression either in place-1 or place-2 the compilation job fails with:
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: The term 'Get-NetAdapter' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.
I assume it tries to execute Get-NetAdapter on the pull server, but I may be misinterpreting the error message.
How can I register the result of the cmdlet expression on a destination machine and register it in $InterfaceAlias variable for the xDnsServerAddress resource?
You currently cannot perform a query keep the results of the operation and use it to declare the next state (See notes at the end of the answer.)
You can work around this limitation using the documented workaround/solution from xNetworking, which will find an active ethernet adapter named Ethernet1 if it does not, it will find the first active ethernet adapter and make sure it is named Ethernet1. Then, use a resource to set the DSC server address on Ethernet1.
This is investigational, names and parameters are subject to change. The DSC team is investigating a better way to do this.
Configuration SetDns
{
param
(
[string[]]$NodeName = 'localhost'
)
Import-DSCResource -ModuleName xNetworking
Node $NodeName
{
script NetAdapterName
{
GetScript = {
Import-module xNetworking
$getResult = Get-xNetworkAdapterName -Name 'Ethernet1'
return #{
result = $getResult
}
}
TestScript = {
Import-module xNetworking
Test-xNetworkAdapterName -Name 'Ethernet1'
}
SetScript = {
Import-module xNetworking
Set-xNetworkAdapterName -Name 'Ethernet1' -IgnoreMultipleMatchingAdapters
}
}
xDnsServerAddress DnsServerAddress
{
Address = '10.0.0.4'
InterfaceAlias = 'Ethernet1'
AddressFamily = 'IPv4'
DependsOn = #('[Script]NetAdapterName')
}
}
}
Notes:
There is a question in the comments. The summary of the question is if querying turns the declarative paradigm into an imperative paradigm.
Answer:
I don't believe querying turns it into an imperative paradigm, but you
currently cannot perform a query keep the results of the operation and
use it to declare the next state.
This currently forces us into something a little further away from
declarative for the problem that I would like. My personal opinion is that we
should work with what we have and write resources that query and set a
known state. Then, use the known state through the rest of the
configuration (a form a declarative-relative, per your terminology).
The DSC team has this similar UserVoice
suggestion
we are using to track this request. Please upvote it if you think this
is a useful feature.
It seems the DSC Node Configuration (which is a MOF file) must have all the values set at the time of compilation.
As a workaround I decided to use a PowerShell script resource instead of xDnsServerAddress (some values below are hardcoded to match the example in the question):
Script DnsServerAddressSetToDc1
{
GetScript = {
Return #{
Result = [string](get-DnsClientServerAddress -InterfaceAlias (Get-NetAdapter|Where Name -Like "Ethernet*"|Select-Object -First 1).Name -AddressFamily IPv4).ServerAddresses
}
}
TestScript = {
if (([string](get-DnsClientServerAddress -InterfaceAlias (Get-NetAdapter|Where Name -Like "Ethernet*"|Select-Object -First 1).Name -AddressFamily IPv4).ServerAddresses) -eq '10.0.0.4') {
Write-Verbose "DNS server set"
Return $true
} Else {
Write-Verbose "DNS Server not set"
Return $false
}
}
SetScript = {
Set-DnsClientServerAddress `
-InterfaceAlias (Get-NetAdapter|Where Name -Like "Ethernet*"|Select-Object -First 1).Name `
-ServerAddresses 10.0.0.4 `
-Validate `
-ErrorAction Stop
}
}

What's the fastest way to get online computers

I'm writing a function which returns all Online Computers in our network, so I can do stuff like this:
Get-OnlineComputers | % { get-process -computername $_ }
Now I basically got my function ready, but it's taking way too long.
I want to only return Computers which have WinRM active, but I also want to provide the option to get every computer even those which haven't got WinRM set up (switch parameter).
This is my function. first it creates a pssession to the domaincontroller, to get all computers in our LAN. then foreach computer, it will test if they have WinRM active or if they accept ping. if so, it gets returned.
$session = New-PSSession Domaincontroller
$computers = Invoke-Command -Session $session { Get-ADComputer -filter * } | select -ExpandProperty Name
$computers | % {
if ($IncludeNoWinRM.IsPresent)
{
$ErrorActionPreference = "SilentlyContinue"
$ping = Test-NetConnection $_
if ($ping.PingSucceeded -eq 'True')
{
$_
}
}
else
{
$ErrorActionPreference = "SilentlyContinue"
$WinRM = Test-WSMan $_
if ($WinRM)
{
$_
}
}
}
Is this the best way I can go to check my online computers? Does anyone have a faster and better idea?
Thanks!
Very Quick Solution is using the -Quiet Parameter of the Test-Connection cmdlet:
so for example:
$ping = Test-Connection "Computer" -Quiet -Count 1
if ($ping)
{
"Online"
}
else
{
"Offline"
}
if it's not enough fast for you, you can use the Send Method of the System.Net.NetworkInformation.Ping
here's a sample function:
Function Test-Ping
{
Param($computer = "127.0.0.1")
$ping = new-object System.Net.NetworkInformation.Ping
Try
{
[void]$ping.send($computer,1)
$Online = $true
}
Catch
{
$Online = $False
}
Return $Online
}
Regarding execute it on multiple computers, I suggest using RunSpaces, as it's the fastest Multithreading you can get with PowerShell,
For more information see:
Runspaces vs Jobs
Basic Runspaces implemenation
Boe Prox (master of runspaces) has written a function which is available from the Powershell Gallery. I've linked the script below.
He uses many of the answers already given to achieve the simultaneous examination of 100s of computers by name. The script gets WMI network information if test-connection succeeds. It should be fairly easy to adapt to get any other information you want, or just return the result of the test-connection.
The script actually uses runspace pools rather than straight runspaces to limit the amount of simultaneous threads that your loop can spawn.
Boe also wrote the PoSH-RSJob module already referenced. This script will achieve what you want in native PoSH without having to install his module.
https://gallery.technet.microsoft.com/scriptcenter/Speedy-Network-Information-5b1406fb