gulpからssh接続して色々やってみる

# はじめに
ssh接続して定常的な作業を行う事が多々あるかと思います。
ターミナルから毎回sshコマンド打つのが面倒だったのでgulpで何とかならないかと模索したところ何とかなりました。

gulp-sshとssh2shellを使った例で説明しますが、以下２点はgulp-sshでなく、より複雑な処理が可能なssh2shellを使って実現しました。

* su でユーザー変更
* SSHログインしてさらにSSHログイン

# gulp-sshを使う方法
sshログインして単純なコマンド実行するだけなら簡単でオススメです。

## gulp-sshのインストール
```
npm install --save-dev gulp-ssh
```

## gulpのサンプルコード

秘密鍵でsshログインして、``pwd``コマンドを実行する処理です。
今回、秘密鍵でログインしていますが、passwordのパラメータを付与すればパスワードでのログインももちろん可能です。

詳細は以下から確認してみてください。
https://www.npmjs.com/package/gulp-ssh


```js:sample.js
var gulp = require('gulp');
// SSH通信用のパッケージ
var GulpSSH = require('gulp-ssh');
// File System (Nodeデフォルトで用意。npm installしないでOK)
var fs = require('fs');

module.exports = function() {
  gulp.task('sample', function () {

    // 接続設定
    var config = {
      // ホスト名
      host: '192.168.1.1',
      // ポート番号
      port: 22,
      // SSH接続ユーザー名
      username: 'testuser1',
      // 秘密鍵
      privateKey: fs.readFileSync('/Users/username/.ssh/id_rsa')
    };

    // インスタンス生成
    var gulpSSH = new GulpSSH({
      ignoreErrors: false,
      sshConfig: config
    });

    // 実行
    return gulpSSH
    // ログイン後にpwdを実行して/logs/commands.log に実行結果を書き出す
    .exec(['pwd'], {filePath: 'commands.log'})
    .pipe(gulp.dest('logs'));

  });
};
```

## 実行
```
gulp sample
```

実行すると

```
[11:48:02] Starting 'sample'...
[11:48:02] gulp-ssh :: Connect...
[11:48:02] gulp-ssh :: Ready
[11:48:02] gulp-ssh :: Executing :: pwd
[11:48:02] Finished 'sample' after 226 ms
[11:48:02] gulp-ssh :: End
[11:48:02] gulp-ssh :: Close
```
Executing や接続状況が表示されます。

コード内で指定しているように、/log/commands.logにも``pwd``の結果が書き出されているはずです。

## gulpのタスク実行時にパラメータを与える
サンプルスクリプトの状態だと、configで指定されたポートや接続先、ユーザーしかログインできません。

あまり実用性が無いので、実行時にパラメータを付与するように変更してみました。

## yargsを使ってgulpのタスク実行時にパラメータを付与
yargsというパッケージを使う事によってgulpのタスク実行にパラメータを付与する事ができます。まずはインストールしてみましょう。

```
npm install --save-dev npm install yargs
```

インストールが終わったら、以下のように先ほどのサンプルコードを修正します。

yargsのrequire

```js
var argv = require('yargs').argv;
```

SSHのログインユーザーを``var user = argv.user;``として定義

config内の接続ユーザーを

```js
// SSH接続ユーザー名
username: user,
```
として定義します。

変更したコードの全貌

```js:sample.js
'use strict';

var gulp = require('gulp');
// SSH通信用パッケージ
var GulpSSH = require('gulp-ssh');
// パラメータ取得パッケージ
var argv = require('yargs').argv;
// File System (Nodeデフォルトで用意。npm installしないでOK)
var fs = require('fs');

module.exports = function() {
  gulp.task('sample', function () {

    // 実行パラメータからユーザー名のセット
    var user = argv.user;

    // 接続設定
    var config = {
      // ホスト名
      host: '192.168.1.1',
      // ポート番号
      port: 22,
      // SSH接続ユーザー名
      username: user,
      // 秘密鍵
      privateKey: fs.readFileSync('/Users/username/.ssh/id_rsa')
    };

    // インスタンス生成
    var gulpSSH = new GulpSSH({
      ignoreErrors: false,
      sshConfig: config
    });

    // 実行
    return gulpSSH
    // ログイン後にpwdを実行して/logs/commands.log に実行結果を書き出す
    .exec(['pwd'], {filePath: 'commands.log'})
    .pipe(gulp.dest('logs'));

  });
};

```

## パラメータをつけて gulp の実行

``--user`` に SSHログインユーザーを渡して実行する事ができます。

```
gulp sample --user username
```

これを応用すればポートや接続先など色々な情報を実行時のパラメータに渡して処理する事ができます。

# ssh2shellを使う方法
冒頭にも書きましたが複雑な処理はssh2shellで実装します。
もしかしたらgulp-sshでも出来るのかもしれませんが、模索した結果行き着いた先はssh2shellとなりました。

ssh2shellの詳細はこちらから
https://www.npmjs.com/package/ssh2shell

## 今回やる作業
* SSHログイン
* su で別のユーザー(ユーザー名:changeuser)に切り替え
* さらに別のSSHサーバーにログイン (10.1.1.1)
* cd でディレクトリ移動
* git pull

以上の作業をssh2shellを使って実装します。

```js:sample.js
var gulp = require('gulp');

// パラメータ取得パッケージ
var argv = require('yargs').argv;

// File System (Nodeデフォルトで用意。npm installしないでOK)
var fs = require('fs');

// SSH2通信のパッケージ
var SSH2Shell = require('ssh2shell');

module.exports = function() {
  // タスク名
  gulp.task('sample', function() {

    // 実行パラメータからsshユーザー名のセット
    var user = argv.user;

    var sshObj = {
      server: {
        host: '192.168.1.1',
        port: 22,
        userName: user,
        privateKey: fs.readFileSync('/Users/username/.ssh/id_rsa'),
      },
      // 実行コマンド
      commands: [
        // changeuserにユーザー切り替え
        'su - changeuser',
        // 他のサーバーにSSHログイン
        'ssh 10.1.1.1',
        'cd /var/www/html/',
        'git pull',
        // 10.1.1.1からのexit
        'exit',
        // changeuserからのexit
        'exit',
      ],
      // コンソールの出力
      msg: {
        send: function(message) {
          console.log(message);
        }
      },

      // デバッグ用フラグ
      debug: false,
      // 主にデバッグ用 レスポンスを全て表示
      verbose: false,
      // タイムアウト時間。未指定だとデフォルト5000
      idleTimeOut: 5000,
      // 接続開始時のメッセージ
      connectedMessage: 'Connected',
      // コマンド実行直前に表示するメッセージ
      readyMessage: 'Running commands Now',
      // コマンド実行完了時のメッセージ
      closedMessage: 'Completed'
    };

    // su ユーザー名 コマンド実行したか？判定用
    var exeSu = false;

    // SSHインスタンス生成
    var SSH = new SSH2Shell(sshObj);

    // コマンド実行準備中の処理
    SSH.on('ready', function onReady(command, response, sshObj) {
      // 何かあれば記載
    });

    // コマンド実行が完了した場合の処理
    SSH.on('commandComplete', function onCommandComplete(command, response, sshObj) {
      // 何かあれば記載
    });

    // コマンド実行中の処理
    SSH.on('commandProcessing', function onCommandProcessing(command, response, sshObj, stream) {
      // suの実行がまだなら
      if (exeSu === false) {
        // コマンドが su ユーザー名 で ターミナルからのレスポンスが : の時
        if (command === 'su - changeuser' && response.indexOf(':') !== -1) {
          // パスワード入力して\nでエンター
          stream.write('mypassword\n');
          // su ユーザー名コマンド実行済みに
          write = true;
        }
      }
    });

    // コマンド実行完了したら
    SSH.on('end', function onEnd(sessionText, sshObj) {
      // 実行内容を表示
      sshObj.msg.send('\nThis is the full session responses:\n' + sessionText);
    });

    // SSHの接続処理
    SSH.connect();

  });
};


```

## サンプルコードの解説

```
var sshObj = {
```
の中にSSH接続と実行内容をセットしていきます。

### サーバー情報設定
```js
server: {
        host: '192.168.1.1',
        port: 22,
        userName: user,
        privateKey: fs.readFileSync('/Users/username/.ssh/id_rsa'),
      },
```

ここでは秘密鍵で接続していますが、passwordの設定もできますし、秘密鍵のパスフレーズを設定する事も可能です。

### 実行コマンドの設定

```js
commands: [
        // changeuserにユーザー切り替え
        'su - changeuser',
        // 他のサーバーにSSHログイン
        'ssh 10.1.1.1',
        'cd /var/www/html/',
        'git pull',
        // 10.1.1.1からのexit
        'exit',
        // changeuserからのexit
        'exit',
      ],
```

末尾でexitを２回していますが、きちんとexitしてあげないと、Node.jsでたまに苦しめられる write after endのエラーになってしまいます。

```
Stream error: Error: write after end
```

### コンソールの出力

```
// コンソールの出力
msg: {
   send: function(message) {
   console.log(message);
     }
},
```
ここでconsole出力しておくと、実行内容がコンソール上に表示されます。

``console.log(message);``をコメントアウトにした場合、例えば``commands: [``の中で``ll``とか``pwd``を実行しても表示されません。
逆に、こういった表示をする必要が無いのであればconsole表示はさせないでも問題ないと思います。

また、``msg: {}``　自体を削除した場合は``console.log(message);``をコメントアウトした場合と同様の結果かと思っていたのですが、逆にコンソールに実行内容が表示されました。

### SSH.onの処理

```js
// SSHインスタンス生成
var SSH = new SSH2Shell(sshObj);
```

ここで生成するSSHに対して、状況に応じた様々な処理を追加していく事ができます。
``SSH.on``の形でfunctionを作ってその中に処理を書いていきます。

サンプルでは``ready`` ``commandComplete`` ``commandProcessing`` ``end``が記載されていますが、その他、``error``や``close`` などを追加する事が可能です。

## su でユーザー変更した時の処理
通常、自分で 
``su - ユーザー名``
とコマンドを打った場合
``パスワード:``
と尋ねられると思いますが、ssh2shellではコマンド実行中の処理を制御する``SSH.on('commandProcessing'``においてこの動作を実現する事が出来ます。

### まず、このコマンドを実行したか？を判定するフラグを作ります。

```js
// su ユーザー名 コマンド実行したか？判定用
var exeSu = false;
```

### コマンド実行中の処理にsuをした時の処理を入れていきます。

```js
// コマンド実行中の処理
SSH.on('commandProcessing', function onCommandProcessing(command, response, sshObj, stream) {
  // suの実行がまだなら
  if (exeSu === false) {
    // コマンドが su ユーザー名 で ターミナルからのレスポンスが : の時
    if (command === 'su - changeuser' && response.indexOf(':') !== -1) {
      // パスワード入力して\nでエンター
      stream.write('mypassword\n');
      // su ユーザー名コマンド実行済みに
      exeSu = true;
    }
  }
});
```

ここでまずsuを実行しているか？を確認します。

```js
// suの実行がまだなら
if (exeSu === false) 
```

実行していなければ、実行中のコマンドが対象の``su - changeuser`` 且つレスポンスが``:``であるか？を確認します。

```js
// コマンドが su ユーザー名 で ターミナルからのレスポンスが : の時
if (command === 'su - changeuser' && response.indexOf(':') !== -1) {
```

このif文に合致する場合のみパスワード入力を実行します。
今回のパスワードはmypassword

```js
// パスワード入力して\nでエンター
stream.write('mypassword\n');
```

#### 重要ポイント
パスワード文字列の後の``\n``は忘れがちですが、かなり重要です。
これを入れないとエンターした事にならないため、永遠に処理が進まないです。
これに気づかず長いことハマりました。

# おわりに
バックエンド寄りのエンジニアでも毎回コマンド何度も打つのは面倒ですし、フロントエンド寄りであまり黒い画面が好きでないエンジニアも手間が省けますし、gulpのタスクでssh通信して処理を作るのはなかなか面白いと思いました。

あと、Node.jsが書ければgulpのタスクはいかようにでもなるんだなあと実感しました。