Saturday, November 17, 2007

Experimenting with the erlang SSH support, or remote 'tail' with SSH...

While designing my monitoring tool, and working and treregex, I found the ssh documentation and realize that it can be very useful for my tool.

A simple question needed to be answered, is the ssh module able to easily spawn a remote process for me ?
To verify, I tried to build a remote tail module called ssh_tail :)

-module(ssh_tail).

-export([tail/3]).
-define(TIMEOUT, 5000).

tail(Host, User, Pass) ->
case ssh_cm:connect(Host, 22, [{user_dir, "/var/tmp/ssh"}, {user, User}, {password, Pass}]) of
{ok, CM} ->
session(CM, fun(X) -> io:format("-ssh: ~p~n", [X]) end);

Error ->
Error
end.

From the ssh documentation user_dir let you decide where you want to store keys, from my experience it's better to use a separate directory from the ~/.ssh.
It happens that latest version of ssh add meta information to their files that the ssh module can't handle. (more on this in another post).

For the test I wanted to do a "tail -f" on a specific file ie "/var/log/syslog".

session(CM, Callback) ->
case ssh_cm:session_open(CM, ?TIMEOUT) of
{ok, Channel} ->
case ssh_cm:shell(CM, Channel) of
ok ->
ssh_cm:send(CM, Channel, "tail --follow=name /var/log/syslog\n"),
ssh_loop(CM, Channel, Callback);

Error ->
error_logger:error_msg("Error: ~p~n", [Error])
end;
Error ->
error_logger:error_msg("Session Error: ~p~n", [Error])
end.

ssh_cm is responsible for starting a shell, and sending commands to the remote shell process. I send

tail --follow=name /var/log/syslog\n

Don't forget the final '\n' character, since you won't get any results if you don't send it :p
(I didn't think of that while testing for the first time and think that the code didn't work at all...)

ssh_loop(CM, Channel, Callback) ->
receive
stop ->
% Closing channel
% ssh_cm:detach(CM, ?TIMEOUT),
ssh_cm:close(CM, Channel);

{ssh_cm, CM, {data, _Channel, 0, Data}} ->
Callback(Data),
ssh_loop(CM, Channel, Callback);

{ssh_cm, CM, {data, Channel, Type, Data}} ->
io:format("extended (~p): ~p~n", [Type, Data]),
ssh_loop(CM, Channel, Callback);

{ssh_cm, CM, {closed, _Channel}} ->
ssh_cm:detach(CM, ?TIMEOUT);

E ->
error_logger:info_msg("[~p] Received: ~p~n", [?MODULE, E]),
ssh_loop(CM, Channel, Callback)
end.

ssh_cm sends various message to the calling process, more important tuples are

{ssh_cm, CM, {data, _Channel, 0, Data}}

Data holds what you want, and in our case a line sent by the tail process...
The callback defined at the beginning is then executed:

tail(Host, User, Pass) ->
case ssh_cm:connect(Host, 22, [{user_dir, "/var/tmp/ssh"}, {user, User}, {password, Pass}]) of
{ok, CM} ->
session(CM,
fun(X) -> % Our Callback
io:format("-ssh: ~p~n", [X]) % simple display...
end);

Error ->
Error
end.


To conclude this simple module is able to spawn a remote "tail -f" using a ssh connection and using a callback function on every data received.

The code was designed from the ssh_ssh module that you can find in the ssh module source code, because the ssh documentation is really sparse for now...

1 comment:

etnt said...

Very nice!

Would it be possible/easy to implement an Erlang RPC service using the SSH stuff?

Cheers, Tobbe

Sticky