この記事では、PythonのPexpectの使い方について解説します。
今回は次の方法について紹介ていきます。
・expectコマンドの使い方
・Pexpect(ローカル)の使い方
・Pexpect(リモート)の使い方
いずれのパターンもCentoOS7またはMacにsuコマンドを実行し、rootユーザーへのスイッチを行います。
pxssh(Pexpect)を使用したssh接続の方法を確認したい方はこちら。
- Pexpectとは
- expectコマンドとは
- expectコマンドの実行
- Pexpectの実行(ローカルMac or CentOS7)
- Pexpectの実行(ローカルからリモートのCentOS7)
Pexpectとは
対話型のプログラムなどに対して、その応答を自動化できるPythonのパッケージのことです。
Linuxにexpectというコマンドがありますが、簡単にいえばそのPython版です。
それに加えてPexpectは、サーバーにssh接続してセッションを制御することもできます。
expectコマンドとは
Linuxコマンドのひとつ。Pexpectと同じく任意のコマンドの返答を識別して次の入力を自動的に行なってくれます。
たとえばsuコマンドでrootユーザーにスイッチする際にパスワードを求められますが、expectコマンドを使用するとこのパスワード入力を自動化することができます。
expectコマンドの実行
Linuxのexpectコマンドの実行例を紹介します。
蛇足的な内容ですが、次章で紹介するPexpectの実行例でも同じような処理を行うので、動作イメージをつかむ参考になるかと思います。
この例は、CentOS7(Linux)で一般ユーザーからrootユーザーへのスイッチを自動化するものです。
rootユーザーへスイッチするために、CentOSでsuコマンドを実行すると次のように、Passwordを尋ねられ、入力が終わるまで待機します。
[centos@ip-xxx-xxx-xxx-xxx]$ su
Password:
では、expectコマンドを使用してsuコマンドのパスワード入力を自動化してみます。
yum install expect
expect -c "
spawn [実行したいコマンド]
expect [コマンドの返答]
send [コマンドへの回答]
"
[centos@ip-xxx-xxx-xxx-xxx]$ expect -c "
spawn env LANG=C su
expect \"Password:\"
send \"rootのパスワード\n\"
interact
"
[root@ip-xxx-xxx-xxx-xxx centos]# whoami
root
spawn に入力待ちを行うコマンド(su)を設定し、
expect に入力待ち時に表示される文字列(Password:)を設定、
send に入力する文字列(今回はrootのパスワード)と改行文字を設定すると
自動でrootユーザーにスイッチしてくれます。
コマンド実行例の補足
・env LANG=C コマンド
環境変数LANGを一時的に「C」にしたうえでコマンドを実行できる。
LANG=Cとはデフォルトのロケールを使用するという意味。
プログラムの出力結果は(日本語などに)翻訳されずに、
デフォルトのまま出力されます。
・expect
コマンド実行後に期待される返答を記述する。完全一致でなくてもよい。
・send
パスワードの最後には改行コード(\n)が必要です。
・interact
現プロセスの制御をexpectからユーザーに返します。
・\"
ダブルクウォーテーションをエスケープしています。
Pexpectの実行(ローカルMac or CentOS7)
この章ではMacでPexpectのコードを実行し、一般ユーザーからrootユーザーへスイッチする例を紹介します。
想定されるプロンプト(expectの引数)を書き換えれば、CentOS7でも同じコードが使えます。
Pexpectをインストールしていない場合は、こちらのコマンドでインストールできます。
pip install pexpect
Macのターミナルでsuコマンドを実行した例
Macのターミナルでsuコマンドを実行すると次のようになります。
言語表記を統一(英語に)するため、su
コマンドと一緒に env LANG=C
コマンドも実行しています。
(base) mbp:~ user_name$ env LANG=C su
Password:
パスワード入力後、whoami
コマンドで現在のログインユーザーを確認します。
(base) mbp:~ user_name$ env LANG=C su
Password:
sh-3.2# whoami
root
ソースコードと解説
次はPythonのソースコードです。
import pexpect
# rootユーザーにスイッチする
p = pexpect.spawn('env LANG=C su')
p.expect("Password:")
p.sendline("rootのパスワード")
p.expect("sh-3.2# ")
print(p.before.decode(encoding='utf-8'))
# 現在のログインユーザーを確認する
p.sendline("whoami")
p.expect("sh-3.2# ")
print(p.before.decode(encoding='utf-8'))
このコードを実行すると次のような結果となります。
whoami
root
ソースコード解説
p = pexpect.spawn('env LANG=C su')
pexpect.spawn
には入力受付が発生するコマンドを引数に設定してインスタンス化します。
env LANG=C
は su
コマンド実行時の入力受付文の言語を統一(英語へ)するために使用しています。
p.expect("Password:")
p.sendline("rootのパスワード")
コマンド実行後にプロンプトに表示される文字列をexpect
の引数に設定します。
プログラムの動作としては、expect
の引数に指定した文字列の右側に、次のsendline
で設定する文字列がターミナルに送られて実行されるという動きをします。
p.expect("sh-3.2# ")
print(p.before.decode(encoding='utf-8'))
パスワードを入力してrootユーザーにスイッチすると、Macのプロンプトは sh-3.2#
に変更されます。
その右側の文字列をp.before
で取り出していますが、パスワード入力後は何も文字列が返ってこないので、ここでは出力結果なしとなります。
# 現在のログインユーザーを確認する
p.sendline("whoami")
p.expect("sh-3.2# ")
print(p.before.decode(encoding='utf-8'))
p.sendline("whoami")
は、whoami
コマンドをターミナルに送信する処理です。
p.expect("sh-3.2# ")
には、ターミナルのプロンプトを指定します。
最後に、print(p.before.decode(encoding='utf-8'))
と記述すると、"sh-3.2# "
の右側に表示された文字列を表示してくれます。
before
にはプロンプトを除くコマンドラインに出力された文字列が格納されますが、バイト型で格納されるため、decode(encoding='utf-8')
でバイト型を文字列型に変換しています。
printの結果は次のようになります。
whoami
root
beforeとafterの使い分け
インスタンス.before
、インスタンス.after
でpexpectの実行結果を確認できます。
sendline
関数からexpect
関数実行前までの結果はbefore
。
expect
関数実行以降の結果はafter
で確認できるようです。
コード実行例
import pexpect
# rootユーザーにスイッチする
p = pexpect.spawn('env LANG=C su')
p.expect("Password:")
p.sendline("rootのパスワードを書く")
p.expect("sh-3.2# ")
# 現在のログインユーザーを確認する
p.sendline("whoami")
p.expect("sh-3.2# ")
print(p.before.decode(encoding='utf-8'))
print(60 * "-")
print(p.after.decode(encoding='utf-8'))
print(60 * "#")
# ls -lを実行する
p.sendline("ls -l")
p.expect("sh-3.2# ") # コマンド結果が途切れる場合、p.expect(pexpect.EOF)を試す
print(p.before.decode(encoding='utf-8'))
print(60 * "-")
print(p.after.decode(encoding='utf-8'))
・実行結果
/usr/local/bin/python3/Users/hogeuser/pexpect_test.py
whoami
root
------------------------------------------------------------
sh-3.2#
############################################################
ls -l
total 264
-rw-r--r-- 1 hogeuser staff 4111 Jul 20 11:24 hoge.py
-rw-r--r-- 1 hogeuser staff 1176 Aug 2 10:48 hoge_2.py
-rw-r--r-- 1 hogeuser staff 1089 Mar 16 2021 read_json.py
------------------------------------------------------------
sh-3.2#
プロセスは終了コード 0 で完了しました
コマンドの結果が途切れる場合
ls -l
のようなコマンドで実行結果が長い場合、途中で文字列が途切れる場合があります。
そうした際は次のように記述することで、全ての結果を取り出すことができます。マニュアル
p = pexpect.spawn('ls -l')
p.expect(pexpect.EOF)
print(p.after.decode(encoding='utf-8'))
expectのコマンド実行結果の受付時間を調整する
中にはコマンドの実行結果の出力に時間がかかる処理もあると思います。
そういった場合は、expect関数の引数にtimeout=秒数
を設定するとexpectのコマンド実行結果の受付時間を調整できます。
import pexpect
# rootユーザーにスイッチする
p = pexpect.spawn('env LANG=C su')
p.expect("Password:")
p.sendline("rootのパスワード")
p.expect("sh-3.2# ")
print(p.before.decode(encoding='utf-8'))
# 現在のログインユーザーを確認する
p.sendline("whoami")
# expectのコマンド想定結果を間違ったものに変更し、結果の受付時間を10秒に設定
p.expect("hoge-3.2# ", timeout=10)
print(p.before.decode(encoding='utf-8'))
この状態でソースを実行すると、10秒後にエラーが出力されます。
Traceback (most recent call last):
・・・
pexpect.exceptions.TIMEOUT: Timeout exceeded.
<pexpect.pty_spawn.spawn object at 0x7fd6119c6fd0>
・・・
マニュアルの該当箇所はこちらです。
https://pexpect.readthedocs.io/en/stable/api/pexpect.html
Pexpectの実行(ローカルからリモートのCentOS7)
この章では、ローカルのMacからリモートAWSのCentOS7にPexpectを行う方法について紹介します。
マニュアル: pexpectでsshセッションを制御する
前章でもそうですが、プロンプトの設定はとても重要です。
きちんと設定しないと動作が遅い、動きが不安定、そもそも動作しないといった状態になるので注意が必要です。
具体的にはコード中のoriginal_prompt
、PROMPT
がプロンプトを設定する箇所です。
著者の環境では、CentOS7のプロンプトは次のようになっており、それを前提にコードを記述しています。
[centos@ip-172-11-22-33 ~]$ ← 一般ユーザー接続時のプロンプト
[root@ip-172-11-22-33 centos]# ← rootユーザー接続時のプロンプト
ソースコード
こちらがMacからAWSのCentOS7にsshでログインし、各コマンドを実行しているコードです。
from pexpect import pxssh
try:
# ログイン情報の設定とログイン
s = pxssh.pxssh()
s.login(server="55.44.222.111",
username="centos",
ssh_key="/Users/hoge/.ssh/rurukblog.pem",
original_prompt=r"\[.*\]\$ ")
# パスワードを使用したい場合はloginの引数にpassword=""を使用する。
# 現在のログインユーザーを確認(centosユーザー想定)
s.sendline('whoami')
s.prompt()
print(s.before.decode(encoding='utf-8'))
# rootユーザーにスイッチする
s.PROMPT = r"\[.*\]# "
s.sendline("env LANG=C su")
s.expect("Password:")
s.sendline("hogehoge")
s.prompt()
print(s.before.decode(encoding='utf-8'))
# 現在のログインユーザーを確認(rootユーザー想定)
s.sendline("whoami")
s.prompt()
print(s.before.decode(encoding='utf-8'))
# カレントディレクトリからtestディレクトリに移動した後lsコマンドを実行
s.sendline("cd test ; ls")
s.prompt()
print(s.before.decode(encoding='utf-8'))
# rootユーザーからexit
s.PROMPT = r"\[.*\]\$ "
s.sendline("exit")
s.prompt()
# サーバーからログアウト
s.logout()
except pxssh.ExceptionPxssh as e:
print("pxssh failed on login.")
print(e)
上記コードの実行結果は次のようになります。
whoami
centos
whoami
root
cd test ; ls
hoge1.txt hoge2.txt hoge3.txt
ソースコードの解説
from pexpect import pxssh
pexpectでssh接続を行うには、pxsshクラスを使用します。
s = pxssh.pxssh()
s.login(server="55.44.222.111",
username="centos",
ssh_key="/Users/user_name/.ssh/rurukblog.pem",
original_prompt=r"\[.*\]\$ ")
# パスワードを使用したい場合はloginの引数にpassword=""を使用する。
s = pxssh.pxssh()
で pxssh
のインスタンスを作成した後、s.login
の引数にAWSの接続情報を設定いています。
server
にはログインするサーバーのIPアドレス。
username
にはログインするユーザー名。
ssh_key
にはAWSのpemキーをフルパスで指定します。
パスワードで認証を行う場合は引数にpassword=
を使用します。
詳細についてはマニュアルに記載があります。
original_prompt
には、CentOSのプロンプト情報をなるべく正確に設定します。
著者の環境では一般ユーザーのプロンプトは、
[centos@ip-172-11-22-33 ~]$
となるので、設定は r"\[.*\]\$ "
としています。(右端に空白有り)
プロンプトに$
や#
が含まれている場合は、
original_prompt="$"
または
original_prompt="#"
でも動作しますが、意図しない動作をする場合があります。
# 現在のログインユーザーを確認(centosユーザー想定)
s.sendline('whoami')
s.prompt()
print(s.before.decode(encoding='utf-8'))
whoami
コマンドを実行し、そのコマンドの実行結果を出力しているコードです。
whoami
は現在自分がログインしているユーザーを表示するコマンドです。
prompt()
メソッドは、ローカルのPexpectで実行した、expect("引数")
メソッドへのショートカットです。 (マニュアル)
つまり、やっているこは s.expect("r"\[.*\]\$ ")
と同じです。(プロンプトの設定)
print(s.before.decode(encoding='utf-8'))
でコマンドラインの情報を取得すると次のように表示されます。
whoami
centos
次はrootユーザーにスイッチする処理です。
# rootユーザーにスイッチする
s.PROMPT = r"\[.*\]# "
s.sendline("env LANG=C su")
s.expect("Password:")
s.sendline("hogehoge")
s.prompt()
print(s.before.decode(encoding='utf-8'))
s.PROMPT = r"\[.*\]# "
でsu
実行後のプロンプトの状態を設定します。
こちらをきちんと設定しないと動作が非常に遅くなる、もしくはプログラムが動作しなくなります。
s.sendline("env LANG=C su")
で言語をデフォルト(英語)にしたsu
コマンドを実行し、
s.expect("Password:")
でプロンプトに表示される入力待ち文字列を設定します。
そのあとs.sendline("hogehoge")
でrootのパスワードを送信しています。今回パスワードはhogehoge
としています。
あとは、s.prompt()
で現在のプロンプトの状態を設定しています。
s.prompt()
を実行すると、expect([TIMEOUT, self.PROMPT], timeout=10)
が実行されるので、結果的には s.expect(r"\[.*\]# ")
が実行されているのと同じです。
また、s.before
の結果は空になるので、print
しても何も表示されません。
# 現在のログインユーザーを確認(rootユーザー想定)
s.sendline("whoami")
s.prompt()
print(s.before.decode(encoding='utf-8'))
最初に実行した処理と同じ内容です。suに成功した状態で実行すると次の結果が返ってきます。
whoami
root
今度は試しにlsコマンドを実行している処理です。
# カレントディレクトリからtestディレクトリに移動した後lsコマンドを実行
s.sendline("cd test ; ls")
s.prompt()
print(s.before.decode(encoding='utf-8'))
cd test ; ls
とすることで、testディレクトリに移動した後にlsコマンドを実行する処理になります。
こちらを実行するとtestフォルダの状態にもよりますが、次のようのな結果が返ってきます。
cd test ; ls
hoge1.txt hoge2.txt hoge3.txt
最後にログアウト処理です。
# rootユーザーからexit
s.PROMPT = r"\[.*\]\$ "
s.sendline("exit")
s.prompt()
# サーバーからログアウト
s.logout()
きちんとrootユーザーから一般ユーザーに戻しておかないと、logout()
処理でエラーとなるのでexit
コマンドを実行しています。
また、eixt
実行にプロンプトが r"\[.*\]\# "
から r"\[.*\]\$ "
へ変更されるのでここを考慮してs.PROMPT
を再設定する必要があります。
exit
処理が終わったらs.logout()
を実行してセッションを終了します。
解説は以上です。お疲れ様でした。
参考
https://pexpect.readthedocs.io/en/stable/
https://living-sun.com/ja/python/716079-libs-for-work-with-ssh-python-ssh-sudo.html