ForEach Loop failing to continue on the file? - powershell

The ForEach loop on this powershell script is failing to run more than one item before dropping out?
Can someone help me on this one?
function Get-RemoteLogonStatus {
[CmdletBinding()]
param(
[string]$ComputerName = ' '
)
ForEach ($line in Get-Content C:\ADComputers.csv)
{
$Computername = $line
if ( Test-Connection -ComputerName $ComputerName -Count 3 -Quiet ) {
try {
Get-WmiObject –ComputerName $ComputerName –Class Win32_ComputerSystem | Select-Object UserName = $lname -ErrorAction Stop | Out-Null
}
catch {
Write-Output 'No user logged in - RESTARTING.'
Shutdown /r /t 0 /M \\$ComputerName
$ComputerName
return
}
Write-Output 'Computer in use.'
$ComputerName
}
else {
Write-Output 'Computer in Use or is Offline.'
$ComputerName
}
}
$error.clear
}
Get-RemoteLogonStatus
Should run more than one item from the file. The file has 4 items for test:

a function is supposed to contain a block of code that can be repeated a number of times. Your function does all in one go, hence I don't see the need for it. Also it has the possibility to take one argument, but you don't pass it.
'return' is not necessary in PowerShell, it will throw the content of a variable without the need for a 'return'.
Select-Object needs a name that is being passed from the pipe, and not an assignment.
inside the try statement you might want to get an output, but if you pipe the line to Out-null you get nothing. and the catch will never grab any error.
the write-output are not clearly positioned, and difficult to understand.
I can infer what you are trying to achieve is: reboot computers in the csv file IF no user is logged in, is that so? In that case it's much simpler:
foreach ($ComputerName in (Get-Content C:\ADComputers.csv)) {
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $ComputerName | Select-Object UserName
if ($User.UserName -eq $null) {
Restart-Computer -ComputerName $ComputerName -Force
}
}
of if you want to stick to a function and see its purpose see this:
function Restart-Node {
param(
[Parameter(Mandatory=$true)][string]$ComputerName
)
$User = Get-WmiObject -Class Win32_ComputerSystem -ComputerName $ComputerName | Select-Object UserName
if ($User.UserName -eq $null) {
Restart-Computer -ComputerName $ComputerName -Force
}
}
foreach ($ComputerName in (Get-Content C:\ADComputers.csv)) {
Restart-Node -ComputerName $ComputerName
}

Related

Unable to pass variable to Invoke-command

This is my first time using the workflow, could anyone explain me what is wrong with the code ?
Powershell version is 5.1
$Script = {
return(Get-Service WINRM).Status
}
workflow pushupdate{
##Select OUs
$OUs=
"OU=Workstations,DC=contoso,DC=com",
"OU=Notebooks,DC=contoso,DC=com"
foreach -parallel ($computer in ($Ous | foreach { Get-ADComputer -Filter {enabled -eq $true} -SearchBase $_} | Select Name)) {
if ((Test-Connection $computer.name -Quiet) -eq "True") {
Write-Output "Running update on:" $computer.name
InlineScript {
Invoke-Command -ComputerName $computer.name -Script $Script -Verbose
}
}
else{
Write-Output $computer.name "unreachable!"
}
}
}
pushupdate
I keep getting the error:
Invoke-Command : Cannot validate argument on parameter 'ScriptBlock'. The argument is null. Provide a valid value for
the argument, and then try running the command again.
At pushupdate:245 char:245
Variables defined outside the InlineScript block are unknown to the Invoke-Command cmdlet unless you use them as $using:<varname>.
It seems however that you cannot do that with a variable which is actually a scriptblock. That needs to be defined inside the InlineScript itself:
workflow pushupdate{
# Select OUs
$OUs = "OU=Workstations,DC=contoso,DC=com", "OU=Notebooks,DC=contoso,DC=com"
# get a string array of computerNames
$computers = ( $Ous | ForEach-Object { Get-ADComputer -Filter "Enabled -eq 'True'" -SearchBase $_ } ).Name
foreach -parallel ($computer in $computers) {
if (Test-Connection -ComputerName $computer -Quiet -Count 1) {
Write-Output "Running update on:" $computer
InlineScript {
# define the scriptblock here
$script = {(Get-Service WINRM).Status}
Invoke-Command -ComputerName $using:computer -ScriptBlock $Script -Verbose
}
}
else{
Write-Output "$computer unreachable!"
}
}
}
pushupdate

Powershell, I do input a list gather data and output that whole list into one CSV

I am creating a script that reads a list of computer names and collects data from security event logs about who is on the computer, how long they have been on for, and how long it has been since the computer has restarted. I have it working except that it does not output all the data into one CSV. I just receive one CSV file with one computer name.
function Get-KioskInfo {
param (
[parameter(ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True,Position=0)]
[Alias('PSComputerName','DNSHostName','CN','Hostname')]
[string]
$ComputerName = $env:COMPUTERNAME
)
#PARAM
$User = try {(Get-WmiObject -ComputerName $ComputerName Win32_ComputerSystem | Select-Object -ExpandProperty username).trimstart("NG\")} catch {Write-Output "User not detected";break}
$BootStart = ((get-date) - (Get-CimInstance win32_operatingsystem -ComputerName $ComputerName).LastBootUpTime).Days
#These variables are for the DATE & Time calculation
If ($user -NE $null)
{ Write-Verbose 1
# Do something
$Date1 = Get-date
Write-Verbose 2
$SP = Get-WinEvent -ComputerName $ComputerName -FilterHashTable #{LogName = "Security";ID="5379";Data=$User; StartTime=((Get-Date).AddDays(-1))}
Write-Verbose 3
$Date2 =($SP | select -first 1).timecreated
Write-Verbose 4
$USERLOGTIME = ($Date1-$Date2).hours.tostring("N2")
Write-Verbose 5
}
else{Write-Output "No user";break}
Write-Verbose 6
#Rename-Computer -ComputerName "Srv01" -NewName "Server001" -DomainCredential Domain01\Admin01 -Force ------ Rename script for computers if it is needed.
#$computers = Get-Content C:\Users\jaycbee\Desktop\kiosknames.txt ------ To load kiosk list
#foreach ($c in $computers) {start-job -Name $c -ScriptBlock ${Function:get-kioskinfo} -ArgumentList $c} for learning how to do a foreach script
Write "Computer Name: $Computername"
Write "---USER---"
Write "Name: $User"
Write "Log in Time $USERLOGTIME"
Write "Boot start $BootStart days ago"
$ComputerName | ForEach-Object {
if (Test-Connection -ComputerName $ComputerName -Count 1 -Quiet)
{
Invoke-Command -ComputerName $ComputerName {
}
} # Offline Check
else
{
Write-Host "Computer is Unreachable or Offline" -ForegroundColor Gray
}
} # Foreach
$Continue = Read-Host "WARNING! This will READ LIST of computers in \\ou\ouor-groups\Desktop_Support\SD\Kiosks\kiosknames.txt Type CONTINUE to proceed."
if ($Continue -eq "CONTINUE")
{
$Computers = Get-Content '\\ou\ouor-groups\Desktop Support\SD\Kiosks\kiosknames.txt'
foreach ($C in $Computers) {start-job -Name $c -ScriptBlock ${Function:get-kioskinfo} -ArgumentList $c
}
}
[pscustomobject]#{ Name = $ComputerName ; User = $User ; "User Log in time in hours" = $USERLOGTIME;"BootStart days ago" = $BootStart} | export-csv -path "\\ou\ouor-groups\Desktop Support\SD\Kiosks\test45$ComputerName.csv" -Append
} #Function
#For each-computer | do this at this location,
Continuing from my comment. I too wonder why the use of jobs for this use case. Unless you are doing this on hundreds of computers, thus needing parallel processing.
This refactor/formatting is just my way of making sense of what you posted. I'm old, and crowded code just really hurts my eyes. ;-} Yet, code the way you like of course. ;-}
I do not have an environment to test this, but give it a shot.
function Get-KioskInfo
{
param
(
[parameter(ValueFromPipeline = $True,ValueFromPipelineByPropertyName = $True,Position = 0)]
[Alias(
'PSComputerName',
'DNSHostName',
'CN',
'Hostname'
)]
[string]
$ComputerName = $env:COMPUTERNAME
)
($User = try
{
(Get-WmiObject -ComputerName $ComputerName Win32_ComputerSystem |
Select-Object -ExpandProperty username).trimstart("NG\")
}
catch
{
'User not detected'
break
}
)
($BootStart = ((get-date) - (Get-CimInstance win32_operatingsystem -ComputerName $ComputerName).LastBootUpTime).Days)
If ($user -NE $null)
{
($Date1 = Get-date)
($SP = Get-WinEvent -ComputerName $ComputerName -FilterHashTable #{
LogName = 'Security'
ID = '5379'
Data = $User
StartTime = ((Get-Date).AddDays(-1))
})
($Date2 = (
$SP |
select -first 1
).timecreated)
($USERLOGTIME = ($Date1-$Date2).hours.tostring('N2'))
}
else
{
'No user'
break
}
"Computer Name: $Computername
---USER---
Name: $User
Log in Time $USERLOGTIME
Boot start $BootStart days ago"
$ComputerName |
ForEach-Object {
if (Test-Connection -ComputerName $ComputerName -Count 1 -Quiet)
{Invoke-Command -ComputerName $ComputerName}
else
{Write-Warning -Message 'Computer is Unreachable or Offline'}
}
$UserMessage = '
WARNING!
This will READ LIST of computers in:
\\ou\ouor-groups\Desktop_Support\SD\Kiosks\kiosknames.txt
Type CONTINUE to proceed'
$Continue = Read-Host $UserMessage
if ($Continue -eq 'CONTINUE')
{
Get-Content '\\ou\ouor-groups\Desktop Support\SD\Kiosks\kiosknames.txt' |
foreach {
{start-job -Name $PSItem -ScriptBlock ${Function:get-kioskinfo} -ArgumentList $PSItem}
[pscustomobject]#{
Name = $ComputerName
User = $User
'User Log in time in hours' = $USERLOGTIME
'BootStart days ago' = $BootStart
}
} |
Export-Csv -path "$PWD\$ComputerName.csv" -Append
}
}
These didn't help me with my solution, but you were right about the start-jobs. I have to rework the entire script in order to get the correct info.

Can't assign value to a variable inside of Invoke-Command

It seems to be strange but I can't assign a value to variable inside of Invoke-Command. Here is the code below but when print out $targetComputerPath it's simply empty. What's wrong?
foreach ($item in $computersPath){
$computername = $item.Name
$username = $item.UserID
Write-Host computer $computername and user $username
if (Test-Connection -ComputerName $computername -Count 1 -ErrorAction SilentlyContinue)
{
if ($((Get-Service WinRM -ComputerName $computername).Status) -eq "stopped")
{
(Get-Service WinRM -ComputerName $computername).Start()
}
Invoke-Command -ComputerName $computername -ScriptBlock {
if ($((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion").ReleaseId) -eq "1903" )
{
$targetComputerPath = "\\"+$computername+"\c$\Users\"+$username+"\Desktop\"
write-host "1903"
}
else
{
$targetComputerPath = "\\"+$computername+"\c$\Users\"+$username+"\Desktop\"
write-host "something else"
}
}
}
write-host $targetComputerPath
}
The point of WinRM is that you take a script block, and execute it on a different machine.
None of the variables you define in the host script will be available on the remote machine.
This becomes more apparent when you separate the "task", a.k.a the script block, from the Invoke-Command, like this:
$task = {
$version = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"
if ($version.ReleaseId -eq "1903") {
# note that `$username` cannot be available here, it's never been defined!
return "\\$env:COMPUTERNAME\c$\Users\$username\Desktop"
} else {
return "\\$env:COMPUTERNAME\c$\Users\$username\Desktop"
}
}
foreach ($item in $computersPath) {
$computername = $item.Name
$username = $item.UserID
Write-Host computer $computername and user $username
if (Test-Connection -ComputerName $computername -Count 1 -ErrorAction SilentlyContinue) {
$winrm = Get-Service WinRM -ComputerName $computername
if ($winrm.Status -eq "stopped") { $winrm.Start() }
$targetComputerPath = Invoke-Command -ComputerName $computername -ScriptBlock $task
Write-Host "The machine returned: $targetComputerPath"
}
}
As you can see, you can return values from the script block and they will be available as the return value of Invoke-Command.
If you want to pass arguments to your script block, this thread talks about that: How do I pass named parameters with Invoke-Command?

Options to use different param

I'm looking for a way to to have a choice of a list or a single computername in a foreach loop.
If the user enters in a single computername I want the script to execute for that one computername
but if that user wants to use a path to a list of computers how could I replace $computername with the path that user wants?
function Get-OSInfo {
[CmdletBinding()]
param (
#[Parameter(ValueFromPipeline=$True,
# ValueFromPipelineByPropertyName=$True)]
[string]$computername,
[string]$errorlog = 'c:\errors.txt',
[switch]$logerrors
)
PROCESS {
foreach ($computer in $computername) {
Try {
$os = Get-WmiObject -EA Stop –Class Win32_OperatingSystem –ComputerName $computer
$cs = Get-WmiObject -EA Stop –Class Win32_ComputerSystem –ComputerName $computer
$bios = Get-WmiObject -EA Stop –Class Win32_BIOS –ComputerName $computer
$cpu = Get-WmiObject -EA Stop -class Win32_processor -ComputerName $computer
$props = #{'ComputerName'=$computer;
'OSVersion'=$os.version;
'SPVersion'=$os.servicepackmajorversion;
'OSBuild'=$os.buildnumber;
'OSArchitecture'=$os.osarchitecture;
'Manufacturer'=$cs.manufacturer;
'Model'=$cs.model;
'BIOSSerial'=$bios.serialnumber
'CPU Count'=$CPU.Count
'Memory'= [Math]::round(($cs.TotalPhysicalMemory/1gb),2)
'CPU Speed'= $CPU.MaxClockSpeed[0]}
$obj = New-Object -TypeName PSOBject -Property $props
$obj.PSObject.TypeNames.Insert(0,'Get-OS.OSInfo')
#Write-Output $obj
$obj | Export-Csv c:\test4.csv -Append
} Catch {
if ($logerrors) {
$computer | Out-File $errorlog -append
}
Write-Warning "$computer failed"
}
}
}
}
Change the type of the $ComputerName parameter to a string array instead of just a single string:
param(
[string[]]$ComputerName,
[string]$errorlog = 'c:\errors.txt',
[switch]$logerrors
)
Notice the [] after the type name, this denotes an array of strings, rather than a single string.
Now you can do:
PS C:\> $computers = Get-Content C:\computers.txt
PS C:\> Get-OSInfo -ComputerName $computers
If you'd like to be able to specify a path to a file containing the target computers as the argument to the function, you can use multiple parameter sets:
[CmdletBinding(DefaultParameterSetName='ByName')]
param(
[Parameter(ParameterSetName='ByName',ValueFromPipeline)]
[string[]]$ComputerName,
[Parameter(ParameterSetName='ByFile')]
[string]$InputFile
)
begin {
if($PSCmdlet.ParameterSetName -eq 'ByFile'){
try{
$ComputerName = Get-Content -LiteralPath $InputFile
}
catch{
throw
return
}
}
}
process {
foreach($Computer in $ComputerName){
# Work with $Computer here...
}
}

Error Handling Issue with Try/Catch in Powershell

I have been working on the following example from one of Don Jones' powershell books as part of my personal development and am having some serious trouble getting the try/catch construct to work as it should. As you can see, when the catch block executes, it sets a variable called $everything_ok to $false - which should trigger the else block in the following code. Which it does - the logfile is appended as per my expectations.
However it does not stop the script from ALSO executing the code in the if block and spewing out 'The RPC Server is unavailable' errors when it tries to query the made-up machine 'NOTONLINE' (Exception type is System.Runtime.InteropServices.COMException).
What makes this even stranger is that I went through the script with breakpoints, checking the contents of the $everything_ok variable along the way, and it never contained the wrong value at any point. So why on earth is the if block still executing for 'NOTONLINE' when the condition I have specified ( if ($everything_ok = $true) ) has not been met?
Am I doing something wrong here?
function get-systeminfo {
<#
.SYNOPSIS
Retrieves Key Information on 1-10 Computers
#>
[cmdletbinding()]
param (
[parameter(mandatory=$true,valuefrompipeline=$true,valuefrompipelinebypropertyname=$true,helpmessage="computer name or ip address")]
[validatecount(1,10)]
[validatenotnullorempty()]
[alias('hostname')]
[string[]]$computername,
[string]$errorlog = "C:\retry.txt",
[switch]$logerrors
)
BEGIN {
write-verbose "Error log will be $errorlog"
}
PROCESS {
foreach ($computer in $computername) {
try {$everything_ok = $true
gwmi win32_operatingsystem -computername $computer -ea stop
} catch {
$everything_ok = $false
write-verbose "$computer not Contactable"
}
if ($everything_ok = $true) {
write-verbose "Querying $computer"
$os = gwmi win32_operatingsystem -computername $computer
$cs = gwmi win32_computersystem -computername $computer
$bios = gwmi win32_bios -computername $computer
$props = #{'ComputerName' = $cs.__SERVER;
'OSVersion' = $os.version;
'SPVersion' = $os.servicepackmajorversion;
'BiosSerial' = $bios.serialnumber;
'Manufacturer' = $cs.manufacturer;
'Model' = $cs.model}
write-verbose "WMI Queries Complete"
$obj = new-object -type psobject -property $props
write-output $obj
}
elseif ($everything_ok = $false) {
if ($logerrors) {
"$computer $_" | out-file $errorlog -append
}
}
}
}
END {}
}
get-systeminfo -host localhost, NOTONLINE -verbose -logerrors
The equals sign in Powershell is used as the assignment operation. -eq is used to test for equality. So your if statement is assigning $true to $everything_ok, which then tests true.