GoTTYでブラウザからルータを操作してみた

# はじめに
[ネットワークプログラマビリティ勉強会 #8](http://network-programmability.connpass.com/event/26539/)の「ブラウザからルータを操作してみた」の関連記事です。GoTTYでWebブラウザのターミナルからルータへTELNETやRubyスクリプトの実行をしてみました。

Webブラウザで「ルータへのTELNET」と「コンフィグ一括保存スクリプト」の実行デモ
![Web Terminal.gif](https://qiita-image-store.s3.amazonaws.com/0/12026/2a0da88b-efb4-27a6-4deb-294cb63e6ffb.gif)

## 動機
Qiitaの[GoTTY 良さそう](http://qiita.com/A-I/items/49bcb56ed977b4fb36ff)を見て、Webブラウザ上にターミナルを表示する方法を知りました。Go言語のバイナリをダウンロードし、gottyコマンドで非常に簡単にWebブラウザでターミナルを表示でき、通常のターミナルと遜色なく利用できたため、ルータを操作するために活用しようと考えました。
最初はルータへのTELNETのみを目的としていました。しかしながら、PerlやRubyで作成したCLIスクリプトの実行も可能である気づき、今まで作っていたスクリプトをWebブラウザ上で実行しようと考えました。
今回は、WebブラウザからルータへのTELNETと、Rubyスクリプトの実行を目的として作成しました。

GoTTYの使い方については、Qiitaの投稿の[GoTTY 良さそう](http://qiita.com/A-I/items/49bcb56ed977b4fb36ff)や[gottyに感動した](http://qiita.com/tukiyo3/items/2cea89b061b6ba7a3db6)をご参照ください。

# 仕組み
WebブラウザはGoTTYサーバのターミナルを、HTMLのiframeまたは直接アクセスで表示します。
GoTTYサーバはリクエストのたびに、TelnetコマンドやCLIスクリプトを実行します。リクエストにはルータのIPアドレスなどのパラメータが渡されます。それを元にルータにTelnetしたり、ルータをCLIスクリプトから操作します。
![仕組み.png](https://qiita-image-store.s3.amazonaws.com/0/12026/1fc20034-86c2-05e0-7297-64d9f560863f.png)

# 環境

- [yudai/gotty](https://github.com/yudai/gotty) 0.0.12 : GoTTYサーバ
- Ubuntu 14.04
- Ruby 2.2.4
  - sinatra 1.4.7 : Webフレームワーク
  - slim 3.0.6 : HTMLテンプレートエンジン
  - thor 0.19.1 : コマンドラインツールのフレームワーク
  - expect4r 0.0.11 : Expectライブラリ
- Google Chrome 48 : Webブラウザ
- Cisco VIRL 0.10.22.3 : Ciscoルータの仮想実行環境

# スクリプト
ファイル一覧。gottyバイナリはGithubからダウンロードし、同じディレクトリに配置します。その他のファイルは下記に示します。

```bash
.
├── Gemfile        Rubyライブラリ
├── Rakefile       起動用スクリプト
├── cli.rb         CLIスクリプト、GoTTYから実行
├── gotty          GoTTYのバイナリ
├── views
│   └── index.slim HTMLテンプレートファイル
└── web.rb         Webサーバ
```

## Rakefile

タスクとして下記を記述してます

- server : Webサーバの起動
- gotty : GoTTYサーバの起動

```rb:Rakefile
task :server do
  ruby 'web.rb'
end

task :gotty do
  sh './gotty -w --permit-arguments ruby cli.rb'
end
```

## Gemfile

使用しているRubyライブラリ

```rb
source 'https://rubygems.org'
gem 'sinatra'
gem 'sinatra-contrib'
gem 'slim'
gem 'expect4r'
gem 'thor'
```

## CLIスクリプト - cli.rb
GoTTYから起動する実行ファイルです。RubyとThorで各種コマンドを記述しています。
GoTTYから各サブコマンドを呼び出して、ターミナル上で表示しています。

Ruby/ThorはCLIアプリケーション用のフレームワークです。gitコマンドのようなサブコマンドを簡単に定義できます。今回はGoTTYから各種のサブコマンドを実行しています。

telnetサブコマンドが呼び出されると、直接tenletコマンドを実行します。
saveサブコマンドが呼び出されると、expect4rを利用して各ルータにTELNETし、copyコマンドでコンフィグを保存します。

```rb
#!/usr/bin/env ruby
require 'thor'
require 'expect4r'

HOSTS = %w(172.16.1.158 172.16.1.162 172.16.1.161).freeze

# TermCLI
class TermCLI < Thor
  desc 'telnet IPADDRESS', 'telnet to IPADDRESS'
  def telnet(ipaddr)
    exec "telnet #{ipaddr}"
  end

  desc 'bash', 'execute bash shell'
  def bash
    exec 'bash'
  end

  desc 'save', 'save config'
  def save
    HOSTS.each do |host|
      puts '#' * 60
      puts "#{host}の保存"
      ios = Expect4r::Ios.new_telnet(host: host, user: 'cisco', pwd: 'cisco')
      ios.login
      puts ios.putline("copy running-config startup-config\r", no_trim: true)
      puts "#{host}の保存完了！"
      puts
    end
    STDIN.gets
  end

  desc 'route', 'show ip route'
  def route
    HOSTS.each do |host|
      puts '#' * 60
      puts "#{host}のルート情報"
      ios = Expect4r::Ios.new_telnet(host: host, user: 'cisco', pwd: 'cisco')
      puts ios.show_ip_route
      puts
    end
    STDIN.gets
  end
end

TermCLI.start(ARGV)
```

## Webサーバ - web.rb
Ruby/SinatraのWebサーバです。
``GOTTY_BASE``はGoTTYが動作するURIです。``@terminals``変数にHTMLテンプレートに渡すパラメータを直接埋め込んでいます。

```rb
#!/usr/bin/env ruby
require 'sinatra'
require 'sinatra/reloader'
require 'slim'
require 'yaml'

set :bind, '0.0.0.0'

GOTTY_BASE = 'http://172.30.21.18:8080'.freeze

get '/' do
  @terminals = YAML.load(<<EOL)
- :name: R1
  :type: Router
  :desc: Telnet 172.16.1.158
  :uri:  #{GOTTY_BASE}/?arg=telnet&arg=172.16.1.158
- :name: R2
  :type: Router
  :desc: Telnet 172.16.1.162
  :uri:  #{GOTTY_BASE}/?arg=telnet&arg=172.16.1.162
- :name: R3
  :type: Router
  :desc: Telnet 172.16.1.161
  :uri:  #{GOTTY_BASE}/?arg=telnet&arg=172.16.1.161
- :name: Bash
  :type: Script
  :desc: シェル実行
  :uri:   #{GOTTY_BASE}/?arg=bash
- :name: Save
  :type: Script
  :desc: コンフィグ一括保存
  :uri:   #{GOTTY_BASE}/?arg=save
- :name: Route
  :type: Script
  :desc: ルート確認
  :uri:  #{GOTTY_BASE}/?arg=route
EOL

  slim :index
end
```

## HTMLテンプレート - views/index.slim
テンプレートエンジンのslimと、JavaScriptのjQueryを利用しています。
ターミナル一覧を表示します。ルータへのTELNETやCLIスクリプトの選択し、実行します。ターミナルの実行結果はiframeで埋め込みます。iframeで埋め込む処理はjQueryで記述してます。
「ターミナルを開く」をクリックすると、同じWebページ内にiframeでGoTTYを呼び出しターミナルが表示されます。
「新しいウィンドウで開く」をクリックすると、別ページとして、GoTTYのターミナルが表示されます。

```slim
doctype html
html lang="ja"
  head
    meta charset="utf8"
    meta http-equiv="X-UA-Compatible" content="IE=edge"
    meta name="viewport" content="width=device-width, initial-scale=1"
    link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous"
    script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"
    script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"
    title
      | Web Terminal
  body
    table.table
      thead
        th TYPE
        th NAME
        th DESCRIPTION
        th
        th
      tbody
        - @terminals.each do |terminal|
          tr
            td = terminal[:type]
            td = terminal[:name]
            td = terminal[:desc]
            td
              a.terminal_link href="#" data-name="#{terminal[:name]}" data-uri="#{terminal[:uri]}"
                span.glyphicon.glyphicon-log-in
                | &nbsp;ターミナルを開く
            td
              a href="#{terminal[:uri]}" target="_blank"
                span.glyphicon.glyphicon-new-window
                | &nbsp;新しいウィンドウで開く

    #terminal_area
    #terminal_template style="display:none"
      .terminal_window
        .panel.panel-default
          .panel-heading
            span.title
            button.close type="button"
              | &times;
          .panel-body
            .embed-responsive.embed-responsive-16by9
              iframe.embed-responsive-item src=""
    javascript:
      $(function(){
        $(document).on('click', '.close', function(){
          $(this).parents(".terminal_window").remove();
        });
        $("a.terminal_link").on('click', function(){
          $('#terminal_area').append($('#terminal_template').html())
          var el = $('#terminal_area > div:last')
          el.find('iframe').attr('src', $(this).data("uri"))
          el.find('.title').text($(this).data("name") + ' Terminal')
        });
      })
```

# 実行結果

WebサーバとGoTTYサーバをrakeで起動します。その後、WebブラウザでWebサーバのIPアドレスをポートを指定して開きます。今回はWebサーバの「[http://172.30.21.18:4567](http://localhost:4567)」になります。

```bash
$ rake server
/home/kooshin/.rbenv/versions/2.2.4/bin/ruby web.rb
[2016-02-29 00:39:28] INFO  WEBrick 1.3.1
[2016-02-29 00:39:28] INFO  ruby 2.2.4 (2015-12-16) [x86_64-linux]
== Sinatra (v1.4.7) has taken the stage on 4567 for development with backup from WEBrick
[2016-02-29 00:39:28] INFO  WEBrick::HTTPServer#start: pid=4801 port=4567
```

```bash
$ rake gotty
./gotty -w --permit-arguments ruby cli.rb
2016/02/29 00:39:49 Permitting clients to write input to the PTY.
2016/02/29 00:39:49 Server is starting with command: ruby cli.rb
2016/02/29 00:39:49 URL: http://127.0.0.1:8080/
2016/02/29 00:39:49 URL: http://172.30.21.18:8080/
```

## ルータへTELNET
ルータR1にTELNETして、ルータ上でshowコマンドの実行結果です。
![Web Terminal1.png](https://qiita-image-store.s3.amazonaws.com/0/12026/9fa45dee-cc11-a1ed-9891-befcb864b31d.png)

## RubyのCLIスクリプト実行
CLIスクリプトに定義したsaveコマンドを実行しています。今回は3台のルータのコンフィグを保存しています。
![Web Terminal2.png](https://qiita-image-store.s3.amazonaws.com/0/12026/95c6b3bd-94f2-d1ef-6d49-f0ea1125379e.png)

# おわりに
GoTTYによってWebブラウザで簡単にCLIスクリプトの実行が可能になりました。LinuxサーバにSSHでログインして実行や、Windows上で各種ライブラリを集めて実行環境を作成することから解放されます。複数のユーザで同じ実行環境を作ることが可能になると思います。
既存資産のスクリプトを有効活用できます。運用で使っているちょっとしたスクリプトを実行するには最適な環境と言えます。
実際の運用で利用するためには、セキュリティには十分注意する必要があります。bashの直接実行ができてしまい、踏み台にされてしまう可能性があります。ユーザ認証やDocker上で起動させるなど、更に作りこみが必要になります。

# 参考文献

- [yudai/gotty - github](https://github.com/yudai/gotty)
- [GoTTY 良さそう - Qiita](http://qiita.com/A-I/items/49bcb56ed977b4fb36ff)
- [gottyに感動した - Qiita](http://qiita.com/tukiyo3/items/2cea89b061b6ba7a3db6)
- [Thor入門 - RakeからThorへ - - Qiita](http://qiita.com/yatmsu/items/5ba7016db33c13d7a882)
- [Thorの使い方まとめ - Qiita](http://qiita.com/succi0303/items/32560103190436c9435b)
- [Rubyのexpect4rでCiscoルータにTelnet/SSHしてコマンド実行する - Qiita](http://qiita.com/kooshin/items/27f2b9ed5c8896279bcf)
- [Rubyで多数のCiscoルータで複数のコマンドを実行する - Qiita](http://qiita.com/kooshin/items/fe077630aad78b02a88f)
- [Compressor.io](https://compressor.io/)