Execute a different command depending on the output of the previous - powershell

I am trying out something which is quite simple, yet I can't find an answer or rather can't figure out how to ask the question on Google.
So I thought it would be better to just show what I'm doing with pictures, here.
Here is the script I'm working on:
What it does is simple: get all virtual machines depending on their state (running, saved or off) and then starting them or stopping them. That is where I am having trouble.
I tried to pipe it with different commands, but it keeps giving an error which is
The input object cannot be bound to any parameters for the command either
because the command does not take pipeline input or the input and its properties do not match any of the parameters that take pipeline input.
So what I want is if the machine are running then save them. Is there a way to do so?

Use a ForEach-Object loop and a switch statement:
Get-VM -VMName $name | ForEach-Object {
switch ($_.State) {
'running' {
# do some
}
'saved' {
# do other
}
'off' {
# do something else
}
default {
throw ('Unrecognized state: {0}' -f $_.State)
}
}
}

I think the actual issue here (shown by the error message) is that start-vm doesn't accept pipeline input. I'm guessing this is the Hyper-V Start-VM cmdlet, by the way.
You could do this to get around the lack of pipeline-aware parameters:
Get-VM -VMName $name | where {$_.State -eq $state} | foreach-object {Start-VM -VM $_}

Related

How to capture warnings to a log

I have a script to manage hyper-v virtual machines. When I execute the command to shut down a machine, I need the warning to be saved in the log if it is shut down.
The script I have only captures the errors, I need to get both errors and warnings:
Try {
Stop-VM $Machine -ErrorAction Stop
} Catch {
Write-Host "Error: $_"
Add-Content -Path $Log -Value "`n$_"
}
Thanks!
Assuming that Stop-VM issues non-terminating errors:
Stop-VM $Machine *> output.log
Note: This redirects all of PowerShell's output streams to file output.log, including success output, if any, and it would work with passing an array of VM names in $Machine.
As Abraham Zinala points out, you can selectively capture (some of the) output streams, in variables, using the the common -WarningVariable parameter as well as -ErrorVariable, which you can later send to a file as needed. Note that using these variables still produces the original stream output, but you can silence that with -WarningAction SilentlyContinue and -ErrorAction SilentlyContinue. See the answer linked below for details.
As Santiago Squarzon points out, you could extend your original approach by adding -WarningAction Stop, but the limitation, as
explained in this answer, is that only the first error or warning emitted by the call is then captured, and, perhaps more importantly, the command is terminated at that point - even if multiple VMs were specified.

Using PowerShell to identify a machine as a server or PC

I'm trying to write a PowerShell script that will give me a list if of roles and features if run on a server but if run on a client machine will say "Only able to execute command on a server."
I've played around with this script a lot and can get it to run on either a client machine or server (depending on what I've tweaked) but not both. Here's the latest iteration:
$MyOS="wmic os get Caption"
if("$MyOS -contains *Server*") {
Get-WindowsFeature | Where-Object {$_. installstate -eq "installed"
}}else{
echo "Only able to execute command on a server."}
What am I doing wrong?
The quotes around your wmic command will create the $MyOS variable with a String and not execute the command. Still, I would recommend you use native PowerShell commands such as Get-CimInstance. Like the $MyOS variable your if statement condition will always equal true as the quotes will make it a String.
$MyOS = Get-CimInstance Win32_OperatingSystem
if ($MyOS.Caption -like "*Server*") {
Get-WindowsFeature | Where-Object { $_. installstate -eq "installed" }
}
else {
Write-Output "Only able to execute command on a server."
}
You can also use the ProductType property. This is a (UInt32) number with the following values:
1 - Work Station
2 - Domain Controller
3 - Server
$MyOS = (Get-CimInstance Win32_OperatingSystem).ProductType
if ($MyOS -gt 1) {
Get-WindowsFeature | Where-Object { $_. InstallState -eq "installed" }
}
else {
Write-Output "Only able to execute command on a server."
}
Try to use '-like' instead of 'contains', it should work
Generally, I try to avoid pre-checks like this that make assumptions about functionality that may not be true forever. There's no guarantee that Get-WindowsFeature won't start working on client OSes in a future update.
I prefer to just trap errors and proceed accordingly. Unfortunately, this particular command produces a generic Exception rather than a more specifically typed exception. So you can't really do much other than string matching on the error message to verify specifically what happened. But there's very little that can go wrong with this command other than the client OS error. So it's pretty safe to just assume what went wrong if it throws the exception.
try {
Get-WindowsFeature | Where-Object { $_. InstallState -eq "installed" }
} catch {
Write-Warning "Only able to execute command on a server."
}
If you don't want to accidentally hide an error that's not the client OS one, change the warning message to just use the actual text from the error. This also gets you free localization if you happen to be running this code in a location with a different language than your own.
Write-Warning $_.Exception.Message

Restrict multiple executions of the same script

I have tried to restrict multiple executions of the same script in PowerShell. I have tried following code. Now it is working, but a major drawback is that when I close the PowerShell window and try to run the same script again, it will execute once again.
Code:
$history = Get-History
Write-Host "history=" $history.Length
if ($history.Length -gt 0) {
Write-Host "this script already run using History"
return
} else {
Write-Host "First time using history"
}
How can I avoid this drawback?
I presume you want to make sure that a script is not running from different powershell processes, and not from the same one as some sort of self-call.
In either case there isn't anything in powershell for this, so you need to mimic a semaphore.
For the same process, you can leverage a global variable and wrap your script around a try/finally block
$variableName="Something unique"
try
{
if(Get-Variable -Name $variableName -Scope Global -ErrorAction SilentlyContinue)
{
Write-Warning "Script is already executing"
return
}
else
{
Set-Variable -Name $variableName -Value 1 -Scope Global
}
# The rest of the script
}
finally
{
Remove-Variable -Name $variableName -ErrorAction SilentlyContinue
}
Now if you want to do the same, then you need to store something outside of your process. A file would be a good idea with a similar mindset using Test-Path, New-Item and Remove-Item.
In either case, please note that this trick that mimics semaphores, is not as rigid as an actual semaphore and can leak.

Stop a process running longer than an hour

I posted a question a couple ago, I needed a powershell script that would start a service if it was stopped, stop the process if running longer than an hour then start it again, and if running less than an hour do nothing. I was given a great script that really helped, but I'm trying to convert it to a "process". I have the following code (below) but am getting the following error
Error
"cmdlet Start-Process at command pipeline position 3
Supply values for the following parameters:
FilePath: "
Powershell
# for debugging
$PSDefaultParameterValues['*Process:Verbose'] = $true
$str = Get-Process -Name "Chrome"
if ($str.Status -eq 'stopped') {
$str | Start-Process
} elseif ($str.StartTime -lt (Get-Date).AddHours(-1)) {
$str | Stop-Process -PassThru | Start-Process
} else {
'Chrome is running and StartTime is within the past hour!'
}
# other logic goes here
Your $str is storing a list of all processes with the name "Chrome", so I imagine you want a single process. You'll need to specify an ID in Get-Process or use $str[0] to single out a specific process in the list.
When you store a single process in $str, if you try to print your $str.Status, you'll see that it would output nothing, because Status isn't a property of a process. A process is either running or it doesn't exist. That said, you may want to have your logic instead check if it can find the process and then start the process if it can't, in which case it needs the path to the executable to start the process. More info with examples can be found here: https://technet.microsoft.com/en-us/library/41a7e43c-9bb3-4dc2-8b0c-f6c32962e72c?f=255&MSPPError=-2147217396
If you're using Powershell ISE, try storing the process in a variable in the terminal, type the variable with a dot afterwards, and Intellisense (if it's on) should give a list of all its available properties.

Powershell Remoting Speeding up a Foreach Loop Hosted Exchange

I have a CSV of email adddresses and Departments that I need to set on Live#edu. The command I currently have looks something like this:
Import-CSV departments.csv | ForEach-Object { Set-User $_.EmailAddress $_.Department }`
The problem is, this operation takes FOREVER.
My first thought is that it would be great to have the ForEach-Object command actually be forwarded over to the remote machine, so that it will only need to create the one pipeline between the two machines, but when I go into the PSSession, there doesn't seem to be any foreach-object available. For reference, How I Import the PSSession is:
Import-PSSession(New-PSSession -ConfigurationName Microsoft.Exchange `
-ConnectionUri 'https://ps.outlook.com/powershell' `
-Credential (Get-Credential) `
-Authentication Basic -AllowRedirection)
Is there a better way that I can import the session to allow ForEach-Object to be remote, or to import an aliased version of the remote foreach-object, perhaps as ForEach-Object-Remote, or perhaps does anybody have something better to suggest to streamline this process?
UPDATE:
A Couple Things I've tried:
Using the -AsJob switch on the implicitly remoted command.
Import-CSV departments.csv | ForEach-Object { Set-User $_.EmailAddress $_.Department -AsJob }
This, unfortunately, doesn't work because there are throttling limits in place that don't allow the additional connections. Worse than that, I don't even know that anything went wrong until I check the results, and find that very few of them actually got changed.
Importing the ForEach-Object under a different name.
Turns out that adding a prefix is easy as putting -Prefix RS in the Import-PSSession Command to have things like the ForEach-Object from the Remote Session become ForEach-RSObject in the local session. Unfortunately, this won't work for me, because the server I'm connecting to does not does not have the Microsoft.Powershell ConfigurationName available to me.
UPDATE 2: The Set-User cmdlet seems to be Microsoft provided for Live#edu administration. Its purpose is to set User attributes. It is not a script or cmdlet that I am able to debug. It doesn't take pipeline input, unfortunately, so that would not be able to fix the issue.
As Far as I can tell, the problem is that it has to construct and tear down a pipeline to the remote machine every time this command runs, rather than being able to reuse it. The remote ForEach idea would have allowed me to offload that loop to avoid having to create all those remote pipelines, while the -asJob would have allowed them to all run in parallel. However, it also caused errors to fail silently, and only a few of the records actually get properly updated.
I suspect at this point that I will not be able to speed up this command, but will have to do whatever I can to limit the amount of data that needs to be changed in a particular run by keeping better track of what I have done before (keeping differential snapshots). Makes the job a bit harder.
EDIT: Start-Automate left a very useful help, unfortunately, neither of them work. It is my feeling at this point that I won't find a way to speed this up until my provider gives access to more powershell cmdlets, or the exchange cmdlets are modified to allow multiple pipelines, neither of which I expect to happen any time soon. I am marking his answer as correct, despite the ultimate result that nothing helps significantly. Thanks, Start-Automate.
You can speed up your script and also avoid trying to make two connections to the server by the use of the foreach statement, instead of Foreach-Object.
$departments = #(import-csv .\departments.csv)
foreach ($user in $departments) {
Set-User $user.EmailAddress $user.Department
}
If you need to batch, you could use the for statement, moving forward in each batch
for ($i =0; $i -lt $departments.Count; $i+=3) {
$jobs = #()
$jobs+= Invoke-Command { Set-User $departments[$i].EmailAddress $departments[$i].Department } -AsJob
$jobs+= Invoke-Command { Set-User $departments[$i + 1].EmailAddress $departments[$i + 1].Department } -AsJob
$jobs+= Invoke-Command { Set-User $departments[$i + 2].EmailAddress $departments[$i + 2].Department } -AsJob
$jobs | Wait-job | Receive-job
}
Hope this helps