zaki work log

作業ログやら生活ログやらなんやら

expectモジュールを使ったAnsibleでの対話式のプログラム実行の呼び出し

対話式のプログラムなんかをAnsibleで実行したい場合はexpectモジュールを使用する。
なお、以下のライブラリが追加で必要

  • pexpect >= 3.3

システムにインストールされていないとエラーになる。
※ インストールが必要なのはコントロールノードでなく、ターゲットノード。もちろんPlaybookでインストールするtaskを書いてよい。

TASK [expect] ******************************************************************
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: ImportError: No module named pexpect
fatal: [localhost]: FAILED! => changed=false 
  msg: Failed to import the required Python library (pexpect) on control-node's Python /usr/bin/python2. 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

入っていてもバージョンが古いとやはりエラーになる。(yumでインストールできるCentOSのPython2用のpexpectだと2.3)

TASK [expect] ******************************************************************
fatal: [localhost]: FAILED! => changed=false 
  msg: Insufficient version of pexpect installed (2.3), this module requires pexpect>=3.3. Error was 'module' object has no attribute 'runu'

なので、インストールするにはpipを使うかPython3版をyum installする。

  • Python3でyum install
  • Python3でpip install
  • Python2でpip install

whlファイルをpip2であればこんな感じ

$ sudo pip2 install ptyprocess-0.6.0-py2.py3-none-any.whl
$ sudo pip2 install pexpect-4.7.0-py2.py3-none-any.whl

yumでPython3のライブラリインストール

$ yum info python36-pexpect

Python2なAnsibleでPython3のライブラリを使う場合は以下も参照。

zaki-hmkc.hatenablog.com

実行例

プロンプト毎に値を入力する

サンプルスクリプト

#!/usr/bin/perl

use strict;

print "Username: ";
my $username = <>;
print "Password: ";
my $password = <>;

chomp($username, $password);

print "----\n";
print "input uesrname: $username\n";
print "input password: $password\n";

こんなスクリプトを用意。
実行例

[zaki@control-node expect]$ ./interactive
Username: zaki
Password: curry_tabetai
----
input uesrname: zaki
input password: curry_tabetai
[zaki@control-node expect]$

playbook

---
- hosts: localhost
  gather_facts: False
  tasks:
  - expect:
      command: /home/zaki/ansible/expect/interactive
      responses:
        "Username: " : "zaki"
        "Password: " : "curry_tabetai"

ansible-playbook実行

[zaki@control-node expect]$ ansible-playbook -i inventory.ini playbook.yml -v
Using /home/zaki/ansible/expect/ansible.cfg as config file

PLAY [localhost] ***************************************************************

TASK [expect] ******************************************************************
changed: [localhost] => changed=true
  cmd: /home/zaki/ansible/expect/interactive
  delta: '0:00:00.206796'
  end: '2020-01-15 21:05:56.902099'
  rc: 0
  start: '2020-01-15 21:05:56.695303'
  stdout: |-
    Username: Password: ----
    input uesrname: zaki
    input password: curry_tabetai
  stdout_lines: <omitted>

ちゃんとプロンプトに応じて入力が行われてる。

サンプルはlocalhostに対して処理してるけど、もちろんリモートでも実行できる。
その際、pexpect Pythonライブラリはexpect処理を行うターゲットノード上に必要

同じプロンプトに対して順番に違う値を入力する

平たく言うとシェルのように毎回同じプロンプトみたいなやつ。

playbook

やってる内容は以下の2つの処理

  • ssh remote.example.org commandで、sshの引数にコマンドも与えて、リモートホスト上のcommandコマンドを実行
  • ssh remote.example.orgsshシェルログインしてから、(expectモジュール経由で)aliasexitを実行
---
- hosts: nodes
  gather_facts: False
  tasks:
  - command: alias

- hosts: localhost
  gather_facts: False
  tasks:
  - shell: ssh 192.168.0.141 alias

- hosts: localhost
  gather_facts: False
  tasks:
  - expect:
      command: ssh 192.168.0.141
      responses:
        "\\]\\$ ":
          - "alias"
          - "exit"

リモートホスト上のプロンプトは以下のように、[username@hostname dir]$になっているので]$部分を正規表現で指定している。

[zaki@control-node expect]$ ssh 192.168.0.141
Last login: Wed Jan 15 21:21:53 2020 from 192.168.0.140
[zaki@target-node01 ~]$ hostname
target-node01
[zaki@target-node01 ~]$

実行結果

[zaki@control-node expect]$ ansible-playbook -i inventory.ini playbook.yml -v
Using /home/zaki/ansible/expect/ansible.cfg as config file

PLAY [localhost] ***************************************************************

TASK [shell] *******************************************************************
changed: [localhost] => changed=true
  cmd: ssh 192.168.0.141 alias
  delta: '0:00:00.259279'
  end: '2020-01-15 21:05:57.277030'
  rc: 0
  start: '2020-01-15 21:05:57.017751'
  stderr: ''
  stderr_lines: <omitted>
  stdout: |-
    alias egrep='egrep --color=auto'
    alias fgrep='fgrep --color=auto'
    alias grep='grep --color=auto'
    alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
  stdout_lines: <omitted>

PLAY [localhost] ***************************************************************

TASK [expect] ******************************************************************
changed: [localhost] => changed=true
  cmd: ssh 192.168.0.141
  delta: '0:00:00.475647'
  end: '2020-01-15 21:05:57.879156'
  rc: 0
  start: '2020-01-15 21:05:57.403509'
  stdout: |-
    Last login: Wed Jan 15 21:05:56 2020 from 192.168.0.140
    [zaki@target-node01 ~]$ alias egrep='egrep --color=auto'
    alias fgrep='fgrep --color=auto'
    alias grep='grep --color=auto'
    alias l.='ls -d .* --color=auto'
    alias ll='ls -l --color=auto'
    alias ls='ls --color=auto'
    alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
    [zaki@target-node01 ~]$
    Connection to 192.168.0.141 closed.
  stdout_lines: <omitted>

ちゃんと動いてる。

shellを使ってssh remote.example.org aliasについては特筆することはないので見た通り。 expectの方は"\\]\\$ "に対してaliasexitを順番に実行できている。(aliasの結果とexitの結果が出力されてる)


余談 ... aliasをぶっこ抜く

なんでこんなことやってるかとゆーと、仕事で「対象全ホストの設定情報ぜんぶ抜く」という作業があり、環境変数やらaliasやらをcommand: aliasなどで雑に集めたら「sshで手作業で確認したaliasの内容と異なるんだが!?」という指摘され、確認したら確かに差異(↑の出力もcolorオプションの部分が異なる)があってなんでやろーと調べて行ったところ、、、

/etc/profile.d/colorls.shで定義されてる、lsの色設定のエイリアスに関するスクリプト、冒頭に

# Skip all for noninteractive shells.
[ ! -t 0 ] && return

という処理があり、シェルログインみたいにインタラクティブな状態じゃない時には設定されないという定義を発見。
そのため、ssh remote.example.org aliasとやっても、このコードでreturnされ残りのalias設定がされないという。。 (CentOS7、RHEL7で確認)


ちなみにこれ、シェル上からssh -t remote.example.org aliasとやれば、期待通りの結果を得られる。


じゃあPlaybookのshellモジュール(またはcommand)に、ssh -t ...ってやればいいじゃん?と思うんだけど、これはこれで「端末からの実行じゃないのでダメ」と言われる。

TASK [shell] *******************************************************************
changed: [localhost] => changed=true 
  cmd: ssh -t 192.168.0.141 alias
  delta: '0:00:00.283072'
  end: '2020-01-15 21:44:20.438807'
  rc: 0
  start: '2020-01-15 21:44:20.155735'
  stderr: Pseudo-terminal will not be allocated because stdin is not a terminal.  stderr_lines: <omitted>
  stdout: |-
    alias egrep='egrep --color=auto'
    alias fgrep='fgrep --color=auto'
    alias grep='grep --color=auto'
    alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
  stdout_lines: <omitted>

で、shellモジュールでaliasの結果を引っこ抜くのは諦めてたんだけど、ググってみたところ「-tオプションを多重に付与すれば強制的にtty割り当てられる」という情報(というかmanだけど)を発見

 -t      Force pseudo-terminal allocation.  This can be used to execute
         arbitrary screen-based programs on a remote machine, which can be
         very useful, e.g. when implementing menu services.  Multiple -t
         options force tty allocation, even if ssh has no local tty.
- hosts: localhost
  gather_facts: False
  tasks:
  - shell: ssh -tt 192.168.0.141 alias

実行結果

TASK [shell] *******************************************************************
changed: [localhost] => changed=true
  cmd: ssh -tt 192.168.0.141 alias
  delta: '0:00:00.277060'
  end: '2020-01-15 21:52:52.725024'
  rc: 0
  start: '2020-01-15 21:52:52.447964'
  stderr: Connection to 192.168.0.141 closed.
  stderr_lines: <omitted>
  stdout: |-
    alias egrep='egrep --color=auto'
    alias fgrep='fgrep --color=auto'
    alias grep='grep --color=auto'
    alias l.='ls -d .* --color=auto'
    alias ll='ls -l --color=auto'
    alias ls='ls --color=auto'
    alias which='alias | /usr/bin/which --tty-only --read-alias --show-dot --show-tilde'
  stdout_lines: <omitted>

expect使わなくても行けるやん。。

takat.exblog.jp


alias設定スクリプト! -t 0って、動作はわかったけど、この機能の詳細、どうやってググればいいの……?