【自動化】Pythonで複数機器に連続リモートSSH接続【pexpect】

2023-01-06

Pythonで複数機器に 連続リモートSSH接続
トパーズ
トパーズ

全国各地の大量のルーターを設定変更したい…

トパーズ
トパーズ

何百台ものサーバーを1人で管理するのがしんどい…

Pythonで全部自動化できるよ。

サファイア
サファイア

ルーターやサーバーの数が多過ぎてお困りだった経験はないでしょうか?

もし数台程度なら、Tera Term等での手動作業でも出来るかもしれません。

しかしサービス規模によっては、その管理対象が数百台~数千台ということもあります。

そうなると人力での作業は現実的に不可能。

なぜなら、複数の作業要員と膨大な工数を要するからです。

そこで、各種ツールで作業を自動化するのがトレンド。

中でもPythonを用いた自動化ツールの人気が上昇しています。

しかし、Pythonでの「複数機器の作業自動化」はまだあまり情報がないので、いっそ自分で作成してみました。

結果、Pythonの「pexpect」ライブラリを使うことで、複数の機器に連続でリモートSSH接続ができました。

作業対象がどれだけ増えても、今では全く不安はありません。

この記事で、多数のルーターやサーバーに対するSSHでの作業が、Pythonで自動化できます。

ご注意事項

動作環境

  • 本記事における自動SSH接続プログラムの実行環境は、LinuxOS上での動作を前提としています。
  • WindowsOS上では基本的に動作しません。
  • WindowsOSで実行する場合は、「WSL」や「VitualBox」や「VMwareWorkstation」などを用いて、LinuxOS環境をご用意下さい。

前提知識

前提知識として、「Pythonのpexpectライブラリの基礎知識」と「SSH接続自動化の基本」について、以下の記事をご覧ください。

複数機器に自動連続SSH接続

本記事では、「pexpectライブラリとループ処理で複数機器に自動でSSH接続し、対話的にコマンドを投入するプログラム」を作成します。

プログラム名は「ssh_loop_multi_node.py」とします。

フローチャート

処理内容をまとめたフローチャートは以下の様になります。

分かりやすい様に、関数定義部分とループ処理部分の2つに分けました。

フローチャート①で定義した「ssh_work」関数をフローチャート②のループ内で呼び出して、目的の作業を繰り返し実行する流れです。

ssh_loop_multi_node.pyのフローチャート:処理内容版①
ssh_loop_multi_node.pyのフローチャート:処理内容版②

コード内容をまとめたフローチャートは以下の様になります。

なお、出力表示に関する記述は、煩雑さを避けるために敢えて割愛しています。

ssh_loop_multi_node.pyのフローチャート:コード内容版①
ssh_loop_multi_node.pyのフローチャート:コード内容版②

フローチャート②では「ループ」と「定義済み処理」の記号を使用。

この2つを利用することで、複数機器に対する連続自動SSHログインを効率良く実現できます。

コード内容

フローチャートを元に、ssh_loop_multi_node.pyのコードを記述します。

# pexpectライブラリからpxsshモジュールをインポート
from pexpect import pxssh
# timeモジュールをインポート
import time

# 作業内容をssh_work関数として定義
def ssh_work(ip_address, username, password):

    # ログイン情報を設定しSSHサーバーにログイン
    ssh = pxssh.pxssh()
    ssh.login(ip_address, username, password)
    print(ssh.after.decode(encoding='utf-8'), flush=True)

    # ログイン先のグローバルIPを表示
    ssh.sendline("curl inet-ip.info")
    ssh.expect(r"\[.*\]\$ ")
    print(ssh.before.decode(encoding='utf-8'), flush=True)
    print(ssh.after.decode(encoding='utf-8'), flush=True)
    time.sleep(1)

    # ログイン先のプライベートIPを表示
    ssh.sendline("ip -4 a")
    ssh.expect(r"\[.*\]\$ ")
    print(ssh.before.decode(encoding='utf-8'), flush=True)
    print(ssh.after.decode(encoding='utf-8'), flush=True)
    time.sleep(1)

    # ログイン先のユーザー情報を表示
    ssh.sendline("id")
    ssh.expect(r"\[.*\]\$ ")
    print(ssh.before.decode(encoding='utf-8'), flush=True)
    print(ssh.after.decode(encoding='utf-8'), flush=True)
    time.sleep(1)

    # テキストファイル「test.txt」を作成
    ssh.sendline("touch test.txt")
    ssh.expect(r"\[.*\]\$ ")
    print(ssh.before.decode(encoding='utf-8'), flush=True)
    print(ssh.after.decode(encoding='utf-8'), flush=True)
    time.sleep(1)

    # カレントディレクトリのファイルを表示
    ssh.sendline("ls -l")
    ssh.expect(r"\[.*\]\$ ")
    print(ssh.before.decode(encoding='utf-8'), flush=True)
    print(ssh.after.decode(encoding='utf-8'), flush=True)
    time.sleep(1)

    # SSHサーバーからログアウト
    ssh.logout()
    print("Logged out \n")


# 「各リストからのログイン情報読み込み」と「ssh_work関数の実行」をループ処理
with open('ip_list.txt') as ip_list, \
        open('user_list.txt') as user_list, \
        open('pw_list.txt') as pw_list:
    for ip, user, pw in zip(ip_list, user_list, pw_list):
        ssh_work(ip, user, pw)

コード内容の解説

次に、具体的なコード内容を解説していきます。

ssh_loop_multi_node.py :7行目

def ssh_work(ip_address, username, password):

まず始めに、作業内容をssh_work関数として定義します。

仮引数にはSSH接続先のログイン情報として、以下の3つを指定しています。

  • ip_address
  • username
  • password

実引数や具体的なデータの参照元は55行目以降で指定しており、詳細は後述します。

ssh_loop_multi_node.py :11行目

ssh.login(ip_address, username, password)

ssh_work関数から実引数で渡されるログイン情報を用いて、作業対象のSSHサーバーに接続します。

ssh_loop_multi_node.py :15行目

ssh.sendline(“curl inet-ip.info")

「curl inet-ip.info」は「ノード自身のグローバル IP アドレス」を表示させるコマンドです。

「ssh.sendline(“curl inet-ip.info")」により、「現在SSHで接続しているノードのグローバルIP」を表示させるコマンドを送信しています。

これは、接続中の作業対象のグローバルIPに間違いないかを確認する意図となります。

※プライベートIPで接続する場合は特段不要なコマンドです。

ssh_loop_multi_node.py :22行目

ssh.sendline(“ip -4 a")

「ip -4 a」コマンドにより、SSHで接続したノードのプライベートIPを表示させます。

「ip a」だけでは表示される情報がやや多いので、「-4」オプションを付加してIPv4の情報だけを表示させます。

これは、接続中の作業対象のプライベートIPが間違いないかを確認する意図となります。

※グローバルIPで接続する場合は特段不要なコマンドです。

ssh_loop_multi_node.py :29行目

ssh.sendline(“id")

コマンドを実行したユーザーのIDやユーザー名を表示します。

接続中のユーザーが間違いないかを確認する意図となります。

ssh_loop_multi_node.py :55~57行目

with open('ip_list.txt’) as ip_list, \
open('user_list.txt’) as user_list, ¥
open('pw_list.txt’) as pw_list:

※「¥」はバックスラッシュと読み替え下さい。

ログイン情報を3つのテキストファイルから読み込むため、with構文とopen関数を使います。

55行目でいうと、openで「ip_list.txt」ファイルを開き、asでそのファイルを「ip_list」という名前で変数として使えるようにしています。

with構文により、本来必要なファイルを閉じる処理の記述が不要となるので、不用意なエラーが回避しつつコードの簡略化が可能です。

カンマとバックスラッシュで区切ることで、複数のファイルを同時並行でopenできます。

なお事前準備として、上記コードのカッコ内の3ファイルをPythonの実行ファイルと同じディレクトリに作成して下さい。

各ファイルには、作業対象の各ログイン情報が同じ行で対応するように、IPとユーザー名とパスワードを記述します。

ip_list.txtには、各SSHサーバーのIPアドレスを記述します。複数台の作業ということで、IPアドレスは2つです。

ここでは伏字としますが、グローバルIPという想定です。実際には正しいIPアドレスを記述してください。

x.x.x.x
y.y.y.y

user_list.txtには各SSHサーバーのユーザー名を記述します。

もしSSHサーバー側でユーザーが未作成の場合は、事前に作成しておきましょう。

ここでは仮にtestuser1とtestuser2としていますが、実際には正しいユーザー名を記述してください。

testuser1
testuser2

pw_list.txtには各SSHサーバーのパスワードを記述します。

ここでは伏字としますが、実際には正しいパスワードを記述して下さい。

XXXXXXXX
YYYYYYYY
ssh_loop_multi_node.py :58行目

for ip, user, pw in zip(ip_list, user_list, pw_list):

前項の処理で変数化された3つのログイン情報をfor文で1つずつ取り出します。

複数の変数を1行のfor文でまとめて取得するために、zip関数を使います。

zip()のカッコ内の3つの要素から、ipとuserとpwを1セット取り出すようなイメージです。

各テキストファイルの上から順番に1セットずつ取り出すので、接続先の機器間でログイン情報の食い違い等は発生しません。

ssh_loop_multi_node.py :59行目

ssh_work(ip, user, pw)

7行目で定義したssh_work関数をfor文のブロックの中で実行してループさせます。

ループは処理は、3つのテキストファイルの最後の行を読み込むまで実行されます。

ssh_work関数のカッコ内には実引数を設定します。

実引数には、直前のfor文で取り出したログイン情報のセットである、以下の3つの変数を設定してください。

  1. ip
  2. user
  3. pw

この3つの実引数が、プログラム7行目の仮引数に渡され、プログラム11行目のlogin関数でSSH接続が実施されます。

プログラムの実行結果

「ssh_loop_multi_node.py」 を実行すると、以下のような出力が返ります。

[PEXPECT]$ 
curl inet-ip.info
x.x.x.x #グローバルIPが表示

[PEXPECT]$ 
ip -4 a
1: lo:~ #プライベートIPが表示
2: eth0:~ #プライベートIPが表示

[PEXPECT]$ 
id
uid=1003(testuser1) gid=1003(testuser1) groups=1003(testuser1) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

[PEXPECT]$ 
touch test.txt

[PEXPECT]$ 
ls -l
total 0
-rw-rw-r--. 1 testuser1 testuser1 0 Dec  6 13:51 test.txt

[PEXPECT]$ 
Logged out 

[PEXPECT]$ 
curl inet-ip.info
y.y.y.y

[PEXPECT]$ 
ip -4 a
1: lo:  #中略
2: eth0: #中略 

[PEXPECT]$ 
id
uid=1001(testuser2) gid=1001(testuser2) groups=1001(testuser2) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023

[PEXPECT]$ 
touch test.txt

[PEXPECT]$ 
ls -l
total 0
-rw-rw-r--. 1 testuser2 testuser2 0 Dec  6 13:51 test.txt

[PEXPECT]$ 
Logged out 

まず、グローバルIPがx.x.x.xのtestuser1のサーバーにログインできていることが、各確認コマンドから分かります。

テキストファイルの作成が、testuser1を所有者として出来ていることも確認できます。

次に、グローバルIPがy.y.y.yのtestuser2のサーバーにログインできています。

こちらも同様に、testuser2を所有者としてテキストファイルの作成が出来ています。

目的とする動作が問題なく出来ていることが確認出来ました。

実務では、作業を要するノードの数だけ、ログイン情報のテキストファイルに情報を記載して下さい。

作業内容は多岐に渡ると推察されますが、pexpectの各種関数を組み合わせれば大抵の処理は可能です。

もし行き詰まったら…

この記事では、Pythonを使っての複数ノードに対する自動ログインと設定投入の方法を解説しました。

ここまでの解説で、ご期待通りの処理は実現されましたでしょうか?

もし、この記事が一助になれたとしたら幸いです。

しかしながら、現場の業務要件やシステムは複雑で千差万別なもの。

どこかで見聞きした内容を、そっくりそのまま転用できるとは限りません。

それらしい情報をネットや書籍からかき集めて、思いつく限りの方法で散々試行錯誤したとしても、期待通りに動かず行き詰まったことはありませんか?

私も最初からスムーズに動かせたことは滅多に無く、トライアンドエラーを延々と繰り返してようやく形にできています。

では、何度試してもうまくいかず心が折れそうな時に、どうすれば最後まで諦めずに完遂できるのでしょうか?

そこで役立ったものの一つが、以下の一冊です。

「ですから、目標達成のために、まずするべきことは『意志力には限りがある』という事実を受け入れることです。」

-「やり抜く人の9つの習慣 コロンビア大学の成功の科学」第8章より引用-

この一文にはハッとさせられました。

本書によると、多くの人は自分の意志力を過大評価しており、「嫌なことから逃げて楽がしたい」という誘惑に自力で勝てると思い込んでいます。

確かに自分も、何かが上手くいかないと、いつの間にかテレビやネットやゲームに逃げていたことは数えきれないほどあります。

成功する人は、逃げ道や誘惑をなるべく排除することで、目標に集中できる「環境」を作る努力をしているのです。

本書で得た大きな知見は、意志力とは消耗することもあれば強化もできるため、その分コントロールも可能であるということ。

この概念が、目標に対する取り組み方をより楽にさせてくれました。

もし現在行き詰まっている方は、本書で何かしら打開のヒントを得ることが出来ます。

なお本記事に関するご質問やご意見等は、下記のコメント欄かTwitterのDMをご利用ください。