Including common code in header/footer with PowerShell - powershell

I have common functions and formats to most of my scripts. Each script brings up a window for me to paste workstations and it performs basic tasks like checking connectivity before proceeding. Generally, I copy and paste this code and modify the body. What I would like to do is include a header and footer, but I get "Missing closing '}' in statement block." errors. Example:
<# Begin Header #>
if($canceled) {
write-host "Operation canceled."
}
else {
if($computers.length -gt 0) {
[array]$computers = $computers.split("`n").trim()
# Loop through computers entered
foreach($pc in $computers) {
# Skip zero length lines for computers
if(($pc.length -eq $null) -OR ($pc.length -lt 1)) {
continue
}
else {
# Try to connect to the computer, otherwise error and continue
write-host "Connecting to: $pc$hr"
if(test-connection -computername $pc -count 1 -ea 0) {
<# End Header #>
Body of script
<# Begin Footer #>
}
else {
utc # Unable to contact
}
}
write-host "`n"
}
}
}
<# End Footer #>
Rather than copying/pasting each time, I would prefer to do this...
."c:\scripts\header.ps1"
-- code --
."c:\scripts\footer.ps1"
Is that even possible when the header ends with an open bracket? I do this in PHP but I can't figure out a work-around in PowerShell.

Your approach could be changed into storing a function in one file and your custom script that runs for-each server in another. You can store a scriptblock to a variable in PowerShell and pass that as a parameter to a function. You can use Invoke-Command -scriptblock $Variable to execute that code.
Write your function like this:
function runAgainstServerList {
param ( [ScriptBlock]$ScriptBlock)
if($canceled) {
write-host "Operation canceled."
}
else {
if($computers.length -gt 0) {
[array]$computers = $computers.split("`n").trim()
# Loop through computers entered
foreach($pc in $computers) {
# Skip zero length lines for computers
if(($pc.length -eq $null) -OR ($pc.length -lt 1)) {
continue
}
else {
# Try to connect to the computer, otherwise error and continue
write-host "Connecting to: $pc$hr"
if(test-connection -computername $pc -count 1 -ea 0) {
Invoke-Command -ScriptBlock $ScriptBlock
}
else {
utc # Unable to contact
}
}
write-host "`n"
}
}
}
}
Now save that off to your include file like 'myFunctions.ps1'
Then create your custom script that you want to run per server like this:
. myFunctions.ps1
[ScriptBlock]$ScriptBlockToPass = {
## Insert custom code here
}
runAgainstServerList $ScriptBlockToPass
To get you a step closer to what might be your end goal, You may want to append the -ComputerName "ComputerNameHere" argument to your invoke-command statement inside your included function. This would cause your script to be executed on the remote system instead of locally.

Related

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

Powersell - Remotely Query if User Exists Across Domain [Fastest]

Abstract
So I work for a company that has roughly 10k computer assets on my domain. My issue is the time it takes to query if a user exists on a computer to see if they've ever logged into said computer. We need this functionality for audits in case they've done something they shouldn't have.
I have two methods in mind I've researched to complete this task, and a third alternative solution I have not thought of;
-Method A: Querying every computer for the "C:\Users<USER>" to see if LocalPath exists
-Method B: Checking every computer registry for the "HKU:<SID>" to see if the SID exists
-Method C: You are all smarter than me and have a better way? XD
Method A Function
$AllCompFound = #()
$AllADComputer = Get-ADComputer -Properties Name -SearchBase "WhatsItToYa" -filter 'Name -like "*"' | Select-Object Name
ForEach($Computer in $AllADComputers) {
$CName = $Computer.Name
if (Get-CimInstance -ComputerName "$CName" -ClassName Win32_Profile | ? {"C:\Users\'$EDIPI'" -contains $_.LocalPath}) {
$AllCompFound += $CName
} else {
#DOOTHERSTUFF
}
}
NOTE: I have another function that prompts me to enter a username to check for. Where I work they are numbers so case sensitivity is not an issue. My issue with this function is I believe it is the 'if' statement returns true every time because it ran rather than because it matched the username.
Method B Function
$AllCompFound = #()
$AllADComputer = Get-ADComputer -Properties Name -SearchBase "WhatsItToYa" -filter 'Name -like "*"' | Select-Object Name
$hive = [Microsoft:Win32.RegistryHive]::Users
ForEach($Computer in $AllADComputers) {
try {
$base = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($hive, $Computer.Name)
$key = &base.OpenSubKey($strSID)
if ($!key) {
#DOSTUFF
} else {
$AllCompFound += $Computer.Name
#DOOTHERSTUFF
}
} catch {
#IDONTTHROWBECAUSEIWANTITTOCONTINUE
} finally {
if($key) {
$key.Close()
}
if ($base) {
$base.Close()
}
}
}
NOTE: I have another function that converts the username into a SID prior to this function. It works.
Where my eyes start to glaze over is using Invoke-Command and actually return a value back, and whether or not to run all of these queries as their own PS-Session or not. My Method A returns false positives and my Method B seems to hang up on some computers.
Neither of these methods are really fast enough to get through 10k results, I've been using smaller pools of computers in order to get test these results when requested. I'm by no means an expert, but I think I have a good understanding, so any help is appreciated!
First, use WMI Win32_UserProfile, not C:\Users or registry.
Second, use reports from pc to some database, not from server to pc. This is much better usually.
About GPO: If you get access, you can Add\Remove scheduled task for such reports through GPP (not GPO) from time to time.
Third: Use PoshRSJob to make parallel queries.
Get-WmiObject -Class 'Win32_USerProfile' |
Select #(
'SID',
#{
Name = 'LastUseTime';
Expression = {$_.ConvertToDateTime($_.LastUseTime)}}
#{
Name = 'NTAccount';
Expression = { [System.Security.Principal.SecurityIdentifier]::new($_.SID).Translate([System.Security.Principal.NTAccount])}}
)
Be careful with translating to NTAccount: if SID does not translates, it will cause error, so, maybe, it's better not to collect NTAccount from user space.
If you have no other variants, parallel jobs using PoshRSJob
Example for paralleling ( maybe there are some typos )
$ToDo = [System.Collections.Concurrent.ConcurrentQueue[string]]::new() # This is Queue (list) of computers that SHOULD be processed
<# Some loop through your computers #>
<#...#> $ToDo.Enqueue($computerName)
<#LoopEnd#>
$result = [System.Collections.Concurrent.ConcurrentBag[Object]]::new() # This is Bag (list) of processing results
# This function has ComputerName on input, and outputs some single value (object) as a result of processing this computer
Function Get-MySpecialComputerStats
{
Param(
[String]$ComputerName
)
<#Some magic#>
# Here we make KSCustomObject form Hashtable. This is result object
return [PSCustomObject]#{
ComputerName = $ComputerName;
Result = 'OK'
SomeAdditionalInfo1 = 'whateverYouWant'
SomeAdditionalInfo2 = 42 # Because 42
}
}
# This is script that runs on background. It can not output anything.
# It takes 2 args: 1st is Input queue, 2nd is output queue
$JobScript = [scriptblock]{
$inQueue = [System.Collections.Concurrent.ConcurrentQueue[string]]$args[0]
$outBag = [System.Collections.Concurrent.ConcurrentBag[Object]]$args[1]
$compName = $null
# Logging inside, if you need it
$log = [System.Text.StringBuilder]::new()
# we work until inQueue is empty ( then TryDequeue will return false )
while($inQueue.TryDequeue([ref] $compName) -eq $true)
{
$r= $null
try
{
$r = Get-MySpecialComputerStats -ComputerName $compName -EA Stop
[void]$log.AppendLine("[_]: $($compName) : OK!")
[void]$outBag.Add($r) # We append result to outBag
}
catch
{
[void]$log.AppendLine("[E]: $($compName) : $($_.Exception.Message)")
}
}
# we return log.
return $log.ToString()
}
# Some progress counters
$i_max = $ToDo.Count
$i_cur = $i_max
# We start 20 jobs. Dont forget to say about our functions be available inside job
$jobs = #(1..20) <# Run 20 threads #> | % { Start-RSJob -ScriptBlock $JobScript -ArgumentList #($ToDo, $result) -FunctionsToImport 'Get-MySpecialComputerStats' }
# And once per 3 seconds we check, how much entries left in Queue ($todo)
while ($i_cur -gt 0)
{
Write-Progress -Activity 'Working' -Status "$($i_cur) left of $($i_max) computers" -PercentComplete (100 - ($i_cur / $i_max * 100))
Start-Sleep -Seconds 3
$i_cur = $ToDo.Count
}
# When there is zero, we shall wait for jobs to complete last items and return logs, and we collect logs
$logs = $jobs | % { Wait-RSJob -Job $_ } | % { Receive-RSJob -Job $_ }
# Logs is LOGS, not result
# Result is in the result variable.
$result | Export-Clixml -Path 'P:/ath/to/file.clixml' # Exporting result to CliXML file, or whatever you want
Please be careful: there is no output inside $JobScript done, so it must be perfectly done, and function Get-MySpecialComputerStats must be tested on unusual ways to return value that can be interpreted.

value fed by Pipeline versus Argument

I have a simple Powershell script that I want to accept string values via Pipeline/Argument or plain just run the script.
#myscript
[CmdletBinding()]
Param(
[Parameter(ValueFromPipelineByPropertyName,ValueFromPipeline)][string[]]$dnshostname
)
Begin {
If(-Not $dnshostname){
Write-Host "I AM HERE"
$raw = Get-ADComputer -Filter * -Property *
$raw | ForEach {
If (Test-Connection -Delay 15 -ComputerName $_.IPv4Address -Count 1 -ErrorAction SilentlyContinue){
$dnshostname += $_.DNSHostname
}
}
}
}
Process {
...
...
}
When I run this script like this: "myserver" | ./myscript.ps1
I get the "I AM HERE" message which means the $dnshostname variable is empty, but wasn't it fed from the pipeline?
If I run the same script as: ./myscript.ps1 -dnshostname "myserver", I do not get the "I AM HERE" message which mean it correctly determined that the $dnshostname variable is not empty.
I am sure I am missing something very fundamental here. May i get some guidance pretty please as to why the value passed via the pipeline triggers my IF statement?
Thank you!

Why aren't error thrown when I run this function as domain administrator?

This script is intended to recurse through a series of directories and when an error of type DirUnauthorizedAccessError,Microsoft or PowerShell.Commands.GetChildItemCommand is thrown it's supposed to call another function Take-Ownership which takes ownership of the directory and adds full permissions for the localAdmin and domain admin to the folder. (It's really a script used for easing the deletion of old user profiles):
function Test-Folder($FolderToTest, $localAdminName) {
# Remeber the old error preference...
$old_ErrorActionPreference = $ErrorActionPreference
$ErrorActionPreference = 'SilentlyContinue'
$error.Clear()
# Go through the directories...and capture errors in $error
Get-ChildItem $FolderToTest -Recurse -ErrorAction SilentlyContinue -ErrorVariable errz | Select FullName
Write-Host $errz.count
if ($errz.Count -eq 0) {
Write-Host "blah no errors"
foreach ($err in $errz) {
Write-Host "Error: $err"
if ($err.FullyQualifiedErrorId -eq "DirUnauthorizedAccessError,Microsoft.PowerShell.Commands.GetChildItemCommand") {
Write-Host Unable to access $err.TargetObject -Fore Red
Write-Host Attempting to take ownership of $err.TargetObject -Fore Yellow
Take-Ownership -Folder $err.TargetObject, -LocalAdminName $localAdminName
Test-Folder -FolderToTest $err.TargetObject -localAdminName $localAdminName
}
}
}
$ErrorActionPreference = $old_ErrorActionPreference
}
Unfortunately, it doesn't throw any errors when I run it as domain administrator. I've found a list of ErrorActionPreferences here, but the errors just seem to get ignored, and it outputs blah no errors What can I do to make sure I receive errors and that my Take-Ownership function is actually called?
Your code only enters the if block if $errz.Count is 0. With a count of 0 there are no elements in $errz, so there's nothing to do for the foreach loop.
Add an else branch to the conditional, move the foreach loop there, and the code should do what you want.
if ($errz.Count -eq 0) {
Write-Host "blah no errors"
} else {
foreach ($err in $errz) {
Write-Host "Error: $err"
...
}
}

Powershell script foreach ($x in $y) not working correctly

Just writing some basic scripts after avoiding PowerShell for too long and I'm getting kinda stuck here. Trying to pass multiple parameters to a single "argument" in a function but it will only ever return the first value in the string
#--------------------------------
# Returns logged on user of machine(s) given in parameter
#--------------------------------
function Get-UserLogged{
param($computerName)
foreach ($computer in $computerName) {
return (query user /server:$computerName)
}
}
Now if i call the function and run for example...
Get-UserLogged Server01 Server02 Server03
It will only return the output for server 1, so it seems that the foreach isn't working as expected
Different script same issue:
function p{
param($computerName)
foreach ($computer in $computerName) {
return (test-connection $computer -Count 1)
}
}
if I run:
p localhost, google.com, bbc.com
it will only attempt localhost
You're referencing the array $computerName inside the loop, not the "current item", $computer.
function Get-UserLogged{
param($computerName)
foreach ($computer in $computerName) {
query user /server:$computer
}
}
To pass an array of strings to a parameter, use a comma to separate the items:
Get-UserLogged -computerName Server01, Server02, Server03