SSH2 в php5 + Mikrotik RouterOS, подводные камни

    Стояла задача: в цикле, из скрипта на 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. Обрабатываю результат от дитятки.

    Таким образом, хоть и криво, но все вышеупомянутые проблемы обходятся.

    Ниже привожу кусок кода, реализующий этот функционал, дабы, если кому надо, не тратить время на создание велосипеда.

    /* 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);
    
    


    Поделиться публикацией

    Похожие публикации

    Комментарии 18

      0
      ssh2_disconnect == ssh2_exec($connection, 'exit');
      почему не заюзали ssh2_scp_recv?
        0
        ssh2_scp_recv упорно заявлял что ему не удаётся скопировать файл, когда работал с микротиком (даже если это первый и единственный запрос в этом соединении), при том, что с Ubuntu при тех же параметрах работал (менял только IP). Так как ssh2_sftp заработал сразу — не стал возиться.
        0
        Поправьте пожалуйста метки, они должны перечисляться через запятую.
          0
          Пардон, о каких конкретно метках речь? Похоже у меня глаз замылился.
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Поправил, спасибо Вам.
        • НЛО прилетело и опубликовало эту надпись здесь
            0
            Как я понял, эта библиотека писана под php4, при попытке её использовать в требуемом окружении (ubuntu server, php5.3) получил уйму ошибок на неверные параметры к функциям в коде самой библиотеки. Так как сроки поджимали — отказался от неё не разбираясь, хотя по описанию она мне понравилась.
            • НЛО прилетело и опубликовало эту надпись здесь
                0
                Ок, спасибо за информацию, покопаю при случае подробнее, значит не всё так плохо.
            0
            Может я не так понял статью и задумку автора, но не проще ли было сделать бэкндом shell/perl-скрипт, который бы делал легко и со свистом всю «черную» работу, а бэкэнд обращался бы к нему через shell_exec(), скажем?
            В таком случае мы избавляемся от зависимости таймаута http/браузера/php.
              0
              Да, лично мне тоже перл нравится больше. Но задача стояла — реализовать именно на php.
              0
              ин зе мемори…
                0
                <source lang='php'>
                
                </source>

                  0
                  Благодарствую, усвоил, поправил.
                  0
                  Эммм… А почему именно php, а не bash?
                    0
                    Такова была постановка задачи, это дополнение к системе, реализованной на php, у ребят на то свои причины.
                      0
                      сделать локальный скрипт bash через php и его запустить опять же через php никак?

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое