As part of a writing a decommission server PowerShell script at work, I had a requirement for a quick and easy SSH function to connect to our NetBackup server at work and remove the server from the backup system (Symantec if you’re reading this, please can we have a PowerShell module? *wishful thinking*…). Not quite needing the entire functionality provided by this module (based on the SSH.NET Library), I came across a function on PS Fab. One fantastic thing about this function is that supports the automatic acceptance of an SSH key when you connect to a host for the first time.
I made a few changes to the code, amended comments that referenced plist rather than plink and put it into a more standard function form with examples (that you might be able to add to an existing library). This is the result:
Function Invoke-SSH
{
<#
.SYNOPSIS
Uses Plink.exe to SSH to a host and execute a list of commands.
.DESCRIPTION
Uses Plink.exe to SSH to a host and execute a list of commands.
.PARAMETER hostname
The host you wish to connect to.
.PARAMETER username
Username to connect with.
.PARAMETER password
Password for the specified user account.
.PARAMETER commandArray
A single, or list of commands stored in an array object.
.PARAMETER plinkAndPath
The location of the plink.exe including the executable (e.g. F:\tools\plink.exe)
.PARAMETER connectOnceToAcceptHostKey
If set to true, it will accept the remote host key (use when connecting for the first time)
.EXAMPLE
Invoke-SSH -username root -hostname centos-server -password Abzy4321! -plinkAndPath "F:\tools\plink.exe" -commandArray $commands -connectOnceToAcceptHostKey $true
.EXAMPLE
Invoke-SSH -username root -hostname centos-server -password Abzy4321! -plinkAndPath "F:\tools\plink.exe" -commandArray ifconfig -connectOnceToAcceptHostKey $true
.NOTES
Author: Robin Malik
Source: Modified from: http://www.zerrouki.com/invoke-ssh/
#>
Param(
[Parameter(Mandatory=$true,HelpMessage="Enter a host to connect to.")]
[string]
$hostname,
[Parameter(Mandatory=$true,HelpMessage="Enter a username.")]
[string]
$username,
[Parameter(Mandatory=$true,HelpMessage="Enter the password.")]
[string]
$password,
[Parameter(Mandatory=$true,HelpMessage="Provide a command or comma separated list of commands")]
[array]
$commandArray,
[Parameter(Mandatory=$true,HelpMessage="Path to plink (e.g. F:\tools\plink.exe).")]
[string]
$plinkAndPath,
[Parameter(HelpMessage="Accept host key if connecting for the first time (the default is `$false)")]
[string]
$connectOnceToAcceptHostKey = $false
)
$target = $username + '@' + $hostname
$plinkoptions = "-ssh $target -pw $password"
# On first connect to a host, plink will prompt you to accept the remote host key.
# This section will login and accept the host key then logout:
if($ConnectOnceToAcceptHostKey)
{
$plinkCommand = [string]::Format('echo y | & "{0}" {1} exit', $plinkAndPath, $plinkoptions )
$msg = Invoke-Expression $plinkCommand
}
# Build the SSH Command by looping through the passed value(s). Append exit in order to logout:
$commandArray += "exit"
$commandArray | % { $remoteCommand += [string]::Format('{0}; ', $_) }
# Format the command to pass to plink:
$plinkCommand = [string]::Format('& "{0}" {1} "{2}"', $plinkAndPath, $plinkoptions , $remoteCommand)
# Execute the command and display the output:
$msg = Invoke-Expression $plinkCommand
Write-Output $msg
}
Copy and paste this function into a PowerShell window, and then test it with the below code (changing where appropriate of course):
$plinkAndPath = "F:\tools\plink\plink.exe" $username = "root" $password = "Adk3453#5341!" $hostname = "centos-server" # Commands to execute: $Commands = @() $Commands += "ifconfig" $Commands += "ls" Invoke-SSH -username $username -hostname $hostname -password $password -plinkAndPath $plinkAndPath -commandArray $Commands -connectOnceToAcceptHostKey $true
As part of our VM-template, template-VM conversion process at work (which I’ve automated via a webpage as discussed in my previous post “Executing Powershell using PHP and IIS“), I had to find a way to handle the VM question “This VM has questions that must be answered before the operation can continue” when attempting to power the VM on via Start-VM.
I found that handling the question was not possible using a simple try/catch block (the explanation can be found in this excellent post by Clint Bergman). I was however able to catch it using the following block:
try
{
try
{
Start-VM -VM $serverName -ErrorAction Stop -ErrorVariable custErr
}
catch [System.Management.Automation.ActionPreferenceStopException]
{
throw $_.Exception
}
}
catch [VMware.VimAutomation.ViCore.Types.V1.ErrorHandling.VMBlockedByQuestionException]
{
Write-Output "Power on operation triggered a VMBlockedByQuestionException. Answering question with `"I moved it`". <br />"
Get-VMQuestion -VM $serverName | Set-VMQuestion –Option "I moved it" -Confirm:$false
}
It may seem like a lot of work to convert between a VM and template (and vice versa) but in our environment there are a couple of critical steps to the conversation process that must be followed to ensure deploys from these templates don’t break. As there are users outside of the team who update various templates I wanted to provide an easy way for everyone to ensure consistency when doing this, and the best way to do this is of course, automation :)
In order to install cross_cisco-vem-v140-4.2.1.1.5.1.0-3.0.1.vib to our vCenter 5 / ESXi 5 / Cisco 1000V environment we attempted to add the following VEM to VMware Update Manager (VUM): VEM500-201201140102-BG-release.zip (extracted from Nexus1000v.4.2.1.SV1.5.1.zip). Unfortunately on first attempt we received an error along the lines of “invalid vendor code CSCO in patch metadata, another vendor code with different code already exists…” (I didn’t save the error and only googled it in partiality so I can’t copy it here I’m afraid). In short, it stopped us from importing the patch.
This was solved by extracting the VEM500-201201140102-BG-release.zip, opening index.xml (wordpad is suitable) and modifying the code XML node from CSCO to csco. We then rezipped the folder contents and imported it successfully.
Thanks to http://www.thatcouldbeaproblem.com/?p=341 for the nod in the right direction :)
At work I’ve been working on a website to collate various bits of reporting information about our infrastructure. I wanted one of these reports to be selected eventlog entries of our servers, split up by WSUS patch phases (i.e. one report per AD security group). The idea behind this was that we could arrive at work in the morning knowing that Phase X had patched overnight and take a look at a jQuery sortable HTML table showing any issues with servers in that phase/group. If this was quick enough, we could even execute it live against a single server via another website I’ve been working on (I’ll post about that later). I also want to say that while have a Solarwinds monitoring solution (APM) and their Windows based log forwarder application, the forwarded eventlogs are sent to a SQL database as syslog messages which simply don’t have the same level of detail as a Powershell event object. Anyway back on topic.
Two approaches sprang to mind:
- Invoke-Command by way of WinRM
- The remote capabilities of either Get-WinEvent or Get-EventLog
Rolling out WinRM is an ongoing project and so the latter it was. A read around online and Get-WinEvent was touted a quickest especially when querying remote computers so I started with that. I constructed my query to retrieve any errors or critical messages in the application eventlog since 4am which is the time of patching:
$computerName = "remoteserver"
# Create a timestamp after which to retrieve events. This should be from 4am on the current day:
$currDatetime = Get-Date
$day = $currDatetime.Day
$month = $currDatetime.Month
$year = $currDatetime.Year
$patchDateTime = New-Object -TypeName System.DateTime($year,$month,$day,04,00,00)
$appLog = Get-WinEvent -LogName "Application" -ComputerName $computerName -ErrorAction SilentlyContinue | Where-Object { ( ($_.LevelDisplayName -eq "Error" -or $_.LevelDisplayName -eq "Critical") -and ($_.TimeCreated -ge $patchDateTime) ) } | Select-Object TimeCreated,LogName,ProviderName,Id,LevelDisplayName,Message
It took forever to run. Ok not quite, but it took 11 minutes! Very slow.
“Back to basics” I thought. Let’s test the execution time of a simpler query on my local machine:
$yesterday = (get-date) - (new-timespan -day 1)
Measure-Command -Expression { Get-WinEventLog -LogName "Application" -ErrorAction SilentlyContinue | Where-Object { ($_.TimeCreated -ge $yesterday) -and ($_.LevelDisplayName -eq "Error") } }
22 seconds for 1 result (out of 104 records in the last 24 hours). Hmmm.
How about against the original server?:
Measure-Command -Expression { Get-WinEventLog -LogName "Application" -ErrorAction SilentlyContinue | Where-Object { ($_.TimeCreated -ge $yesterday) -and ($_.LevelDisplayName -eq "Error") } }
330 seconds (5.5 minutes) for 2 results (out of 128 records in the last 24 hours). Very disappointing. We have 140 servers and querying them all was not going to happen using this approach and standard sequential / synchronous processing. While it would be possible to save time by using asynchronous processing (background jobs or multiple threads) I was certain that this command should be orders of magnitude faster.
I did a little more reading and discovered the -FilterHashTable parameter of the Get-WinEvent cmdlet. This filters the objects while being retrieved on the server, rather than retrieving all the objects and then filtering as happens with Where-Object.
Get-Help Get-WinEvent -Parameter FilterHashTable
showed the key:value pairs accepted by the parameter. The user friendly “LevelDisplayName” key was not one of these, but luckily “Levels” of events (error,warning,information etc.) are also given the “Level” property which is accepted by FilterHashTable. “Error” = Level 2. If you want to know how to see this sort of information the easiest way is to double click an eventlog entry, click the “details” tab and then select XML view.
A new, equivalent query was born using this new method and executed against my local computer:
Measure-Command -Expression { Get-WinEvent -FilterHashTable @{LogName='Application'; Level=2; StartTime=$yesterday} -ErrorAction SilentlyContinue }
0.09 seconds! Some 244x faster. I stripped off the Measure-Command to find it was indeed pulling back the same single error record as previously (without -FilterHashtable). Fantastic!
Now against the remote server:
Measure-Command -Expression { Get-WinEvent -FilterHashTable @{LogName='Application'; Level=2; StartTime=$yesterday} -ErrorAction SilentlyContinue -ComputerName $remoteserver }
0.43 seconds.
So back to my original command. As we’re not allowed duplicate keys in a hash table two queries were needed:
$appErrors = Get-WinEvent -FilterHashTable @{LogName='Application'; Level=2; StartTime=$yesterday} -ErrorAction SilentlyContinue -ComputerName $remoteserver | Select-Object TimeCreated,LogName,ProviderName,Id,LevelDisplayName,Message
$appCritical = Get-WinEvent -FilterHashTable @{LogName='Application'; Level=1; StartTime=$yesterday} -ErrorAction SilentlyContinue -ComputerName $remoteserver | Select-Object TimeCreated,LogName,ProviderName,Id,LevelDisplayName,Message
# Combine and sort the arrays
$appCombined = $appErrors + $appCritical | Sort TimeCreated
1.85 seconds. Outstanding :)
I then went on my merry way about creating a fully fledged script. Ultimately it worked very well and against a batch of 20 servers took 3.89 minutes with some 18,000 records returned, although it did complain with many “There are no more endpoints available from the endpoint mapper.” error messages which I noticed were against our 2003 servers. It appears that Get-WinEvent doesn’t work against 2003 but at least from a 2008R2 server it happily works against 2008+ with -FilterHashTable. I may have to construct a different, equivalent Get-EventLog query purely for the 2003 servers.