At PowerShell Deep dive conference in April /n software gave away USB keys with a full set of “PowerShell inside” software including their NetCmdlets. It turns out that this is 30 day evaluation software, and initially this put me off using them; Joel wrote about an alternative saying “It’s also possible to do this using the NetCmdlets from /n and I’m constantly surprised at how unwilling people are to pay for them.” Part of the problem is you get nearly 100 cmdlets, and if you want only half a dozen if feels like most of the cost is wasted. After checking a free library and finding it flakey I dug out the USB key, figuring if they worked, or even mostly worked they’d be worth the cost.
I only need to do a couple of things – use SSH to send commands to the Linux server which hosts my application using (for which there are 3 commands: Connect-SSH
, Disconnect-SSH
and Invoke-SSH
) and move files both ways with sftp (which has Connect-SFTP, Disconnect-SFTP, Get-SFTP, Remove-SFTP, Rename-SFTP
and Send-SFTP
) . I wanted to be able to invoke the commands against my server without re-making connections each time , and that meant some simple “wrapper” commands to get things how I wanted them.
Connect-Remote
Sets up a Remote Session (saves the session object as a global variable)
All the following functions can be passed a remote session but use the global variable as a defaultGet-RemoteItem
Get properties one or more remote items (Like Get-Item / Get-ChildItem) – alias rDir Copy-RemoteItem
Copies a remote item to the local computer (like Copy-Item ) –alias rCopy Copy-LocalItem
Copies a local item to the remote computer Remove-RemoteItem
Deletes an item on the remote computer (like Remove-Item) Invoke-RemoteCommand
Runs a command on the remote computer. (Like Invoke-Command)
The two connect- netcmdlets return different types of object which contain the server, credential and various other connection parameters. I can set them up like this
$credential = $Host.ui.PromptForCredential("Login",
"Enter Your Details for $server","$env:userName","")
$Global:ssh = Connect-SSH -Server $server -Credential $credential –Force
I found downloading causes the sftpConnection object to “stick” to the path of downloaded file. Get-, Remove-, Rename- and Send- all allow the server and credential to be passed, so I can pass the SSHConnection object into my SFTP wrappers and use its server and credential properties – even though it is the “wrong” connection type – being for SSH command-lines rather than SFTP file access. I wrote a Connect-Remote function which also stores the connections so I can switch between them more easily. It looks like this:
$Global:AllSSHConnections = @{}
Function Connect-Remote {
Param ([String]$server = $global:server,
[String]$UserName =$env:userName ,
[Switch]$force
)
if ($Global:AllSSHConnections[$server] -and -not $force) {
$Global:ssh = $Global:AllSSHConnections[$server]}
else { $credential = $Host.ui.PromptForCredential("Login","Enter Your Details for $server",$username,"")
# Connect-ssh takes errorAction parameter but it doesn't work. So use try-catch
try {$Global:ssh = Connect-SSH -Server $server -Credential $credential -Force -ErrorVariable SSHErr }
catch {continue}
If ($?) {$remotehost = (invoke-RemoteCommand -Connection $ssh -CmdLine "hostname")[0] -replace "\W*$",""
Add-Member -InputObject $ssh -MemberType "NoteProperty" -Name "HostName" -Value $remoteHost
$Global:AllSSHConnections[$server] = $ssh
$ssh | out-default
}
}
Storing the connection simplifies using the other cmdlets. I have a little more error checking in the real version of the script so I can tell the user if there was simply a password error or something more fundamental at fault.
The first task I wanted to perform was to get a file listing via sftp. I have a couple of easily-solved gripes with the netcmdlets: I’ve already mentioned the sticking sftp connection, the other is that the EntryInfo object which is returned when getting a file listing doesn’t contain the fully qualified path, just the file name – so I sort the entries into order, and add properties for the directory and fully qualified path and used New-TableFormat to give me the start of a formatting XML file to display the files nicely . The function also takes the path, and a connection object – which defaults to the connection I set up before.
Function Get-RemoteItem {
param( [Parameter(ValueFromPipeLine=$true)]
[String]$path= "/*",
$Connection = $Global:ssh
)
process {
if ($Connection.server -and $Connection.Credential ) {
$Directory = $path -replace "/[^/]*$",""
Get-sftp -List $path -Credential $Connection.Credential`
-Server $Connection.Server -force |
Sort-Object -property @{e={-not $_.isdir}}, filename |
Add-Member -MemberType NoteProperty -Name "directory" -Value $directory -PassThru |
Add-Member –MemberType ScriptProperty -Name "Path" `
-Value {$this.directory + "/" + $this.Filename} –PassThru
}
else {write-warning "We don't appear to have a valid connection."}
}
}
The next things to add were copy-RemoteItem and Copy-LocalItem – which just have to call Get-SFTP and Send-SFTP with some pre-set parameters.
Function Copy-RemoteItem {
[CmdletBinding(SupportsShouldProcess=$true)]
param ([Parameter(ValueFromPipeLine=$true, ValueFromPipelineByPropertyName=$true, mandatory=$true)]
[String]$path,
[String]$destination = $PWD,
$Connection = $Global:ssh,
[Switch]$force
)
process { if ($Connection.server -and $Connection.Credential -and $path ) {
#Because we re-make the connection in the Get-Sftp command we can pass an SSH object.
write-verbose "Copying $path from $($Connection.Server) to $destination"
Get-SFTP -RemoteFile $path -LocalFile $destination -Credential $Connection.Credential `
-Server $Connection.Server -Force -Overwrite:$force |
Add-Member -PassThru -MemberType Noteproperty -Name "Destination" -Value $destination
}
Else {Write-warning "We don't seem to have a valid destination and path" }
}
}
Set-Alias -Name rCopy -Value Copy-RemoteItem
Function Copy-LocalItem {
[CmdletBinding(SupportsShouldProcess=$true)]
param ([Parameter(ValueFromPipeLine=$true, ValueFromPipelineByPropertyName=$true, mandatory=$true)]
[String]$path,
[Parameter(Mandatory=$true)][String]$destination ,
$Connection = $Global:ssh,
[Switch]$force
)
process { if ($Connection.server -and $Connection.Credential -and $path ) {
write-verbose "Copying $path to $destination on $($Connection.Server)"
Send-SFTP -LocalFile $path -RemoteFile $destination -Credential $Connection.Credential `
-Server $Connection.Server -Force -Overwrite:$force
}
Else {Write-warning "We don't seem to have a valid destination and path" }
}
}
The last wrapper I needed was one to go round Invoke-SSH – primarily to set default parameters, and return only the response text.
Function Invoke-RemoteCommand {
[CmdletBinding(SupportsShouldProcess=$true)]
param ([parameter(Mandatory=$true , ValueFromPipeLine=$true)][String]$CmdLine,
$Connection = $Global:ssh
)
process { if ($Connection -is [PowerShellInside.NetCmdlets.Commands.SSHConnection]) {
write-verbose "# $cmdline"
Invoke-SSH -Connection $Connection -Command $CmdLine |
Select-Object -ExpandProperty text
}
}
}
Job done … well almost. I’ll talk about a couple of tricks I’ve used in combination with these in future posts.