なんかね。
今の仕事先ってアホみたいにサーバー台数あるんですよ。
うちが管理してる台数というかインスタンスが軽く200は超えてて、全体だと一体いくつあるんだ…?!ってくらい。
しかも、IPが固定じゃないのでインスタンス作り直すたびにIPが変わるというね。
メンテナンスする側からしたら泣いちゃうよね。
ということでサーバーアクセスの自動化をしたかったんですが、いろいろ制限があったのでこうなりました。
目次
前提
前提はこんな感じ?
制約がいっぱいあって笑うw
- PC環境
- Windows Server → Linux(踏み台) → 対象サーバーへのアクセス
- Windows Serverは自社管理じゃなくて、新しいツールの導入ができない
- 外部へのネットアクセスができないのでダウンロードも不可
- 開発環境
- ターミナルはPuTTY限定
- 使えそうなプログラミング言語のインストールなし
- Excelが入ってなくてVBAも使用不可
- でもGUI使いたい
- サーバー接続先情報
- 接続先の一覧はWEBで見れる
- でもCSVで落としたりはできない
- 対象のサーバーは200台以上
- IPはリリースでインスタンス作り直すたびに変わる
- 基本は1台だけど、まれに全台確認したい時がある
- 自社管理じゃないのでAPI叩いたりスクレイピングでごにょごにょするのは避けたい
おすすめはRLOGIN × VBAだけど…
個人的に、自動化の組み合わせでベストなのはRLOGIN × VBAだと思ってまして。
VBAでIPの管理と接続先の選択してCMDを呼び出してRLOGINを起動するって流れ。
- RLOGINだと大量にサーバー開いてもタブ切り替えなので1ウィンドウで済む
- Excelは簡単にGUI作れるしみんなが操作慣れてる
- VBAはExcel入ってれば使えるので環境構築不要
なのがいいかなって。
↓ 前にRLOGINをCMDから起動するのを書いてました
-
コマンドプロンプトでRLoginを起動してログインする
以前書いた、RLoginでサーバーにログインする方法 この方法は、RLoginの一番スタンダードな使い方だと思います。 接続設定を手動で作成する方式です。 ただ、この方法だと非常にまずい場合がありまし ...
続きを見る
実際、同じシステムに対して別のPCで自動化してた時はRLOGINでやってたんですよ。
ただ、今回のPCだとRLOGINは導入できないし、Excelも入ってない。。。
困った。。。
PuTTY × JSで自動化する
まぁダメなもんはダメなので、あるものでどうにかします。
流れとしては
- 接続先一覧のWEBサイトからIPが書いてあるテーブルタグを丸ごとコピー(手動)
- HTMLで作った作業用画面に張り付け
- JSでHTMLにテーブルを取り込む
- 対象となる接続先を選択
- 各IPやユーザーIDを使ってPuTTYのコマンドライン起動用のコマンドを作る
- 選択された台数分ループして全台アクセス
という感じ。
環境
必要なのはIEのみ。
動確はIE11でしてます。
Chromeだとローカルファイルの実行ができないので使えません。
今回はPuTTYを使ってますが、コマンドを変えればRLOGINでもTera Termでも同じことができます。
完成形
作ったコードはここに。
こんな感じのテーブルがあったとして
Chromeでテーブルタグを引っこ抜いて
画面への取り込みやフィルター機能追加などをして
接続先を選択してPutty起動ボタンを押すと
CMD経由でPuTTYでサーバーにアクセスできました。
EC2の場合、画面上に表示されるのはパブリックじゃなくプライベートIPになっちゃうから、Windowタイトル書き換えたほうがいいかも。
なぜか背後霊のようにCMDが後ろにくっついてるのは…PuTTY消すまで消えないのでもう知らない。
処理
特に難しいことはしてないんですけど。
ちなみに今回は大した量じゃないので、あまりファイル分けしてません。
この2つだけ見ればOK。
- index.html
- script.js(constもここに)
テーブルタグの取り込み
コピーしたテーブルタグを張り付けるためのテキストエリアと、テーブルを表示するための空のdivを用意して置いて
1 2 3 4 5 6 7 8 9 |
<div> <textarea id="importTableArea" rows="20" cols="80" placeholder="取り込むテーブルタグ"></textarea> </div> <div> <input id="importTableButton" type="button" value="テーブル読み込み"> </div> <div id="serversTable"> </div> |
テキストエリアの中身をそのままdivに入れてるだけ。
こうすると張り付けたテーブルタグが文字列 → テーブルオブジェクトとして扱えるようになるので、編集が楽になります。
- 先頭列にチェックボックス追加
- テーブルのフィルター用にdata-group属性を追加
- テーブルのフィルター用にグループ列の情報を収集
しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 古いテーブルを削除 $(S_SERVER_TABLE).empty() // 扱いやすくするためDivに入れる $(S_SERVER_TABLE).append(tableTag); // trにグループを設定 // 先頭セルにチェックボックス追加 var groups = []; $(S_SERVER_TABLE_TR).each(function(i) { if (i == 0) { $(this).prepend('<td><input id="' + S_COMMON_CHECKBOX.replace('#', '') + '" type="checkbox"></td>'); } else { $(this).prepend('<td><input name="check" type="checkbox" value="'+ i +'"></td>'); var group = $(this).children('td')[C_GROUP].innerText; $(this).attr('data-group', group); groups.push({'text' : group, 'val' : group}); } }); |
テーブルのフィルター
サーバーの数が多すぎるので、ある程度はフィルターで絞りたいところ。
こんな感じのドロップダウンリストを箱だけ作っておきます。
1 2 3 4 |
<div> <select id="groupSelect"></select> <option></option> </div> |
用意しておいたOptionセット用の関数にgroups{text, val}の連想配列を渡します。
連想配列のKey名を引数に変えるの忘れて汎用性が低いけど見なかったことにして。
JSって連想配列はforEachできないので、Keyだけ取り出してループ処理してます。
(JSめんどくさい第一弾)
groupsに存在するgroupの数だけオプションを追加していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// SelectにOptionをセット setSelectOption(S_GROUP_SELECT, groups); /** * 対象のSelectにOptionを設定する * @param {string} selector Selectのselector * @param {object} options 設定するOptionの連想配列{text, val} */ function setSelectOption(selector, options) { Object.keys(options).forEach( function(i) { var option = $('<option>') .text(options[i]['text']) .val(options[i]['val']); $(selector).append(option); }, options) } |
これでフィルター用のリストとテーブルが用意できたので、ドロップダウンリストのチェンジイベントでテーブルの表示行を切り替える処理を呼ぶようにします。
タグ取り込みの時に追加したdata-groupを使ってテーブルの表示行を変更。
CSSファイルで偶数行に色を付けるようにするとフィルター処理を行った後に1行おきにならない場合があるので、JSで色を付けるようにしてます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
/** * 選択された値に該当する行のみ表示する * @param {string} filter 対象の値 */ function filterList(filter) { // チェックボックスはすべてOFFにする changeCheckbox('[type="checkbox"]', false); $(S_SERVER_TABLE_TR).each(function(i) { if (i > 0) { if (filter == 'ALL') { $(this).show(); } else { $(this).attr('data-group') == filter ? $(this).show() : $(this).hide(); } } }); // 偶数行に色付け setOddColor(S_SERVER_TABLE_TR, CSS_ODD_TR_COLOR); } |
全選択 / 全解除
フィルターをかけたとはいえ数十個のチェックボックスをポチポチしていくのは辛いので、1クリックで全選択できるようにします。
すべてのチェックボックスを対象にするとフィルターで非表示になっているものまでONになってしまうので、表示されている行だけONにするようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// ヘッダーにあるチェックボックスのチェンジイベント $(document).on('change', S_COMMON_CHECKBOX, function(){ changeCheckbox('[name="check"]', $(this).prop('checked')? true : false); }); /** * 対象のチェックボックスすべての状態を変更する * @param {string} selector Checkboxのselector * @param {boolean} visibleFlg 変更後の値 */ function changeCheckbox(selector, visibleFlg) { $(selector).each(function(){ // 表示状態のものだけ変更 if ($(this).is(':visible')) { $(this).prop('checked', visibleFlg); } }); } |
ちなみに、今回のチェックボックスのようにJqueryで動的に追加した要素の場合はdocument.onで指定しないと動かないみたいです。
1 2 3 4 5 6 7 8 9 |
// HTMLに書かれている要素 → selector.onでOK $(S_IMPORT_TABLE_BUTTON).on('click', function() { }); // Jqueryで動的に追加した要素 → document.onで指定 $(document).on('change', S_COMMON_CHECKBOX, function(){ }); |
空文字チェック
必要な情報が入力されていない場合は処理が止まるようにします。
単純に空文字だったらexitすればいいだろと軽く考えていたら
JSはexitがないだと…!!
なのでtry~catchで例外投げて止めるとのこと。
でtryの中で例外投げてcatchの中でエラーメッセージ出そうとしたら
なぜか止まらない…!!
Chromeは問題なかったのでIEの問題っぽい。
しょうがないのでcatchの中でも例外を投げるようにしました。
(JSめんどくさい第二弾…というか全部IEのせい)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * 要素に値が入っているかチェックする * @param {string} selector チェック対象のselector */ function checkTextEmpty(selector) { try { if (!$(selector).val()) { alert($(selector).prop('placeholder') + 'が未入力です'); // IEの場合、これだけだとなぜか止まらない throw new Error($(selector).prop('placeholder') + 'が未入力です'); } } catch(e) { // こっちでも例外投げる throw new Error(e.message); } } |
PuTTYの起動
最後に、画面上に入力したユーザーIDやPuTTYのパス、選択したテーブルの情報を使ってPuTTY起動用のコマンドを作っていきます。
PuTTYのコマンドライン引数はここ。
今回はPuTTYにsessionを作らず直接起動。
session作った方がコマンドがスッキリするし細かい設定もできていいんだけど、RLOGINでやったときに共通のsessionを誰かが編集しちゃって環境壊れたことがあったので。。。
本当は踏み台を通す予定だったけど自宅の環境で踏み台用意するのが面倒だったので、ソースは直接接続するコマンドにしてあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/** * PuTTYでcheckされたIPに接続する * @param {string} userId userId * @param {string} userPassword userPassword * @param {string} puttyPath PuTTY.exeの絶対パス * @param {string} puttyKey PuTTYのPPKファイルの絶対パス */ function launchPutty(userId, userPassword, puttyPath, puttyKey) { // Program FilesだとCMDが動かないので置換(64bit用) puttyPath = puttyPath.replace('Program Files', 'PROGRA~1'); var wsh = new ActiveXObject("WScript.Shell"); var ip; var cmd; $('[name="check"]').each(function(){ if ($(this).is(':visible') && $(this).prop('checked')) { ip = $(this).parents('tr').children('td')[C_IP].innerText; cmd = 'cmd /c ' + puttyPath + ' -ssh ' + userId + '@' + ip + ' -i ' + puttyKey + ' & exit'; console.log(cmd); wsh.run(cmd); } }); } |
出来上がったコマンドがこれ。
ファイルパスの一部をPROGRA~1に変換してるのがポイントです。
1 |
cmd /c C:\PROGRA~1\PuTTY\putty.exe -ssh ec2-user@13.114.104.101 -i C:\Users\hazhikko\Desktop\putty\mykey.ppk & exit |
PuTTYは64bit用だとインストール先を変更しなければProgram Filesの中に入りますが、
Program FilesはCMDだと別パラメータとして分割されてエラーになる
というアホな仕様がありましてね…。
いやいやそんなフォルダ名にしたのお前らじゃん!!
って話なんですけど。
CMDで直接打つ場合はダブルクォーテーションで囲めば文字列として認識してくれますが、今回のようにプログラム内で結合する場合はわけわからなくなるので以下のように書き換えましょう。
これでファイルパスが正しく認識されます。
- 32bit環境
- Program Files → PROGRA~1
- 64bit環境
- Program Files → PROGRA~1
- Program Files (x86) → PROGRA~2
本稼働は後日
まぁ、実際に使うかもまだ未定なんですけど。
残作業としては
- 実際の接続先一覧のテーブルレイアウトに合わせて処理を修正
- 実際に動かすWindows Serverで動くか確認
ですかね。
Windows ServerはWindows10などと比べてセキュリティが高く設定されているので、設定を変えてあげないとCMDの実行ができないかもしれません。
最近のブラウザ(chromeなど)はセキュリティ観点からJSでローカルファイルを実行することはできないようになっているので、今回のツールはIEのセキュリティホールを利用してる感じなんですよね。
対応してるJSのバージョンも古くてアロー関数なんかも使えないみたいだし、IE早くなくなればいいのに。