対話式プログラムへの自動化処理の方式としてはexpectが有名だが、そのpython版としてpexpectというパッケージがある。expectは使ったことないが、TCL言語がベースなので学習コストガー、とかshellはもうイヤだ、等々あるので、pexpectでサクッとできればそっちの方がスマートでよさそうだ。しかし現状日本語情報は少ないし、あっても説明がイマイチ分かりにくい…。単純化するとこういうことみたいである。
spawn()
で、実行するコマンドを定義。expect()
で、想定するプログラムの質問を定義。まさに”expect”。sendline()
で、上記2.に対するアンサーを定義。
つまり、sendline()
が人間が端末で入力する処理を代行してくれる、ということらしい。ではやってみる。
$ sudo pip install pexpect
分かりやすい例として、FTPサイトにログインしてリソースを取得する、というのを自動化してみる。以下は北陸先端科学技術大学院大学のFTPサイトにアクセスし、所定のディレクトリlsの結果を出力する。
ls_ftp.py
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | #!/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
01 02 03 04 05 06 07 08 09 10 11 | 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
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | #!/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
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | #!/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引数でユーザのみ指定。
set_random_password.py
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #!/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
01 02 03 04 05 06 07 08 09 10 11 | #!/bin/bash usersfile= /tmp/users .txt <--ユーザ名を列挙 work_dir= /tmp/passwd cmd=$work_dir /set_random_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を使っているんだろうか。構築や運用時にどうしても避けられない対話式プログラムがある場合、上手く使えるとよさげ。(ちなみにAnsibleでもパスワードセットはできるが、パスワードをハッシュ化してplaybookに記述する必要があったりして、結構面倒くさい)。
追記(2015/08/01)
最後のスクリプトset_random_password.pyは、よくよく考えると対象サーバ1台だけのときはよいが、複数のサーバで各ユーザに同一パスワードをセットしたい場合には使えない。この場合はやはり予めパスワードを決めておいて、ファイルから読み出すといった処理をさせればいいんではないか。
yml形式でこんなファイルを用意する。
userlist.yml
1 2 3 | userhoge: hogepass123! userfuga: fugapass456# userpon: ponpass789$ |
以下の例は、userlist.ymlを読み込んでパスワードをセットする。iteritems()
でuserlistのkey-valueをループさせる。
set_fixed_password.py
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #!/bin/env python # -*- coding: utf-8 -*- import pexpect import yaml import sys import time def main(): userdata = yaml.load( file ( "userlist.yml" )) for user, newPassword in userdata.iteritems(): print "Set password for %s" % user 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() |
パスワードセット後はすみやかにuserlist.ymlを削除するとか、パスワードがちゃんとセットされているかのテストまで含めれば、手を動かすことはほぼない。手というのは、こういう仕事のために使うもんじゃないから(キッパリ)。