【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プロパティが保持するデータの違いが分かりやすい様にしました。
まず以下スクリプトのprint部分に、コメントで「#出力結果1~5」を追記しました。
# 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~5」に紐づけました。
つまり、以下の様な対応関係でプロンプトに「#」が表示されます。
print(p.after.decode(encoding=’utf-8′)) #出力結果1
↓
# ⇒出力結果1
# ⇒出力結果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
beforeとafterで出力させたい内容を慎重に意識しながら、コードを作成してください。
慣れれば簡単に書けるようになります。
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の「pexpect」ライブラリを使うことで、複数の機器に連続でリモートSSH接続する方法をまとめました。