経緯
導入したいミドルウェアで以下の制約事項と要件があったため、Ansibleのexpectモジュールで
解決しようとしました。
- ミドルウェアのインストールに専用の対話式インストーラの実行が必要
- サイレント実行のオプションは存在しない
- インストール処理及びインストーラとコンフィグの配置は
Ansible
でやりたい
環境について
- OS
- Amazon Linux 2
- Pythonバージョン
- 2.7.18
- Ansibleバージョン
- 2.9.9
動作テストで使うもの
動作テスト用のスクリプトは以下です。
メッセージの出力後に、入力を待ち、入力があったら後続の処理を実行します。
#!/bin/bash
echo -n 'Please read the questions carefully and answer them. (y/n) [y]: '
read str
case "${str}" in
[Yy]|"Yes"|"yes")
echo "Let's begin!" ;;
[Yy]|"No"|"no")
echo "Goodbye!" ;;
*)
echo "undefined"
esac
cat << EOS
Sound the flute!
Now it's mute.
Bird's delight
Day and night;
Nightingale
In the dale,
Lark in sky,
Merrily,
Merrily, merrily, to welcome
in the year.
EOS
echo -n 'What is the name of this poem? (Spring/The Waste Land/Daffodils) [Spring]: '
read str
case "${str}" in
"Spring")
echo "You are correct." ;;
"The Waste Land")
echo "Incorrect." ;;
"Daffodils")
echo "Incorrect." ;;
*)
echo "undefined"
esac
Ansibleは以下のmain.yml
を実行します。
---
- name: Copy test.sh
copy:
src: "tmp/test.sh"
dest: "/tmp/test.sh"
owner: root
group: root
mode: 0755
- name: test expect module
expect:
command: ./test.sh
chdir: /tmp/
responses:
"Please read the questions carefully and answer them. (y/n) [y]: ": "y"
"What is the name of this poem? (Spring/The Waste Land/Daffodils) [Spring]: ": "Spring"
echo: true
色々と試した結論
色々試した結果、expectモジュール
を使う際は以下を気を付けた方が良いことが解りました。
- 事前に
pip
でpexpect 3.3
以上をインストールする -
responses
に書く検索文字は文字列
ではなく正規表現
として記載する-
[
,]
,(
,)
,?
,.
など正規表現
で使われる記号は\\
でエスケープする - 毎回エスケープするのは可読性も良くないので、記号や繰り返し出現する文字は省く
-
- 入力待ちになる前に標準出力が複数行ある場合は
.*
を先頭に付与する
整理されてちゃんと動くようになった記法は以下の通りです。
---
- name: Copy test.sh
copy:
src: "tmp/test.sh"
dest: "/tmp/test.sh"
owner: root
group: root
mode: 0755
- name: test expect module
expect:
command: ./test.sh
chdir: /tmp/
responses:
"Please read the questions carefully and answer them": "y"
".*What is the name of this poem": "Spring"
echo: true
今回ハマった内容
テスト1回目
実行結果
実行してみたところ、以下の通りエラーとなりました。
pexpect
ライブラリが無いようです。
TASK [expect_module_test : test expect module] *********************************
task path: /tmp/packer-provisioner-ansible-local/60c164e8-08f2-e040-4694-33da125f33d8/roles/expect_module_test/tasks/main.yml:10
Using module file /usr/lib/python2.7/site-packages/ansible/modules/commands/expect.py
The full traceback is:
Traceback (most recent call last):
File "/tmp/ansible_expect_payload_JdNn_2/ansible_expect_payload.zip/ansible/modules/commands/expect.py", line 108, in <module>
ImportError: No module named pexpect
fatal: [127.0.0.1]: FAILED! => changed=false
invocation:
module_args:
chdir: /tmp/
command: ./test.sh
creates: null
echo: true
removes: null
responses:
'What is the name of this poem? (Spring/The Waste Land/Daffodils) [Spring]: ': Spring
'Please read the questions carefully and answer them. (y/n) [y]: ': y
timeout: 30
msg: Failed to import the required Python library (pexpect) on ip-10-110-0-251.ap-northeast-1.compute.internal's Python /usr/bin/python. Please read module documentation and install in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter
PLAY RECAP *********************************************************************
127.0.0.1 : ok=2 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
原因調査
調べてみるとexpectモジュール
の実行にはpexpect 3.3
以上が必要とのことでした。
凡ミス・・・。
pexpect
のインストール
以下の通り、pip
でpexpect 3.3
をインストールします。
pexpect
のドキュメントを見るとPython2.7
系はpexpect 4.8
も使えるようですが、試しにpexpect 4.8
で実行したところ、expectモジュール
の実行に失敗しました。
どうもバージョンで挙動が異なる様なので、今回はpexpect 3.3
を使用しました。
$ sudo pip install pexpect==3.3
テスト2回目
実行結果
実行したところ、また失敗しました。
Please read the questions carefully and answer them. (y/n) [y]:
のメッセージ自体は想定通り出力されていますが、上手く標準出力の内容を読めていないように見えます。
TASK [expect_module_test : test expect module] *********************************
task path: /tmp/packer-provisioner-ansible-local/60c15f9d-e4a8-2f10-230d-8be76b541724/roles/expect_module_test/tasks/main.yml:10
The full traceback is:
WARNING: The below traceback may *not* be related to the actual failure.
File "/tmp/ansible_expect_payload_piodgn/ansible_expect_payload.zip/ansible/modules/commands/expect.py", line 205, in main
fatal: [127.0.0.1]: FAILED! => changed=true
cmd: ./test.sh
delta: '0:00:30.125857'
end: '2021-06-10 00:41:43.826759'
invocation:
module_args:
chdir: /tmp/
command: ./test.sh
creates: null
echo: true
removes: null
responses:
'Please read the questions carefully and answer them. (y/n) [y]: ': y
'What is the name of this poem? (Spring/The Waste Land/Daffodils) [Spring]: ': Spring
timeout: 30
msg: non-zero return code
rc: 1
start: '2021-06-10 00:41:13.700902'
stdout: 'Please read the questions carefully and answer them. (y/n) [y]: '
stdout_lines: <omitted>
PLAY RECAP *********************************************************************
127.0.0.1 : ok=2 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
原因調査
【小ネタ】Ansible expect モジュールの罠を見ると、responses
で検知する文字列の [
]
(
)
は \\
でエスケープしているようです。
ここで改めて expectモジュール
のドキュメントを読むと、responses
には期待する文字列/正規表現
と応答の文字列マッピングを記載するとかいてあります。
でも記号が入ってたら文字列
としてではなく正規表現
としてマッチしているから、正規表現
のみがマッチしているのでは?
ちょっとドキュメントの説明にモヤッとしますが、正規表現
の記号は\\
でエスケープするのが無難そう。
Mapping of expected string/regex and string to respond with. If the response is a list, successive matches return successive responses. List functionality is new in 2.1.
出典: expect – Executes a command and responds to prompts
カッコをエスケープする
記事を参考に、カッコをエスケープしていきます。
---
- name: Copy test.sh
copy:
src: "tmp/test.sh"
dest: "/tmp/test.sh"
owner: root
group: root
mode: 0755
- name: test expect module
expect:
command: ./test.sh
chdir: /tmp/
responses:
"Please read the questions carefully and answer them. \\(y/n\\) \\[y\\]: ": "y"
"What is the name of this poem? \\(Spring/The Waste Land/Daffodils\\) \\[Spring\\]: ": "Spring"
echo: true
テスト3回目
実行結果
再び失敗しました。
TASK [expect_module_test : test expect module] *********************************
task path: /tmp/packer-provisioner-ansible-local/60c160b7-a58f-c24f-40d8-1071b388514b/roles/expect_module_test/tasks/main.yml:10
The full traceback is:
WARNING: The below traceback may *not* be related to the actual failure.
File "/tmp/ansible_expect_payload_N0QzcP/ansible_expect_payload.zip/ansible/modules/commands/expect.py", line 205, in main
fatal: [127.0.0.1]: FAILED! => changed=true
cmd: ./test.sh
delta: '0:00:30.212636'
end: '2021-06-10 00:46:25.353336'
invocation:
module_args:
chdir: /tmp/
command: ./test.sh
creates: null
echo: true
removes: null
responses:
'Please read the questions carefully and answer them. \(y/n\) \[y\]: ': y
'What is the name of this poem? \(Spring/The Waste Land/Daffodils\) \[Spring\]: ': Spring
timeout: 30
msg: non-zero return code
rc: 1
start: '2021-06-10 00:45:55.140700'
stdout: |-
Please read the questions carefully and answer them. (y/n) [y]: y
Let's begin!
Sound the flute!
Now it's mute.
Bird's delight
Day and night;
Nightingale
In the dale,
Lark in sky,
Merrily,
Merrily, merrily, to welcome
in the year.
What is the name of this poem? (Spring/The Waste Land/Daffodils) [Spring]:
stdout_lines: <omitted>
PLAY RECAP *********************************************************************
127.0.0.1 : ok=2 changed=1 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
原因調査
1回目の質問についてはy
と入力していますが、後続の質問については何も入力されていない事がわかります。
入力待ちになる前に、複数行の標準出力がある場合は記載方法を変える必要があるようです。
複数行の標準出力行を回避する
2回目の質問の前に、標準出力が複数行表示されているはずなので.*
で回避して、末行の文字列だけマッチさせます。
また、.
や?
はエスケープしていませんでしたが、こちらもエスケープするようにしました。
---
- name: Copy test.sh
copy:
src: "tmp/test.sh"
dest: "/tmp/test.sh"
owner: root
group: root
mode: 0755
- name: test expect module
expect:
command: ./test.sh
chdir: /tmp/
responses:
"Please read the questions carefully and answer them\\. \\(y/n\\) \\[y\\]: ": "y"
".*What is the name of this poem\\? \\(Spring/The Waste Land/Daffodils\\) \\[Spring\\]: ": "Spring"
echo: true
テスト4回目
実行結果
以下の通り成功しました。
expectモジュール
の事例があまりなくて、少々苦労しましたが無事対話式のスクリプトをちゃんと実行することができました。
ただし、ここで気付いたのですが
TASK [expect_module_test : test expect module] *********************************
task path: /tmp/packer-provisioner-ansible-local/60c1637b-8b75-0021-f5c0-6f4b9fd137e0/roles/expect_module_test/tasks/main.yml:10
changed: [127.0.0.1] => changed=true
cmd: ./test.sh
delta: '0:00:00.234625'
end: '2021-06-10 00:57:43.057192'
invocation:
module_args:
chdir: /tmp/
command: ./test.sh
creates: null
echo: true
removes: null
responses:
'.*What is the name of this poem\? \(Spring/The Waste Land/Daffodils\) \[Spring\]: ': Spring
'Please read the questions carefully and answer them\. \(y/n\) \[y\]: ': y
timeout: 30
rc: 0
start: '2021-06-10 00:57:42.822567'
stdout: |-
Please read the questions carefully and answer them. (y/n) [y]: y
Let's begin!
Sound the flute!
Now it's mute.
Bird's delight
Day and night;
Nightingale
In the dale,
Lark in sky,
Merrily,
Merrily, merrily, to welcome
in the year.
What is the name of this poem? (Spring/The Waste Land/Daffodils) [Spring]: Spring
You are correct.
stdout_lines: <omitted>
PLAY RECAP *********************************************************************
127.0.0.1 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
エスケープが必要な記号の除去
ここまできて、実はエスケープする記号は除去したほうが可読性が良いことに気付く・・・。
以下の記載にするとよりシンプルですね。
---
- name: Copy test.sh
copy:
src: "tmp/test.sh"
dest: "/tmp/test.sh"
owner: root
group: root
mode: 0755
- name: test expect module
expect:
command: ./test.sh
chdir: /tmp/
responses:
"Please read the questions carefully and answer them": "y"
".*What is the name of this poem": "Spring"
echo: true
テスト5回目
実行結果
TASK [expect_module_test : test expect module] *********************************
task path: /tmp/packer-provisioner-ansible-local/60c1637b-8b75-0021-f5c0-6f4b9fd137e0/roles/expect_module_test/tasks/main.yml:10
changed: [127.0.0.1] => changed=true
cmd: ./test.sh
delta: '0:00:00.234625'
end: '2021-06-10 00:57:43.057192'
invocation:
module_args:
chdir: /tmp/
command: ./test.sh
creates: null
echo: true
removes: null
responses:
'.*What is the name of this poem': Spring
'Please read the questions carefully and answer them': y
timeout: 30
rc: 0
start: '2021-06-10 00:57:42.822567'
stdout: |-
Please read the questions carefully and answer them. (y/n) [y]: y
Let's begin!
Sound the flute!
Now it's mute.
Bird's delight
Day and night;
Nightingale
In the dale,
Lark in sky,
Merrily,
Merrily, merrily, to welcome
in the year.
What is the name of this poem? (Spring/The Waste Land/Daffodils) [Spring]: Spring
You are correct.
stdout_lines: <omitted>
PLAY RECAP *********************************************************************
127.0.0.1 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
おわりに
expectモジュール
の挙動が解っていればなんということはないですが、今後使う人の助けになればと思います。