対話式プログラムへの自動化処理の方式としてはexpectが有名だが、そのpython版としてpexpectというパッケージがある。expectは使ったことないが、TCL言語がベースなので学習コストガー、とかshellはもうイヤだ、等々あるので、pexpectでサクッとできればそっちの方がスマートでよさそうだ。しかし現状日本語情報は少ないし、あっても説明がイマイチ分かりにくい…。単純化するとこういうことみたいである。
spawn()で、実行するコマンドを定義。expect()で、想定するプログラムの質問を定義。まさに”expect”。sendline()で、上記2.に対するアンサーを定義。
つまり、sendline()が人間が端末で入力する処理を代行してくれる、ということらしい。ではやってみる。
$ sudo pip install pexpect
分かりやすい例として、FTPサイトにログインしてリソースを取得する、というのを自動化してみる。以下は北陸先端科学技術大学院大学のFTPサイトにアクセスし、所定のディレクトリlsの結果を出力する。
ls_ftp.py
#!/bin/env python
# -*- coding: utf-8 -*-
import pexpect
child = pexpect.spawn ('ftp ftp.jaist.ac.jp')
child.expect ('Name .*: ')
child.sendline ('anonymous')
child.expect ('Password:')
child.sendline ('')
child.expect ('ftp> ')
child.sendline ('ls /pub/mysql/Downloads/MySQL-5.6/')
child.expect('ftp> ')
print child.before
child.sendline ('bye')
child.close()
実行結果。
$ python ls_ftp.py
ls /pub/mysql/Downloads/MySQL-5.6/ 229 Entering Extended Passive Mode (|||65200|). 150 Here comes the directory listing. -r--r--r-- 1 ftp ftp 215224320 May 06 20:48 MySQL-5.6.25-1.el6.i686.rpm-bundle.tar -r--r--r-- 1 ftp ftp 185 May 06 21:15 MySQL-5.6.25-1.el6.i686.rpm-bundle.tar.asc -r--r--r-- 1 ftp ftp 73 May 06 21:08 MySQL-5.6.25-1.el6.i686.rpm-bundle.tar.md5 -r--r--r-- 1 ftp ftp 31557791 May 06 17:27 MySQL-5.6.25-1.el6.src.rpm -r--r--r-- 1 ftp ftp 222781440 May 06 20:48 MySQL-5.6.25-1.el6.x86_64.rpm-bundle.tar -r--r--r-- 1 ftp ftp 185 May 06 21:15 MySQL-5.6.25-1.el6.x86_64.rpm-bundle.tar.asc -r--r--r-- 1 ftp ftp 75 May 06 21:08 MySQL-5.6.25-1.el6.x86_64.rpm-bundle.tar.md5 (略)
interact()は処理を人間の手に渡す。続けて操作したい場合は、最後のchild.sendline ('bye')をchild.interact()に変更すると、対話式モードに入る(Ctrl + Cで、ftp>プロンプトになる)。
以下は所定のリソースをダウンロード。って、最初からwgetすればいいじゃないかという話だが…、ま、例なので。
get_ftp.py
#!/bin/env python
# -*- coding: utf-8 -*-
import pexpect
import time
child = pexpect.spawn ('ftp ftp.jaist.ac.jp')
child.expect ('Name .*: ')
child.sendline ('anonymous')
child.expect ('Password:')
child.sendline ('')
child.expect ('ftp> ')
child.sendline ('cd /pub/mysql/Downloads/MySQL-5.6/')
child.expect('ftp> ')
child.sendline ('get MySQL-5.6.25-1.el6.x86_64.rpm-bundle.tar.md5')
time.sleep(0.10)
child.sendline ('bye')
child.close()
素振りはこの辺までにして、もっと実用的にLinux上 OSユーザのパスワードセット/変更例など。以下は実行時の第1引数にユーザ名、第2引数にパスワードを指定する前提。
create_password.py
#!/bin/env python
# -*- coding: utf-8 -*-
import pexpect
import sys
import time
def main():
(user, newPassword) = (sys.argv[1], sys.argv[2])
child = pexpect.spawn("/usr/bin/passwd %s" % user)
child.expect("New password:")
child.sendline(newPassword)
child.expect("Retype new password:")
child.sendline(newPassword)
time.sleep(0.1)
child.close()
if __name__ == "__main__":
main()
# python create_password.py hogeuser1 Hoge1#$
…などと実行すると、確かにパスワードがセットされた。
しかし怠け者さんは、スクリプト内でランダムにパスワード生成させてセットさせたいだろう。そうするべきだ。で、こちらをマネしてやってみた。マネというかほぼそのまんまなのだがちょっと動かない部分があったので、こうなった。今度は第1引数でユーザのみ指定。
#!/bin/env python
# -*- coding: utf-8 -*-
import pexpect
import sys
import string
import time
from random import Random
def main():
user = sys.argv[1]
global newPassword
newPassword = GeneratePassword()
print "Changing password for %s" % user
print user, ":", newPassword
ChangePassword(user, newPassword)
def GeneratePassword():
passwdChars = string.letters + string.digits + '!@#$%^&*-_+?'
passwdLength = Random().sample([10, 11, 12], 1).pop()
return ''.join(Random().sample(passwdChars, passwdLength))
def ChangePassword(user, child):
child = pexpect.spawn("/usr/bin/passwd %s" % user)
child.expect("New password:")
child.sendline(newPassword)
child.expect("Retype new password:")
child.sendline(newPassword)
time.sleep(0.1)
child.close()
if __name__ == "__main__":
main()
補足。expect()でパターンにマッチする文字列が表れないと待機状態に入って止まってしまう。expectで想定する値は正規表現が使えるので、対応させた方がいいかもしれない。
さらに複数のユーザに一度にセットしたければ、こんな感じでshellから呼び出すとか。
do_crerate_password.sh
#!/bin/bash
usersfile=/tmp/users.txt <--ユーザ名を列挙
work_dir=/tmp/passwd
cmd=$work_dir/create_password.py
pass_log=$work_dir/passfile.txt <--生成されたパスワードはここに吐き出す
for u in `cat $usersfile`
do
/usr/bin/python $cmd $u | tee -a $pass_log
done
実行してみると、確かに複数人数分一気にパスワードがセットされる。これでユーザが何人いても楽チンだ。
Ainsibleでも対話モードに対応するモジュールがあったはずなので、内部でpexpectを使っているんだろうか。構築や運用時にどうしても避けられない対話式プログラムがある場合、上手く使えるとよさげ。