LoginSignup
2
3

More than 1 year has passed since last update.

Ansibleのexpectモジュールでハマった話

Last updated at Posted at 2021-06-16

経緯

導入したいミドルウェアで以下の制約事項と要件があったため、Ansibleのexpectモジュールで解決しようとしました。

  • ミドルウェアのインストールに専用の対話式インストーラの実行が必要
    • サイレント実行のオプションは存在しない
  • インストール処理及びインストーラとコンフィグの配置は Ansible でやりたい

環境について

  • OS
    • Amazon Linux 2
  • Pythonバージョン
    • 2.7.18
  • Ansibleバージョン
    • 2.9.9

動作テストで使うもの

動作テスト用のスクリプトは以下です。
メッセージの出力後に、入力を待ち、入力があったら後続の処理を実行します。

test.sh
#!/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を実行します。

Ansibleの内容
---
- 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モジュールを使う際は以下を気を付けた方が良いことが解りました。

  • 事前にpippexpect 3.3以上をインストールする
  • responsesに書く検索文字は文字列ではなく正規表現として記載する
    • [, ], (, ), ?, . など正規表現で使われる記号は\\でエスケープする
    • 毎回エスケープするのは可読性も良くないので、記号や繰り返し出現する文字は省く
  • 入力待ちになる前に標準出力が複数行ある場合は.*を先頭に付与する

整理されてちゃんと動くようになった記法は以下の通りです。

Ansibleの内容
---
- 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のインストール

以下の通り、pippexpect 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

カッコをエスケープする

記事を参考に、カッコをエスケープしていきます。

Ansibleの内容
---
- 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回目の質問の前に、標準出力が複数行表示されているはずなので.*で回避して、末行の文字列だけマッチさせます。
また、.?はエスケープしていませんでしたが、こちらもエスケープするようにしました。

Ansibleの内容
---
- 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

エスケープが必要な記号の除去

ここまできて、実はエスケープする記号は除去したほうが可読性が良いことに気付く・・・。
以下の記載にするとよりシンプルですね。

Ansibleの内容
---
- 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モジュールの挙動が解っていればなんということはないですが、今後使う人の助けになればと思います。

2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3