Стояла задача: в цикле, из скрипта на 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);
