Plink, Cisco and PowerShell walk into a bar…wait…I just did that gag.

I've been banging my head against this one for many days. PowerShell automation of Cisco switches through SSH. Easy, right? Not so much. A lot of command line based SSH clients are very old and don't have a scripting language like PowerShell in mind. More modern ones, or SSH cmdlets made for PowerShell, are expensive for commercial use and I prefer to avoid them when possible. With no native SSH in PowerShell or .NET I needed a good, working solution.

I tried a couple of .NET DLLs, but none of them worked consistently so I tried Plink, the command-line version of the tried and true PuTTy SSH client. While it works well for most SSH connections there is a well documented issue with Cisco IOS. Well documented online at least. To sum up the issue, when you use -batch to feed Plink.exe a batch file against Cisco IOS it doesn't work. You usually get an "invalid autocommand" error or something like "only only command per line" if you try to be sneaky and fit it all on one line.

It turns out there is a workaround for this by using StdIn (<) to input the text line by line.  Just one problem. PowerShell doesn't support StdIn redirection.

The article below mentions the workaround  for command prompt in the last comment of the blog post: (link down)

This is where it gets messy. In order for Plink.exe to work with Cisco IOS in a PowerShell script you have to call CMD.exe from PowerShell to do a redirect input to Plink.exe, then use a StdOut (>) to redirect the output to a text file that PowerShell can read to validate the success or failure of the command. My head hurts just writing that.

  1. function invoke-SshFileIOS {  
  2.     param ([string]$username = (throw “A username must be specified”),  
  3.                  $password = (throw “A password must be specified”),  
  4.                  [string]$sshHost = (throw “The hostname or IP of the SSH host must be provided.”),  
  5.                  $filePath = (throw “A full file path must be specified.”),  
  6.                 [switch]$Delete = $false,  
  7.                 [switch]$noOutput = $false)  
  9.     # validates the filePath variable.        
  10.     if ($filePath.startsWith(‘.’)) {  
  11.         $fileName = $filePath.TrimStart(“.”)  
  12.         $filePath = $pwd.path.toString() + $fileName  
  13.     }  
  14.     # First checks if the path is valid with a test-path call.  
  15.     # Next it makes sure an actual file is passed and not a directory by using get-item and checking the PSIsContainer property.  
  16.     if (!(test-path $filePath)) {  
  17.         write-error “$filePath is invalid or was not found.”  
  18.         return $false  
  19.     } elseif ((gi $filepath).PSIsContainer) {  
  20.         write-error “You must provide the full path to a file, not a directory.”  
  21.         return $false  
  22.     }  
  24.     # generate logpath based on filepath  
  25.     $logPath = split-path $filepath -parent  
  27.     # if the password variable is a secure string decrypt it to clear text, as SSH doesn’t like securestring  
  28.     if ($password -is [System.Security.SecureString]) {  
  29.         # backup the raw password so the clear text password can be deleted  
  30.         $passwordRAW = $password  
  31.         $BSTR = [System.Runtime.InteropServices.marshal]::SecureStringToBSTR($password)  
  32.         $password = [System.Runtime.InteropServices.marshal]::PtrToStringAuto($BSTR)  
  33.         [System.Runtime.InteropServices.marshal]::ZeroFreeBSTR($BSTR)  
  34.     }  
  36.     # setting up the plink command in these two steps: 1. the cmd.exe call, 2. the command in cmd as an argument  
  37.     $install_cmd = “cmd.exe”  
  38.     $install_args = “/c `”$PlinkPath -ssh -2 -l $username -pw $password $SshHost -batch < $filePath > $logPath\$SshHost`.txt`“”  
  39.     #Run command and wait for exit  
  40.     $PlinkCMD = [System.Diagnostics.Process]::Start(“$install_cmd”,“$install_args”)  
  41.     $PlinkCMD.WaitForExit()  
  42.     # grab the commnd output  
  43.     $Output = get-content “$logPath\$SshHost`.txt”  
  45.     # delete the clear text password  
  46.     if ($passwordRAW -is [System.Security.SecureString]) {  
  47.         remove-variable password  
  48.     }  
  50.     # delete the file if -delete has been specified  
  51.     if ($Delete) {  
  52.         remove-item $filePath -force  
  53.     }  
  55.     # if -noOutput was passed then nothing is returned, otherwise the output variable is sent back  
  56.     if (!$noOutput) {  
  57.         return $output  
  58.     } else {  
  59.         return $true  
  60.     }  

This is the function I created to do all the Plink work for Cisco IOS. Beyond the parameters you need to generate a $PlinkPath variable, or you need to replace $PlinkPath with a static path. Most of the function is just data validation, which I tend to do a lot of. The meat of the code is just five lines and starts with $install_cmd.

$install_args is where the actual plink.exe command is setup, then a System.Diagnosis.Process is used to start cmd.exe and run Plink.exe. I guess I could of use start-process, but I recycled some old PowerShell 1.0 code for this part. The output is collected via a simple get-content call and you’re basically done.

WARNING!!! When generating batch files for plink.exe using invoke-SshFileIOS you need to add an extra ‘exit’ command or two. Plink cannot, and will not, end properly when using StdIn if you do not have the appropriate number of exit commands. From the root command line I use two exits. From “conf t” I use three exits. From “interface” I use four exits, and so on. If your script hangs on this function call this is probably why. Add an extra exit, or a line break and a ‘!’, and that should fix it.

This is kind of an obscure workaround need, but I’ve seen enough people online asking about that I figure it was due a blog post.

Written by James Kehr Employee @ SherWeb