Response to Marc Clifton - Part 1
Marc,
I didn't want your post/question/issue to hang out there for too long without someone saying something from the community. I'm far from an expert, so bear with me. You have the following questions in your post:
1. How do I instantiate a shell?
There are probably more ways, but i have found/used 2 of them to make things happen in my code with Ssh.Net, and one of them (the first I used) is with a shell. In the above, you are not using a shell, I think. Here is how the docs say:
Private ss As Renci.SshNet.ShellStream
Public Property Client As Renci.SshNet.SshClient
Private _shStream As Renci.SshNet.ShellStream
NOTE: For the purists out there, I am trying to both cleanse the code pasted, but also not give fragments, if I can get away with it. I get very frustrated with fragments because they never work as-is, and not everybody already understands the missing bits... :)
You see, SshNet has no way of knowing whether your server is still thinking or not. In fact, it's not sure what the 'prompt' looks like until you tell it what to 'expect'. That is all I was doing. The above is probably a horrible example to experts, but it took me days to get it to work consistently. I spent a lot of time in the source code, to be honest. :) As you can tell, this function just grabs a running config off a router.
Hopefully you can see that the following snippet from the above is simply waiting until ss.DataAvailable is False (meaning "we're all done!" for small commands/output), waiting a little bit (I ran into trouble when doing it rapid-fire), and saving the response:
I also have it do 'retry' where I catch the exceptions thrown, and analyze them. I might make a change in the SshNet parameter, or connect here or there, then try it again (up to X times, usually 3). I also had to allow the code to decide what type of line ending to use (none, LF, CR, CRLF), as devices use these in an inconsistent way. I noticed the same errors were happening (you know, like after a connection times out, etc.), and I wanted to remove the logic to manage these exceptions from every part of my biz logic that wanted to send a command (a lot of dupes doing it that way!).
My post is limited to 10k, so this will be Part 1.
pat
:(
Marc,
I didn't want your post/question/issue to hang out there for too long without someone saying something from the community. I'm far from an expert, so bear with me. You have the following questions in your post:
1. How do I instantiate a shell?
There are probably more ways, but i have found/used 2 of them to make things happen in my code with Ssh.Net, and one of them (the first I used) is with a shell. In the above, you are not using a shell, I think. Here is how the docs say:
public ShellStream CreateShellStream(
string terminalName,
uint columns,
uint rows,
uint width,
uint height,
int bufferSize,
IDictionary<TerminalModes, uint> terminalModeValues
)
I use this way when I have to enter a command, wait for the response, then do another command based on that response-all in the same session, which it looks like you were trying to do in your post, creating an interactive shell, of sorts(?). My implementation is a bit involved, but it boils down to:Private ss As Renci.SshNet.ShellStream
Public Property Client As Renci.SshNet.SshClient
Private _shStream As Renci.SshNet.ShellStream
Public Property ShStream As Renci.SshNet.ShellStream Implements IASASecureComms.ShStream
Get
If Me.ss Is Nothing Then
_shStream = Me.GetShellStream
End If
Return _shStream
End Get
Set(value As Renci.SshNet.ShellStream)
_shStream = value
End Set
End Property
Public Function GetShellStream() As Renci.SshNet.ShellStream
Try
If Me.Client Is Nothing Then
log.Error("GetShellStream() given an uninstantiated SshClient!")
Return Nothing
End If
'
' are we at least connected?
'
If Not Me.Client.IsConnected Then
log.Error("GetShellStream() trying to connect client...")
Me.Client.Connect()
log.Error("GetShellStream() could we connect? " & Client.IsConnected)
End If
'
' only create ShellStream if connected
'
If Me.Client.IsConnected And ss Is Nothing Then ' class level variable XXX change to param, so we can auto-instantiate in Get()
Me.ss = Me.Client.CreateShellStream("dumb", 80, 24, 800, 600, 102400)
ElseIf Me.Client.IsConnected And Not ss Is Nothing Then
' all is well! We are both connected, and ss is instantiated!
log.Debug("GetShellStream() all is well, nothing to do here...")
Else
log.Fatal("GetShellStream() Client is not connected!")
Throw New Exception("GetShellStream() Client is not connected!")
End If
Catch ex As Exception
log.Fatal("GetShellStream() cannot create new ShellStream: ", ex.ToString)
End Try
Return ss
End Function
... as you can see from this line:Me.ss = Me.Client.CreateShellStream("dumb", 80, 24, 800, 600, 102400)
...I'm literally creating a shell, complete with height, width, and a buffer. Here is how I would use it for small, interactive exchanges (by small, I mean no more than 1/2 the buffer (no idea why)):NOTE: For the purists out there, I am trying to both cleanse the code pasted, but also not give fragments, if I can get away with it. I get very frustrated with fragments because they never work as-is, and not everybody already understands the missing bits... :)
Public Function GetRunningConfig() As String
Dim sb As StringBuilder = Nothing
Dim line As String = ""
Try
Dim reader = New StreamReader(Me.ss)
Dim writer = New StreamWriter(Me.ss)
writer.AutoFlush = True
sb = New StringBuilder(System.String.Empty)
ss.Expect(New Regex(":.*>#"), TimeSpan.FromSeconds(3))
ss.WriteLine("terminal pager 0")
Thread.Sleep(1000)
While ss.DataAvailable = False
Thread.Sleep(500)
Debug.WriteLine("No data available")
End While
ss.WriteLine("show running-config")
Thread.Sleep(1000)
While ss.DataAvailable = False
Thread.Sleep(500)
Debug.WriteLine("No data available")
End While
ADDMORE:
line = ss.ReadLine(TimeSpan.FromSeconds(3))
If Not IsNothing(line) Then
If line.Length > 0 Then
'Debug.WriteLine("adding to sb: [" & line & "]")
sb.AppendLine(line)
GoTo ADDMORE
Else
Debug.WriteLine("no more!")
End If
End If
Debug.WriteLine("result: [" & sb.ToString() & "]")
Catch ex As Exception
log.Fatal("GetRunningConfig() failed: " & ex.ToString())
End Try
Return sb.ToString()
End Function
One of the things here that you should note is this line:ss.Expect(New Regex(":.*>#"), TimeSpan.FromSeconds(3))
...which tells the SshNet what to look for to tell it is is done...You see, SshNet has no way of knowing whether your server is still thinking or not. In fact, it's not sure what the 'prompt' looks like until you tell it what to 'expect'. That is all I was doing. The above is probably a horrible example to experts, but it took me days to get it to work consistently. I spent a lot of time in the source code, to be honest. :) As you can tell, this function just grabs a running config off a router.
Hopefully you can see that the following snippet from the above is simply waiting until ss.DataAvailable is False (meaning "we're all done!" for small commands/output), waiting a little bit (I ran into trouble when doing it rapid-fire), and saving the response:
While ss.DataAvailable = False
Thread.Sleep(500)
Debug.WriteLine("No data available")
End While
ss.WriteLine("show running-config")
Thread.Sleep(1000)
While ss.DataAvailable = False
Thread.Sleep(500)
Debug.WriteLine("No data available")
End While
Below further I'll paste in a Function() that logs you in enable, as another example of using the ShellStream.Friend Function LoginEnable() As Boolean
Dim r As String = "" ' for 'response' from FW
log.Debug("===== STARTING LoginEnable()")
Try
If Not Client.IsConnected Then
Me.Client.Connect()
Me.ss = GetShellStream()
log.Debug("LoginEnable() connected? " & Me.Client.IsConnected)
End If
If Me.Client.IsConnected Then
Me.LoggedInUser = True
End If
'------------------------------------------------------------------
Me.ss.Flush()
Dim retriesEnable As Integer = 3
While (Not r.ToLower.Contains("password")) And (retriesEnable > 0)
r = SendCommand("enable", "LF")
log.Debug("LoginEnable(): r1 = [" & r & "]")
Thread.Sleep(1500)
retriesEnable -= 1 ' count down...
End While
'------------------------------------------------------------------
Me.ss.Flush()
r = SendCommand(Me.EnablePass)
log.Debug("LoginEnable(): r2 = [" & r & "]")
Thread.Sleep(1500)
If DoesContain(r, "#") Then
Me.LoggedInEnable = True
Me.LoggedInUser = True
log.Info("LoginEnable(): Logged in with enable!")
Else
Me.LoggedInEnable = False
Throw New Exception("LoginEnable(): enable attempt FAILED! " & r)
End If
Catch ex As Exception
log.Fatal("LoginEnable(): Cannot log into enable! " & ex.ToString)
Me.LoggedInEnable = False
End Try
log.Debug("===== ENDING LoginEnable() with: " & Me.LoggedInEnable)
Return Me.LoggedInEnable
End Function
Now I'm sure the Function SendCommand() caught your eye... That is a very long Function I wrote to allow any part of my code to simply send a command at any time. It does things like check to mae sure we're logged on (if not, log on), logged in enable (if not, log in enable), make sure we even have a ShellStream (if not, go make one), etc. You get the idea.I also have it do 'retry' where I catch the exceptions thrown, and analyze them. I might make a change in the SshNet parameter, or connect here or there, then try it again (up to X times, usually 3). I also had to allow the code to decide what type of line ending to use (none, LF, CR, CRLF), as devices use these in an inconsistent way. I noticed the same errors were happening (you know, like after a connection times out, etc.), and I wanted to remove the logic to manage these exceptions from every part of my biz logic that wanted to send a command (a lot of dupes doing it that way!).
My post is limited to 10k, so this will be Part 1.
pat
:(