2021.07.18   2022.03.03

Pexpectの使い方:基本からAWSにSSH接続してrootユーザにスイッチするまで

Python,  Linux    

この記事では、PythonのPexpectの使い方について解説します。
今回は次の方法について紹介ていきます。

expectコマンドの使い方
Pexpect(ローカル)の使い方
Pexpect(リモート)の使い方

いずれのパターンもCentoOS7またはMacにsuコマンドを実行し、rootユーザーへのスイッチを行います。

pxssh(Pexpect)を使用したssh接続の方法を確認したい方はこちら


    目次
  1. Pexpectとは
  2. expectコマンドとは
  3. expectコマンドの実行
  4. Pexpectの実行(ローカルMac or CentOS7)
  5. Pexpectの実行(ローカルからリモートのCentOS7)

Pexpectとは

対話型のプログラムなどに対して、その応答を自動化できるPythonのパッケージのことです。

Linuxにexpectというコマンドがありますが、簡単にいえばそのPython版です。

それに加えてPexpectは、サーバーにssh接続してセッションを制御することもできます。

pexpectマニュアル

expectコマンドとは

Linuxコマンドのひとつ。Pexpectと同じく任意のコマンドの返答を識別して次の入力を自動的に行なってくれます。

たとえばsuコマンドでrootユーザーにスイッチする際にパスワードを求められますが、expectコマンドを使用するとこのパスワード入力を自動化することができます。

expectコマンドの実行

Linuxのexpectコマンドの実行例を紹介します。

蛇足的な内容ですが、次章で紹介するPexpectの実行例でも同じような処理を行うので、動作イメージをつかむ参考になるかと思います。

この例は、CentOS7(Linux)で一般ユーザーからrootユーザーへのスイッチを自動化するものです。

rootユーザーへスイッチするために、CentOSでsuコマンドを実行すると次のように、Passwordを尋ねられ、入力が終わるまで待機します。

suコマンド実行例
[centos@ip-xxx-xxx-xxx-xxx]$ su
Password:

では、expectコマンドを使用してsuコマンドのパスワード入力を自動化してみます。

expectのインストール
yum install expect
書式
expect -c "
spawn [実行したいコマンド]
expect [コマンドの返答]
send [コマンドへの回答]
"
rootユーザにスイッチする
[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=Csuコマンド実行時の入力受付文の言語を統一(英語へ)するために使用しています。

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_promptPROMPTがプロンプトを設定する箇所です。

著者の環境では、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

コメント
現在コメントはありません。
コメントする
コメント入力

名前 (※ 必須)

メールアドレス (※ 必須 画面には表示されません)

送信