Стояла задача: в цикле, из скрипта на php5 зайти по ssh на Mikrotik, сгенерировать скрипт с текущим конфигом, забрать скрипт на некое локальное хранилище. И так для ~500 роутеров. Так как в провайдерских кругах микротик весьма нередкий зверь — думаю кому-то ещё может пригодиться.
Так как глубоких познаний в тонкостях реализации ssh2 на микротике, в пхп, да и вообще — не имею, а сроки сильно ограничены, встретившиеся проблемы решал подручными средствами и инструментами, особо не заботясь об «элегантности».
В процессе обнаружилось следущее:
1. При использовании ssh2_exec необходимо(!) прочесть ответ, иначе команда не будет запущена (выкопал в коментах в мануалах по PHP, поверил на слово, благо был готовый пример для copy-paste). Если это так — подосреваю, что дело в буферизации.
2. Микротик почему-то переваривает только один запрос (ssh2_exec, ssh2_shell, etc.). Если после ssh2_exec'a запросить второй ssh2_exec или ssh2_shell, или ssh2_sftp — сессия как правило (но не всегда!) «залипает» в ожидании ответа (дольше 5 минут не ждал).
3. Если запросить ssh2_shell для получения интерактивного shell'a, дабы иметь возможность «пообщаться» посодержательнее — микротик естественно соглашается, но вне зависимости от передаваемого типа терминала весело и щедро спамит color-кодами (чего не происходит при вызове ssh2_exec), которые сама библиотека ssh2 естественно парсить и не пытается, передавая их дальше. Это превращает обработку «чата» в гораздо менее тривиальную задачу, чем хотелось бы.
4. В ssh2 библиотеке НЕТ функции ssh2_disconnect, можно конечно пропатчить библиотеку, написав свою, но…
5. Если пытаться забирать скрипт прямо с «терминала» — в нём нет метки конца скрипта. Таким образом не известно с той стороны что-то тормозит и продолжение следует, или же это и в самом деле всё.
update (из коментов):
6. ssh2_scp_recv упорно заявлял что ему не удаётся скопировать файл, когда работал с микротиком (даже если это первый и единственный запрос в этом соединении), при том, что с Ubuntu при тех же параметрах работал (менял только IP).
/update
Как решил:
1. В скрипте делаю fork()
2. В child'e — ssh2_exec с командой '/export file=current'. Если всё успешно — выхожу exit'ом с кодом 0, иначе — 1. Соединение с микротиком само закрывается при завершении работы дитя.
3. Лювлю результат от дитятки, если успешно — форкаю ещё одно дитя, на этот раз с заданием снова подсоединится и уже по sftp забрать свежесозданный файл со скриптом.
4. Обрабатываю результат от дитятки.
Таким образом, хоть и криво, но все вышеупомянутые проблемы обходятся.
Ниже привожу кусок кода, реализующий этот функционал, дабы, если кому надо, не тратить время на создание велосипеда.
и где-то в основном скрипте вызов:
Так как глубоких познаний в тонкостях реализации ssh2 на микротике, в пхп, да и вообще — не имею, а сроки сильно ограничены, встретившиеся проблемы решал подручными средствами и инструментами, особо не заботясь об «элегантности».
В процессе обнаружилось следущее:
1. При использовании ssh2_exec необходимо(!) прочесть ответ, иначе команда не будет запущена (выкопал в коментах в мануалах по PHP, поверил на слово, благо был готовый пример для copy-paste). Если это так — подосреваю, что дело в буферизации.
2. Микротик почему-то переваривает только один запрос (ssh2_exec, ssh2_shell, etc.). Если после ssh2_exec'a запросить второй ssh2_exec или ssh2_shell, или ssh2_sftp — сессия как правило (но не всегда!) «залипает» в ожидании ответа (дольше 5 минут не ждал).
3. Если запросить ssh2_shell для получения интерактивного shell'a, дабы иметь возможность «пообщаться» посодержательнее — микротик естественно соглашается, но вне зависимости от передаваемого типа терминала весело и щедро спамит color-кодами (чего не происходит при вызове ssh2_exec), которые сама библиотека ssh2 естественно парсить и не пытается, передавая их дальше. Это превращает обработку «чата» в гораздо менее тривиальную задачу, чем хотелось бы.
4. В ssh2 библиотеке НЕТ функции ssh2_disconnect, можно конечно пропатчить библиотеку, написав свою, но…
5. Если пытаться забирать скрипт прямо с «терминала» — в нём нет метки конца скрипта. Таким образом не известно с той стороны что-то тормозит и продолжение следует, или же это и в самом деле всё.
update (из коментов):
6. ssh2_scp_recv упорно заявлял что ему не удаётся скопировать файл, когда работал с микротиком (даже если это первый и единственный запрос в этом соединении), при том, что с Ubuntu при тех же параметрах работал (менял только IP).
/update
Как решил:
1. В скрипте делаю fork()
2. В child'e — ssh2_exec с командой '/export file=current'. Если всё успешно — выхожу exit'ом с кодом 0, иначе — 1. Соединение с микротиком само закрывается при завершении работы дитя.
3. Лювлю результат от дитятки, если успешно — форкаю ещё одно дитя, на этот раз с заданием снова подсоединится и уже по sftp забрать свежесозданный файл со скриптом.
4. Обрабатываю результат от дитятки.
Таким образом, хоть и криво, но все вышеупомянутые проблемы обходятся.
Ниже привожу кусок кода, реализующий этот функционал, дабы, если кому надо, не тратить время на создание велосипеда.
/* Notify the user if the server terminates the connection */
function my_ssh_disconnect($reason, $message, $language) {
printf("Server disconnected with reason code [%d] and message: %s\n",
$reason, $message);
}
function backup_mt($device) {
if (!function_exists("ssh2_connect")) die("function ssh2_connect doesn't exist");
$methods = array(
'kex' => 'diffie-hellman-group1-sha1',
'client_to_server' => array(
'crypt' => '3des-cbc',
'comp' => 'none'),
'server_to_client' => array(
'crypt' => 'aes256-cbc,aes192-cbc,aes128-cbc',
'comp' => 'none'));
$callbacks = array('disconnect' => 'my_ssh_disconnect');
// for a process, as there is no ssh2_disconnect funciton and we have to close connection between commands.
$pid = pcntl_fork();
if ($pid == -1) {
echo ('could not fork');
return;
} else if ($pid) {
// we are the parent
pcntl_wait($status); //Protect against Zombie children
if (!pcntl_wifexited ($status)) {
echo "Child faled to exit normally.\n";
return;
}
if (pcntl_wexitstatus($status) >0) {
echo "Child reported failure. \n";
return;
}
} else {
// we are the child, we don't return, we just die when job's done
echo "Trying to connect to mikrotik host ".$device['name']."(".$device['ip'].") via ssh on port 22\n";
if(!($con = ssh2_connect($device['ip'], 22,$methods,$callbacks))){
echo "fail: unable to establish connection\n";
exit(1);
} else {
// try to authenticate with username root, password secretpassword
if(!ssh2_auth_password($con, $device['user'], $device['pass'])) {
echo "fail: unable to authenticate\n";
exit(1);
} else {
echo "Connected. Preparing configuration file.\n";
if (!($stream = ssh2_exec($con, "/export file=curcfg" ))) {
echo "fail: unable to execute command\n";
exit(1);
} else {
// collect returning data from command
stream_set_blocking($stream, true);
$data = "";
while ($buf = fread($stream,4096)) {
$data .= $buf;
}
fclose($stream);
// we don't need $data value for now, we just ignore it, but we have to retrieve it to avoid delays.
exit(0); // do not return, we're child, we don't want to continue main prorgam copy to execute.
}
}
}
} // end of child code
// give mt. time to save config and child to fully die, closing connections.
sleep(1);
// now fork another child to retrieve configuration. Make another connection for that.
$pid = pcntl_fork();
if ($pid == -1) {
die('could not fork');
} else if ($pid) {
// we are the parent
pcntl_wait($status); //Protect against Zombie children
if (!pcntl_wifexited ($status)) {
echo "Child faled to exit normally.\n";
return;
}
if (pcntl_wexitstatus($status) >0) {
echo "Child reported failure. \n";
return;
}
} else {
// we are the child again, we should not return from this section, we die when job's done
echo "Trying to connect to mikrotik host ".$device['name']."(".$device['ip'].") for sftp on port 22\n";
if(!($con = ssh2_connect($device['ip'], 22,$methods,$callbacks))){
echo "fail: unable to establish connection\n";
exit(1);
} else {
// try to authenticate with username root, password secretpassword
if(!ssh2_auth_password($con, $device['user'], $device['pass'])) {
echo "fail: unable to authenticate\n";
exit(1);
} else {
echo "Downloading configuration via sftp\n";
$sftp = ssh2_sftp($con);
echo "Got sftp handle.\n";
$size = filesize("ssh2.sftp://$sftp/curcfg.rsc");
echo "File size: $size\n";
$stream = fopen("ssh2.sftp://$sftp/curcfg.rsc", 'r');
if (! $stream) {
echo "Could not open file /curcfg.rsc\n";
exit(1);
}
else {
echo "Reading file...";
$contents = '';
$read = 0;
$len = $size;
while ($read < $len && ($buf = fread($stream, $len - $read))) {
$read += strlen($buf);
$contents .= $buf;
echo strlen($buf).'B...';
}
file_put_contents ('/tmp/'.$device['ip'],$contents);
@fclose($stream);
echo "done\n";
}
exit(0); // do not return, we're child, we don't want to continue main prorgam copy to execute.
}
}
} // end of child code
}
...
и где-то в основном скрипте вызов:
$device=array('type'=>$type_id,'user'=>$tokens[80],'pass'=>$tokens[56],'name'=>$tokens[71],'ip'=>$ip);
if ($type_id == DEV_TYPE_MIKROTIK) backup_mt($device);