PHPでもTelnetがしたい！

本記事は [NetOpsCoding Advent Calendar 2015](http://qiita.com/advent-calendar/2015/netopscoding)、23日目の記事です。

はじめまして。とある通信会社でネットワークの運用業務をしています。クリスマス・イヴイヴに「ルーターとお喋りする」という悲しいお話をします。

## はじめに

世間では NETCONF/YANG などが話題ですが、Cisco 7206VXR、Catalyst 2950 といった~~生きた化石~~ 歴史ある通信機器が現役な現場では未だ（そしてこれからも） Telnet によるCLI制御が主役です。Perl、Python、Ruby などの主要なLL言語では Telnet 用ライブラリが用意されていますが、PHP にはありません。

Packagist に　[Telnetクライアントライブラリ](https://github.com/bestnetwork/Telnet/) があるものの、プロンプト等の違いによりルータ類にログインできないため、今回はこれを拡張し PHP から Telnet でルータにログインするライブラリを作ったのでご紹介します。


## Telnet ライブラリについて

### 条件

- 実行環境要件 :
  - PHP 5.6以上（PHP 7.0対応)
  - Composer
- 対象ホスト :
  - Linux/UNIXサーバー全般
  - Cisco IOS, IOS-XE, IOS-XR ルーター・スイッチ
  - Juniper JUNOS ルーター・スイッチ
  - AlaxalA, HITACHI ルーター・スイッチ

### 開発／検証環境
- PHP実行環境
  - CentOS 7.2
  - Apache 2.4
  - PHP 7.0
- テスト済み接続先
  - RHEL 5
  - RHEL 6
  - CentOS 7
  - Cisco 7206VXR (IOS 12.3 / 12.4)
  - Cisco Catalyst 2950 / 2960 / 3560 / 3750 (IOS 12.2)
  - Cisco 7604 / 7609 (IOS 12.2)
  - Cisco ASR 1001 / 1002 / 1006 (IOS-XE 15.1)
  - Cisco CRS-1 / CRS-3 (IOS-XR 4.0)
  - Juniper T640 / T1600 / T4000 (JUNOS 12.3)
  - Juniper E120 / E320 (JUNOS 12.3)
  - Juniper MX240 (JUNOS 12.3)
  - HITACHI GR4000 (ROUTE-OS 9)
  - AlaxalA AX7800R (OS-R 10.10)

### ソース

- GitHub : https://github.com/miyahan/Telnet
- Packagist : https://packagist.org/packages/miyahan/telnet

### インストール方法

Composer を使いますので、予めインストールしておきます → https://getcomposer.org/

まず composer.json を作り、以下のようにライブラリを記述します。

```json:composer.json
{
    "require": {
        "miyahan/telnet": "dev-master"
    }
}
```
composer.json を作成したら、`composer install` してライブラリをインストールします。

```bash
$ composer install
Loading composer repositories with package information
Installing dependencies (including require-dev)
  - Installing miyahan/telnet (dev-master e1003c2)
    Cloning e1003c27e422772962d849187c31f8ace38e8753

Writing lock file
Generating autoload files
```

ライブラリのインストールと同時にオートローダーが作成されますので、以後これを require して使います。


### 使い方：Juniper JUNOS ルータにTelnetし情報を取得する

```php:telnet-junos.php
<?php
require 'vendor/autoload.php';

try {
	$telnet = new \miyahan\network\Telnet('10.0.0.1');
	$telnet->connect();
	$telnet->login('root', 'hogehoge', 'junos');
	$telnet->exec('set cli screen-length 0');
	$telnet->setPrompt('root>');

	$result = $telnet->exec('show system uptime');
	echo $result;
} catch (Exception $e) {
	echo $e->getMessage();
}
```

```text:(result)
$ php telnet-juniper.php
show system uptime 
scc-re0:
--------------------------------------------------------------------------
Current time: 2015-12-16 03:26:55 JST
System booted: 2011-06-21 14:00:44 JST (234w0d 13:26 ago)
Protocols started: 2011-06-21 15:04:17 JST (234w0d 12:22 ago)
Last configured: 2015-10-06 11:16:17 JST (10w0d 16:10 ago) by root
 3:26AM  up 1638 days, 13:26, 1 user, load averages: 0.04, 0.23, 0.21

lcc0-re0:
--------------------------------------------------------------------------
Current time: 2015-12-16 03:26:55 JST
System booted: 2011-06-21 14:01:09 JST (234w0d 13:25 ago)
Last configured: 2015-10-06 11:16:11 JST (10w0d 16:10 ago) by root
 3:26AM  up 1638 days, 13:26, 0 users, load averages: 0.00, 0.02, 0.06
/// snip ///
```

取得できました。結果を一度に返すように `set cli screen-length 0` コマンドを発行しておくことがポイントです。あとは `preg_match` などの文字操作関数を使ってよしなに情報を抽出します。


### 使い方：Cisco IOS ルータにTelnetし情報を取得する

```php:telnet-cisco.php
<?php
require 'vendor/autoload.php';

try {
	$telnet = new \miyahan\network\Telnet('10.0.0.2');
	$telnet->connect();
	$telnet->login('root', 'hogehoge', 'ios');
	$telnet->exec('terminal length 0');
	$telnet->exec('enable'."\r\n".'hogehoge');
	$telnet->setPrompt('router#');

	$result = $telnet->exec('show environment all');
	echo $result;
} catch (Exception $e) {
	echo $e->getMessage();
}
```

```text:(result)
$ php telnet-cisco.php
show environment all
Power Supplies:
	Power Supply 1 is Zytek DC Power Supply. Unit is on.
	Power Supply 2 is Zytek DC Power Supply. Unit is on.

Temperature readings:
	chassis inlet    measured at 28C/82F 
	chassis outlet 1 measured at 29C/84F 
	chassis outlet 2 measured at 31C/87F 
	chassis outlet 3 measured at 32C/89F 

Voltage readings:
	+3.45 V       measured at +3.50 V 
	+5.15 V       measured at +5.27 V 
	+12.15 V      measured at +12.34 V 
	-11.95 V      measured at -11.95 V 

Envm stats saved 74822 time(s) since reload
```

取得できました。こちらでも予め `terminal length 0` コマンドを発行しておくことがポイントです。


### トラブルシュート

#### WebからアクセスするとTelnet接続に失敗する

SELinux が有効だとTelnet接続に失敗してしまいます。

```text
$ curl http://localhost/telnet.php | more
Cannot connect to 10.0.0.1 on port 23

$ sudo tail /var/log/audit/audit.log
type=AVC msg=audit(1450217222.842:111): avc:  denied  { name_connect } for  pid=2553 comm="httpd" dest=23 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:telnetd_port_t:s0 tclass=tcp_socket
````

この場合は SELinux を無効化するか、下記コマンドで httpd からのTCPソケット接続を許可してください。

```text
$ sudo setsebool -P httpd_can_network_connect 1
```


#### 遅いコマンドがタイムアウトしてしまう

デフォルトではプロンプトが返って来るまで10秒以上かかるとタイムアウトが発生してしまいます。

```text
$ php telnet.php 
Couldn't find the requested : 'router#' within 10 seconds
```

`show tech-support` など、応答に時間がかかる処理を実行する場合は、コンストラクタの第4引数にタイムアウト時間（秒）を長めに指定してください。

```php:telnet-longtime.php
$telnet = new \miyahan\network\Telnet('10.10.10.10', 23, 10, 300.0);
```


## おわりに

PHP から Telnet するという誰得な内容でしたが、私の会社では PHP で書かれたWebアプリからルーターやスイッチにアクセスし、コマンドの応答結果を整形して表示するといった見える化系ツールで活躍しています。その事例も今後紹介できればなと思っています。

よろしければ使ってみてください。プルリクもお待ちしています。