シェル実行時にサーバーから入力待ち状態のコマンドプロンプトが発生した場合に、自動で応答できるようにするAnsibleの expect – Executes a command and responds to prompts がある。
いくつかハマリポイントがあるため、そのメモ。
Agenda
環境
- Macを使ってVagrant x Ansibleで構築
- CentOS 8
- Vagrant 2.2.6
- ansible-playbook 2.9.3
pexpectのインストール
まずpipが必要になる。いろいろな記事で $ pip install pexpect
と書かれていたが、サーバーにログインしてpipを実行すると、コマンドが見つからないと言われてしまう。
$ vagrant ssh
$ pip install pexpect
-bash: pip: command not found
pipはPythonのバージョンによって、pip2とpip3があるらしい。どれがインストールされているか確認すると、python3-pipがあったため、pip3を利用する。
$ yum list installed | grep pip
Failed to set locale, defaulting to C
libpipeline.x86_64 1.5.0-2.el8 @anaconda
platform-python-pip.noarch 9.0.3-15.el8 @BaseOS
python3-pip.noarch 9.0.3-15.el8 @AppStream
$ pip3 --version
pip 9.0.3 from /usr/lib/python3.6/site-packages (python 3.6)
$ pip3 install pexpect
でインストールすればよいことがわかったため、pexpectをAnsibleのタスクでインストールする。
- name: Install pexpect for prompts
become: yes
shell: "pip3 install pexpect"
pexpectのタスク実行
書き方は正規表現
Ansibleのドキュメント expect には、以下のようなタスクの記載例がある。
- name: Case insensitive password string match
expect:
command: passwd username
responses:
(?i)password: "MySekretPa$$word"
# you don't want to show passwords in your logs
no_log: true
- name: Generic question with multiple different responses
expect:
command: /path/to/custom/command
responses:
Question:
- response1
- response2
- response3
連続で質問に答える場合は、「Question」を利用し、一問一答であればそのままYML形式で書けばよい。このときに、質問はPythonの正規表現形式で記載する必要があるため、カッコやはてなマークなどの記号に注意する必要がある。
たとえばNG例として、以下のような質問をされた場合(Gemfileを上書きしますか?の質問)、
Overwrite /home/vagrant/sample-rails/Gemfile? (enter "h" for help) [Ynaqdhm]
そのまま質問をタスクに記述すると、以下のような記載になる。
[NG記載例]
- name: Rails new
expect:
command: "bundle exec rails new ."
responses:
Overwrite /home/vagrant/sample-rails/Gemfile? (enter "h" for help) [Ynaqdhm]: "Y"
この状態だと、正規表現が適用されてしまうため、うまく動かない。そこで、質問されたときの重要なキーワードだけ残して、それ以外は「(.*)」にすると、うまく動いた。
[正しい記載例]
- name: Rails new
expect:
command: "bundle exec rails new ."
responses:
(.*)Overwrite (.*)Gemfile(.*): "Y"
質問の引用符の有無
GitHubで「become yes expect rsponses」などのキーワードでソースコード検索すると、質問をダブルクォーテーションなどの引用符で囲むケースもあった。しかし、自分の場合は囲っても囲わなくても、結果は変わらなかった。ただし、挙動が変わったという記事も見かけたため、要注意。
なお、Ansibleの公式ドキュメントは引用符を囲まない形式で記載されている。
[Ansibleの公式ドキュメント]
- name: Case insensitive password string match
expect:
command: passwd username
responses:
(?i)password: "MySekretPa$$word"
[引用符で囲む例(正しい書き方なのかは不明)]
- name: Case insensitive password string match
expect:
command: passwd username
responses:
"(?i)password": "MySekretPa$$word"
タイムアウト
質問が発生するタスクの実行自体に時間がかかる場合、ちゃんと回答したとしても(たとえば、なにか大量にインストールするなどで時間がかかった場合など)、デフォルトのタイムアウト設定30秒を過ぎると途中でタイムアウトされてしまう。
たとえば、 $ rails new .
を実行して、Gemfileに記載されたGemをすべてインストールするタスクを実行した場合、以下のように途中でタイムアウトになってしまう。
fatal: [192.168.33.10]: FAILED! => {
"changed": true,
"cmd": "bundle exec rails new .",
"delta": "0:00:30.144455",
"end": "2020-02-22 17:10:17.228032",
"invocation": {
"module_args": {
"chdir": "/home/vagrant/sample-rails/",
"command": "bundle exec rails new .",
"creates": null,
"echo": false,
"removes": null,
"responses": {
"Overwrite {{ sample_rails_path }}Gemfile? (enter \"h\" for help) [Ynaqdhm]": "Y"
},
"timeout": 30
}
},
"msg": "command exceeded timeout",
"rc": null,
"start": "2020-02-22 17:09:47.083577",
"stdout": "\u001b[1m\u001b[34m exist\u001b[0m \r\n\u001b[1m\u001b[32m create\u001b[0m README.md\r\n\u001b[1m\u001b[32m create\u001b[0m Rakefile\r\n\u001b[1m\u001b[32m create\u001b[0m .ruby-version\r\n\u001b[1m\u001b[32m create\u001b[0m config.ru\r\n\u001b[1m\u001b[32m create\u001b[0m .gitignore\r\n\u001b[1m\u001b[31m conflict\u001b[0m Gemfile\r\nOverwrite /home/vagrant/sample-rails/Gemfile? (enter \"h\" for help) [Ynaqdhm] ",
"stdout_lines": [
"\u001b[1m\u001b[34m exist\u001b[0m ",
"\u001b[1m\u001b[32m create\u001b[0m README.md",
"\u001b[1m\u001b[32m create\u001b[0m Rakefile",
"\u001b[1m\u001b[32m create\u001b[0m .ruby-version",
"\u001b[1m\u001b[32m create\u001b[0m config.ru",
"\u001b[1m\u001b[32m create\u001b[0m .gitignore",
"\u001b[1m\u001b[31m conflict\u001b[0m Gemfile",
"Overwrite /home/vagrant/sample-rails/Gemfile? (enter \"h\" for help) [Ynaqdhm] "
]
}
サーバーにログインして、rails newして時間を測ったところ、だいたい7分ほど時間がかかることがわかった。
そこで、タイムアウト設定(timeout)を20分(1,200秒)に変更した。
[Tasks]
- name: Rails new
expect:
command: "bundle exec rails new ."
responses:
(.*)Overwrite (.*)Gemfile(.*): "Y"
timeout: 1200
そうすると、タイムアウトせずに完了できた。
なお、タイムアウト設定を外したい場合は、 timeout: null
と記載する。
ただし、コマンドプロンプトの入力待ち状態で止まってしまうと、インストールに時間がかかっているのか、入力待ち状態が延々続いていのか判別できなくなってしまうため、要注意。サーバーが入力待ちで止まっているのかかどうかは、デバッグモードの $ ansible-playbook -i hosts site.yml -vvv
でも確認ができない。