【pexpect】PythonでSSH接続とコマンド実行を自動化【対話型】

この記事の目的
- PythonでルーターやサーバーへのSSH接続を自動化したい。
- Tera Termマクロやシェルスクリプトではなく、Pythonが最善の選択肢である。
- netmiko/paramiko以外のライブラリが要件に適う。
- pexpectライブラリの実践的な使い方が知りたい。
- ルーターやサーバーに対して対話的にコマンドを実行したい。
このような方向けに、Pythonのpexpectライブラリを使ってSSH接続とコマンド実行を自動化する方法をまとめました。
ご注意事項
- 本記事における自動SSH接続プログラムの実行環境は、LinuxOS上での動作を前提としています。
- WindowsOS上では基本的に動作しません。
- WindowsOSで実行する場合は、「WSL」や「VitualBox」や「VMwareWorkstation」などを用いて、LinuxOS環境をご用意下さい。
SSH接続自動化にPythonを使う理由
ネットワーク作業自動化には、主に以下のような手段があります。
- シェルスクリプト
- Tera Termマクロ
- プログラミング言語(Python,Ruby,Perl .etc)
シェルスクリプトやTera Termマクロは、多くのインフラエンジニアが利用しています。
しかしより高度で自由度が高い処理には、純粋なプログラミング言語であるPythonが向いています。
更にPythonは初心者が習得しやすい言語と言われます。
よって、普段コードを書く機会が少ないインフラエンジニアとって、Pythonは最適な言語だと考えました。
自動化用Pythonライブラリ
ネットワーク作業が自動化出来る主なPythonのライブラリは、以下の4種類です。
ライブラリ名 | 特長 |
pexpect (ピーエクスペクト) |
|
netmiko |
|
napalm (ナパーム) |
|
telnetlib (テルネットライブ ) or (テルネットリブ) |
|
結果的に「pexpect」を使用しました。
なぜなら、ネットワーク構築作業の現場では必ずしも大手の主要ベンダーの製品が使われているとは限らないからです。
主要ベンダーの製品のみで動作する「netmiko」や「napalm」では、対応できないケースもあると考えました。
あらゆるユースケースを想定して、汎用的に使用できるライブラリが有用です。
pexpectの基本動作
使用するPythonのバージョンはPython3.6で、実行環境のOSはLinuxのCentOS7です。
# python3 --version
# Python 3.6.8
まずpipでpexpectをインストールします。
# pip install pexpect
一般には具体的なプログラムを記述する前段階として、フローチャートを作成します。
フローチャートは、プログラムの流れを図解したものです。
フローチャートでは様々な形のアイコンを用いて、実行させたい処理内容を表現します。
以下にて主なアイコンとその処理内容を一覧にしました。

続いてpexpectの基本動作を確認する実行プログラムとして、「pwd_ps.py」を作成します。
pwd_ps.pyの処理内容をまとめたフローチャートは以下の様になります。

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

処理の流れは一本道で非常にシンプルな動きとなります。
フローチャートを元に、pwd_ps.pyのコードを記述します。
# pexpectをimportする
import pexpect
# rootユーザーに切り替える
p = pexpect.spawn('env LANG=C sudo su -')
p.expect("# ")
print(p.after.decode(encoding='utf-8'))
# 現在のディレクトリを確認する
p.sendline("pwd")
p.expect("# ")
print(p.before.decode(encoding='utf-8'))
print(p.after.decode(encoding='utf-8'))
# 実行中のプロセスを一覧表示する
p.sendline("ps")
p.expect("# ")
print(p.before.decode(encoding='utf-8'))
print(p.after.decode(encoding='utf-8'))
処理内容としては、現在ログイン中のユーザーからrootユーザーに切り替え後、「pwd」と「ps」コマンドを投入し、現在のディレクトリと実行中のプロセスを確認します。
続いて、各コード内容を具体的に解説していきます。
expect関数
カッコ内の文字列を想定して応答を待ち受けます。
p.expect(“# “)ではrootユーザーのプロンプトである「#」の応答を待ち受けています。
「#」の応答が返り次第、次のコマンドの実行に進みます。
sendline関数
カッコ内の文字列を送信します。
p.sendline(“pwd")は「pwd」のコマンドを送信。
p.sendline(“ps") は「ps」のコマンドを送信。
beforeプロパティによる実行結果の出力
beforeプロパティは、expect関数で待ち受ける文字列パターンの直前までの、全てのテキストデータを保持しています。
・print(p.before.decode(encoding=’utf-8′))
print関数とbeforeプロパティを組み合わせることで、sendline関数実行後から、expect関数実行前までの出力結果が表示されます。プロンプト「#」は含まれません。
afterプロパティによる実行結果の出力
afterプロパティは、 expect関数で待ち受ける文字列パターンに一致したテキストデータを保持しています。
・print(p.after.decode(encoding=’utf-8′)
print関数とafterプロパティを組み合わせることで、expect関数で待ち受けたプロンプト「#」が出力結果として表示されます。
プログラムの実行結果
「pwd_ps.py」 を実行すると、以下のような出力が返ります。
「hostname」とディレクトリの部分は、使用環境に応じて読み替えて下さい。
#
pwd
/root
[root@hostname ~]
#
ps
PID TTY TIME CMD
32182 pts/6 00:00:00 sudo
32184 pts/6 00:00:00 su
32185 pts/6 00:00:00 bash
32200 pts/6 00:00:00 ps
[root@hostname ~]
#
beforeプロパティによる実行結果の出力
beforeプロパティは、expect関数で待ち受ける文字列パターンの直前までの、全てのテキストデータを保持しています。
・print(p.before.decode(encoding=’utf-8′))
print関数とbeforeプロパティを組み合わせることで、sendline関数実行後から、expect関数実行前までの出力結果が表示されます。プロンプト「#」は含まれません。
afterプロパティによる実行結果の出力
afterプロパティは、 expect関数で待ち受ける文字列パターンに一致したテキストデータを保持しています。
・print(p.after.decode(encoding=’utf-8′)
print関数とafterプロパティを組み合わせることで、expect関数で待ち受けたプロンプト「#」が出力結果として表示されます。
プログラムの実行結果
「pwd_ps.py」 を実行すると、以下のような出力が返ります。
「hostname」とディレクトリの部分は、使用環境に応じて読み替えて下さい。
#
pwd
/root
[root@hostname ~]
#
ps
PID TTY TIME CMD
32182 pts/6 00:00:00 sudo
32184 pts/6 00:00:00 su
32185 pts/6 00:00:00 bash
32200 pts/6 00:00:00 ps
[root@hostname ~]
#
beforeとafterの使い分けはややこしい…

補足として、beforeプロパティとafterプロパティが保持するデータの違いが分かりやすい様に、実行プラグラムにコメント記載した「#出力結果1~5」に対して、プログラムの実行結果の各出力内容を「#出力結果1~5」に紐づけました。
beforeとafterで出力させたい内容を具体的に意識しながら、コードを作成してください。
# pexpectをimportする
import pexpect
# rootユーザーに切り替える
p = pexpect.spawn('env LANG=C sudo su -')
p.expect("# ")
print(p.after.decode(encoding='utf-8')) #出力結果1
# 現在のディレクトリを確認する
p.sendline("pwd")
p.expect("# ")
print(p.before.decode(encoding='utf-8')) #出力結果2
print(p.after.decode(encoding='utf-8')) #出力結果3
# 実行中のプロセスを一覧表示する
p.sendline("ps")
p.expect("# ")
print(p.before.decode(encoding='utf-8')) #出力結果4
print(p.after.decode(encoding='utf-8')) #出力結果5
# ⇒出力結果1
pwd ⇒出力結果2
/root ⇒出力結果2
[root@hostname ~] ⇒出力結果2
# ⇒出力結果3
ps ⇒出力結果4
PID TTY TIME CMD ⇒出力結果4
32182 pts/6 00:00:00 sudo ⇒出力結果4
32184 pts/6 00:00:00 su ⇒出力結果4
32185 pts/6 00:00:00 bash ⇒出力結果4
32200 pts/6 00:00:00 ps ⇒出力結果4
[root@hostname ~] ⇒出力結果4
# ⇒出力結果5
SSH接続と設定自動化
次に、いよいよ自動でSSHログインをします。pexpectライブラリの「pxssh」モジュールを使います。
ご注意
pxsshモジュールはwindowsOSでは非対応のため、importエラーとなります。
以降の記事は、Linux環境でのプログラム実行を前提としています。
もし自宅で学習するような場合には、独自にLinux上でPythonが動作する環境をご用意ください。
「WSL」や「VitualBox」や「VMwareWorkstation」などは、手軽にWindowsPCでLinux環境が用意できます。
または、AWS等のクラウドサービスなどもLinuxサーバーのインスタンスが作成できるのでおススメです。
例えば、テスト用に最低限以下2つのサーバーを用意すれば事足ります。
- Pythonプログラムの実行サーバー:SSHで接続する側
- SSHサーバー:SSHで接続される側
これらがAWSサービスのLightsailなら、1台当たり月額5ドル前後でLinuxサーバーを構築できます。
AWSのクラウド環境下でのLinuxサーバーの知見も高まるので一石二鳥です。

Lightsailは月額利用料金が定額なので、普通に使えば高額課金の心配もありません。
事前準備
SSHで接続される側のノードでは、事前にSSHサーバーとしての設定が必要です。
具体的には、パスワード認証を許可する設定と一般ユーザーの作成をしておきます。
未設定の場合は、事前に以下のサイトを参照してご準備下さい。
・外部サイト(Qiita):CentOS7.3でSSH接続(パスワード認証)する方法
└SSHの設定
└サービスの起動
└一般ユーザーの作成
元々SSHサーバーとして起動済みだった場合には、以下のコマンドでsshd.serviceを再起動することで、設定変更を有効にしてください。
# systemctl restart sshd.service
実行プログラム
続いて、任意のSSHサーバーに接続した後に、対話的にコマンドを投入する実行プログラム として、「 ssh_test.py 」を作成します。
ssh_test.pyの処理内容をまとめたフローチャートは以下の様になります。
ssh_test.pyのコード内容をまとめたフローチャートは以下の様になります。
1つ目のプログラムと同様に、一本道の処理でループや条件分岐はありません。
フローチャートを元に、ssh_test.pyのコードを記述します。
# pexpectライブラリからpxsshモジュールをインポート
from pexpect import pxssh
# timeモジュールをインポート
import time
# ログイン情報を設定しSSHサーバーにログイン
ssh = pxssh.pxssh()
ssh.login(server="x.x.x.x", #接続したいSSHサーバーのIPを記述
username="testuser", #SSHサーバー側のユーザー名を記述
password="xxxxxxxx") #SSHサーバー側のユーザーのパスワードを記述
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果1
# テキストファイル「test.txt」を作成する
ssh.sendline("touch test.txt")
ssh.expect(r"\[.*\]\$ ")
print(ssh.before.decode(encoding='utf-8'), flush=True) #出力結果2
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果3
time.sleep(1)
# カレントディレクトリのファイルを表示する
ssh.sendline("ls -l")
ssh.expect(r"\[.*\]\$ ")
print(ssh.before.decode(encoding='utf-8'), flush=True) #出力結果4
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果5
time.sleep(1)
# SSHサーバーからログアウト
ssh.logout()
from pexpect import pxssh
「pxssh」はpexpectライブラリを拡張し、SSH接続に特化した機能を有するモジュールです。
SSHによるログインとログアウト、および接続先ノードのプロンプトを待ち受けるメソッドを追加します。
ssh = pxssh.pxssh()
ssh = pxssh.pxssh()では、pxsshモジュールのpxsshクラスから「ssh」というインスタンスを作成しています。
このsshインスタンスから各種関数を呼び出すことにより、目的となる作業を実行していきます。
1つ目のプログラムと同様に、一本道の処理でループや条件分岐はありません。
フローチャートを元に、ssh_test.pyのコードを記述します。
# pexpectライブラリからpxsshモジュールをインポート
from pexpect import pxssh
# timeモジュールをインポート
import time
# ログイン情報を設定しSSHサーバーにログイン
ssh = pxssh.pxssh()
ssh.login(server="x.x.x.x", #接続したいSSHサーバーのIPを記述
username="testuser", #SSHサーバー側のユーザー名を記述
password="xxxxxxxx") #SSHサーバー側のユーザーのパスワードを記述
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果1
# テキストファイル「test.txt」を作成する
ssh.sendline("touch test.txt")
ssh.expect(r"\[.*\]\$ ")
print(ssh.before.decode(encoding='utf-8'), flush=True) #出力結果2
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果3
time.sleep(1)
# カレントディレクトリのファイルを表示する
ssh.sendline("ls -l")
ssh.expect(r"\[.*\]\$ ")
print(ssh.before.decode(encoding='utf-8'), flush=True) #出力結果4
print(ssh.after.decode(encoding='utf-8'), flush=True) #出力結果5
time.sleep(1)
# SSHサーバーからログアウト
ssh.logout()
from pexpect import pxssh
「pxssh」はpexpectライブラリを拡張し、SSH接続に特化した機能を有するモジュールです。
SSHによるログインとログアウト、および接続先ノードのプロンプトを待ち受けるメソッドを追加します。
ssh = pxssh.pxssh()
ssh = pxssh.pxssh()では、pxsshモジュールのpxsshクラスから「ssh」というインスタンスを作成しています。
このsshインスタンスから各種関数を呼び出すことにより、目的となる作業を実行していきます。
ssh.login()
ssh.login()でsshインスタンスからlogin関数を呼び出し、作業対象のSSHサーバーにログインします。
カッコ内の引数には、接続先情報としてIPアドレスとユーザー名とパスワードを設定します。
ここではあらかじめSSHサーバー側にて、ユーザー名「testuser」でユーザーを作成しておきました。
IPアドレスとパスワードは伏字にしているので、ご利用環境に応じて読み替えて下さい。
ssh.login()
ssh.login()でsshインスタンスからlogin関数を呼び出し、作業対象のSSHサーバーにログインします。
カッコ内の引数には、接続先情報としてIPアドレスとユーザー名とパスワードを設定します。
ここではあらかじめSSHサーバー側にて、ユーザー名「testuser」でユーザーを作成しておきました。
IPアドレスとパスワードは伏字にしているので、ご利用環境に応じて読み替えて下さい。
print(ssh.after.decode(encoding=’utf-8′), flush=True)
print関数には「flush」オプションを追加しています。
flush=Trueとすることにより、CLI上でプログラム実行中の出力結果が即時表示されます。
つまり、プログラムの進捗を逐次確認するための機能となります。
これを追加しないと、全ての処理が完了してから結果が表示されます。
time.sleep(1)
timeモジュールのsleep関数にてカッコ内の引数で指定した秒数の間、実行中のプログラムの処理を一時停止できます。
もし設定しない場合、CLIの表示が速過ぎて目で追うことが困難となります。
そのため、time.sleep(1)を挿入することで敢えて1秒間のインターバルを設けています。
ssh.logout()
logout関数を呼び出してexitを送信し、SSH接続先からログアウトしてプログラムを終了します。
プログラムの実行結果
「ssh_test.py」 を実行すると、以下のような出力が返ります。
[PEXPECT]$
touch test.txt
[PEXPECT]$
ls -l
total 0
-rw-rw-r--. 1 testuser testuser 0 Dec 3 23:12 test.txt
[PEXPECT]$
touch test.txtのコマンド実行により、testuserを所有者としてtest.txtが作成されています。
ここでは非常に単純なコマンドを用いましたが、要件に応じてより複雑な処理を対話的に行うことができます。
この自由度の高さが、pexpectのメリットと言えるでしょう。
ここでもbeforeプロパティとafterプロパティの具体的な出力内容が分かりやすい様に、実行プログラムの出力結果1~5と照らし合わせます。
[PEXPECT]$ ⇒出力結果1
touch test.txt ⇒出力結果2
[PEXPECT]$ ⇒出力結果3
ls -l ⇒出力結果4
total 0 ⇒出力結果4
-rw-rw-r--. 1 testuser testuser 0 Dec 3 23:12 test.txt ⇒出力結果4
[PEXPECT]$ ⇒出力結果5
もし行き詰まったら…
この記事では、Pythonを使った自動SSH接続と設定投入の方法を解説しました。
ここまでの解説で、ご期待通りの処理は実現されましたでしょうか?
もし、この記事が一助になれたとしたら幸いです。
しかしながら、現場の業務要件やシステムは複雑で千差万別なもの。
どこかで見聞きした内容を、そっくりそのまま転用できるとは限りません。
それらしい情報をネットや書籍からかき集めて、思いつく限りの方法で散々試行錯誤したとしても、期待通りに動かず行き詰まったことはありませんか?
私も最初からスムーズに動かせたことは滅多に無く、トライアンドエラーを延々と繰り返してようやく形にできています。
では、何度試してもうまくいかず心が折れそうな時に、どうすれば最後まで諦めずに完遂できるのでしょうか?
そこで役立ったものの一つが、以下の一冊です。
「ですから、目標達成のために、まずするべきことは『意志力には限りがある』という事実を受け入れることです。」
-「やり抜く人の9つの習慣 コロンビア大学の成功の科学」第8章より引用-
この一文にはハッとさせられました。
本書によると、多くの人は自分の意志力を過大評価しており、「嫌なことから逃げて楽がしたい」という誘惑に自力で勝てると思い込んでいます。
確かに自分も、何かが上手くいかないと、いつの間にかテレビやネットやゲームに逃げていたことは数えきれないほどあります。
成功する人は、逃げ道や誘惑をなるべく排除することで、目標に集中できる「環境」を作る努力をしているのです。
本書で得た大きな知見は、意志力とは消耗することもあれば強化もできるため、その分コントロールも可能であるということ。
この概念が、目標に対する取り組み方をより楽にさせてくれました。
もし現在行き詰まっている方は、本書で何かしら打開のヒントを得ることが出来ます。
なお本記事に関するご質問やご意見等は、下記のコメント欄かTwitterのDMをご利用ください。
あわせて読みたい記事
本記事の応用として、Pythonの「pexpect」ライブラリを使うことで、複数の機器に連続でリモートSSH接続する方法をまとめました。