てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] Ansible 実行環境としての Python とスクリプト実行環境としての Python の指定を合わせる

はじめに

ネットワークモジュールを中心に使っていると、ターゲットノードではなくコントロールノード(Ansible実行サーバー)の Python を利用することが多いです。

あくまで個人の主観ですが、Ansible 自体の実行環境としての Python (ansible_playbook_python ) と、Playbook から生成された Python スクリプトを実行する Python(ansible_python_interpreter) の境界が、あいまいになってしまうことがあります。。。

たとえば、Ansible をインストールした venv に、モジュールの実行に必要な Python モジュールをインストールしたのに「必要なパッケージがない」旨のエラーになってしまう、ということがあります。

その都度「そっか、ansible_python_interpreter に venv のパスを指定しなきゃだと気づいて、

ansible-playbook 略  -e ansible_python_interpreter=$(which python)

のようにしていました。また別の方法があったのでご紹介します。

ansible_python_interpreter 変数に ansible_playbook_python 変数を指定する

最近、以下の記事を読んで知りました。

blue-38.hatenablog.com

ansible_python_interpreter: '{{ ansible_playbook_python }}'

なるほどと思いました。

少し調べてみると、公式ドキュメントにもこの方法が載っていました。

docs.ansible.com

Be sure to set ansible_python_interpreter: “{{ ansible_playbook_python }}” in host_vars/localhost.yml, for example.

目からうろこでした。

なお、ansible_playbook_python 変数は公式ドキュメントの https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html#magic-variables の一覧に掲載されています。

CML の Web UI で複数ノードを選択する3つの方法

はじめに

シスコの仮想ラボ環境 CMLでノードをいくつも配置していると、複数ノードまとめて起動や停止したいたいときがあります。

ラボ全体でしたら、それはそれで操作方法があるのですが「これとこれとこれだけ」という指定するには別の操作が必要です。

私が知ってる限りですが、3つの方法をご紹介します。

CML-P で 2.1.1 で試しましたが、たしか 2.0 でもできた気がします。

方法1: Shift 押しながら連続クリック

Shift 押しながらノードを連続でクリックすると、複数選択できます。

(普通にノードを選択すると1つずつの選択になります)

方法2: Shift 押しながら範囲選択

Shift 押しながらノードを範囲選択すると、その範囲内のノードが複数選択できます。

方法3: NODES 欄から選択

画面下の NODES 欄からチェックボックスで選択できます。ノード名や状態によるソートと併用できるが便利です。

f:id:akira6592:20201226182111p:plain
NODES 欄から選択

おわりに

お好みの方法でどうぞ。私は 方法1 のShift 押しながら連続クリック方法が今のところ一番多いです。

[Ansible] true か false か null かで返す値を変える ternary フィルター

はじめに

ternary フィルターは、対象の値が truefalse かに応じて別の値を返します。

Jinja2 の if をいれる必要はありません。

この記事では、簡単なサンプルでご紹介します。

  • 環境
    • ansible 2.9.14


truefalse

この2値で分ける方法です。

Playbook

ternary フィルターの第1引数には、フィルター対象が true の場合の値、第2引数には false の場合に返す値を指定します。

---
- hosts: localhost
  gather_facts: false

  tasks:
    - name: ternary test
      debug:
        msg: "{{ item | ternary('一致', '不一致') }}"
      loop:
        - "{{ (1 == 1) }}"  # true
        - "{{ (1 == 0) }}"  # false

実行

それぞれ、一致不一致 が表示されます。

TASK [ternary test] *************************************
ok: [localhost] => (item=true) => {
    "msg": "一致"
}
ok: [localhost] => (item=false) => {
    "msg": "不一致"
}


truefalsenull

ternary フィルターの第3引数には、フィルター対象が null の場合に返す値を指定します。

Playbook

先程の Playbook に加えて、第3引数を追加します。マッチさせるために、ループの最後に null を追加します。

---
- hosts: localhost
  gather_facts: false

  tasks:
    - name: ternary test
      debug:
        msg: "{{ item | ternary('一致', '不一致', 'nullです') }}"  # 第3引数を追加
      loop:
        - "{{ (1 == 1) }}"  # true
        - "{{ (1 == 0) }}"  # false
        - null  # null

実行

Playbook を実行します。null のときは、第3引数で指定した nullです が表示されます。

TASK [ternary test] ************************************
ok: [localhost] => (item=true) => {
    "msg": "一致"
}
ok: [localhost] => (item=false) => {
    "msg": "不一致"
}
ok: [localhost] => (item=None) => {
    "msg": "nullです"
}


まとめ

Jinja2 の if で埋め込んで同じことができるかもしれませんが、この程度であれば ternary フィルターのほうがすっきりして Ansible らしいのかなと思います。

参考

docs.ansible.com

[Ansible] ホストを動的にグループ化する group_by モジュール

はじめに

group_by モジュールは、Playbook 実行中に動的にグループ化できます。

用途は色々考えられますが、この記事では、複数台の Cisco IOS 機器のバージョンを収集した上で、特定のバージョン飲みを対象とする Play を実行する例をご紹介します。

  • 環境
    • ansible 2.9.14


インベントリファイル

以下の3台です。参考までに各機器のバージョンをコメントで記載します(変数ではありません)。

[cml]
ios01 ansible_host=192.168.1.11     # 15.9(3)M2
ios02 ansible_host=192.168.1.12     # 15.9(3)M2
iosxe01 ansible_host=192.168.1.13   # 17.03.01a

この3台の中で、17系である、iosxe01 に絞る Playbook を作ります。

Playbook

最初の Play で facts を収集して、その中の ansible_facts.net_version をキーにしてグループ化します。 次の Playbook で グループ ios17 のみを対象としたタスクを実行します。ここではただ facts を表示するだけです。

バージョンは 17.03.01a であれば 1715.9(3)M2 であれば 15 のように、系で扱うことにします。

---
- hosts: cml
  gather_facts: false

  tasks:
    # facts の収集、対象は最小限に
    - name: gather facts
      ios_facts:    # gather_facts: true でも代用可
        gather_subset:
          - min

    # バージョンでグループ化
    - name: group by verion
      group_by:
        key: "ios{{ ansible_facts.net_version | regex_replace('^([0-9]+)\\..+$', '\\1') }}"

# 特定のグループのみを対象とした Play
- hosts: ios17
  gather_facts: false

  tasks:
    # デバッグ
    - debug:
        msg: "{{ ansible_facts }}"


実行

Playbook を実行します。2つめの Playbook で 17 系である iosxe01 のみが実行対象になっていることが分かります。

$ ansible-playbook -i inventory.ini group_by.yml

PLAY [cml] ***********************************************************************************************

TASK [gather facts] **************************************************************************************
[WARNING]: default value for `gather_subset` will be changed to `min` from `!config` v2.11 onwards
ok: [iosxe01]
ok: [ios01]
ok: [ios02]

TASK [group by verion] ***********************************************************************************
changed: [ios01]
changed: [ios02]
changed: [iosxe01]

PLAY [ios17] *********************************************************************************************

TASK [debug] *********************************************************************************************
ok: [iosxe01] => {
    "msg": {
        "net_api": "cliconf",
        "net_gather_network_resources": [],
        "net_gather_subset": [
            "default"
        ],
        "net_hostname": "iosxe01",
        "net_image": "bootflash:packages.conf",
        "net_iostype": "IOS-XE",
        "net_model": "CSR1000V",
        "net_python_version": "3.6.7",
        "net_serialnum": "XXXXXXXX",
        "net_system": "ios",
        "net_version": "17.03.01a",
        "network_resources": {}
    }
}

PLAY RECAP ***********************************************************************************************
ios01           : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
ios02           : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
iosxe01         : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

なお、ansible-playbook コマンドに -v をつけて実行すると、group_by モジュールどのようにグループ化された確認できます。

TASK [group by verion] **********************************************************************
changed: [ios01] => {"add_group": "ios15", "changed": true, "parent_groups": ["all"]}
changed: [iosxe01] => {"add_group": "ios17", "changed": true, "parent_groups": ["all"]}
changed: [ios02] => {"add_group": "ios15", "changed": true, "parent_groups": ["all"]}


おわりに

対象ホストを絞るときに、when で条件を付けることがあると思いますが、場合によってはこのように動的にグループ化して Play を分けたほうがよいケースもあるかもしれません。

CML 2.1.0 から 2.1.1 へのアップグレード方法

はじめに

Cisco の仮想ラボ環境の CML ですが、2020年11月 に 2.1.1 がリリースされていました。

新機能追加はなく、バグフィックスです。

CML 2.1.1 Release Notes

一番気になったのは以下の点です。

The refplat-20201020-fcs.iso released with CML 2.1.0 included new versions of many of the reference platforms, but the non-crypto image of the new IOSv version was used by mistake.

refplat 内の IOSv は non-crypto image だったそうです。以前 Twitter で、IOSv に SSH できないというのを見かけたのですが、もしかしたら関係があるかもしれません。今回は CML 本体の他にも refplat も更新されています。

私の環境は、以前 CML-P 2.0 から 2.1 へアップグレードしてそのままです。ここから 2.1.1 へアップグレードしたときの手順をまとめます。


■ 1. 各種ファイルのダウンロード

Software Download (要ログイン)から CML-Personal 2.1.1 を選択し、以下の2つのファイルをダウンロードします。

f:id:akira6592:20201225204655p:plain:w400
ダウンロードする 2つのファイル
- cml2_p_controller-2.1.1-19.el8.x86_64.rpm - アップデート用 RPM - refplat_p-20201110-fcs.iso - IOSIOS XR などの各種イメージが含まれる ISO。CML 2.1 の頃は refplat_p-20201020-fcs.iso でした。

■ 2. CML コントローラーのアップグレード

RPM のアップロード

CML の Web UI にログインし、Tools > Upgrade System をクリックします。

f:id:akira6592:20201225204733p:plain:w400
Upgrade System

先ほどダウンロードした cml2_p_controller-2.1.1-19.el8.x86_64.rpm を指定して、UPLOAD IMAGE をクリックします。

f:id:akira6592:20201225204806p:plain:w400
UPLOAD IMAGE

アップロードが完了すると、Uploaded files 欄にファイル名が表示されます。確認後、using Cockpit をクリックします。(https://CMLのホスト名:9090 を開くのでもOK)

f:id:akira6592:20201225204847p:plain:w400
アップロード完了後 using Cockpit をクリック

Cockpit からアップグレード

(詳細は公式ドキュメントCML Controller Upgrade - System Administration Cockpit steps を参照)

ここからは システム管理の Web UI である Cockpit (通常通りCMLをインストールした場合 https://cmlのホスト:9090)での操作です。

「特権タスクにパスワードを再使用する」にチェックを必ず入れてログインします。(Cockpit のバージョンによって異なるかもしれませんが、とにかく特権で操作する必要があります。)

ログイン後、左メニューの CML をクリックし、Maintenance セクション内の Controller Software Upgrade を開き、Upgrade Controller をクリックします。

f:id:akira6592:20201225205619p:plain:w400
Upgrade Controller

表示されるポップアップの Upgrade をクリックします。

f:id:akira6592:20201225205706p:plain:w400
Updgrade

(権限が足りない旨のメッセージが表示されたら、右上が「管理アクセス」になっていることを確認してください。もし「制限アクセス」の場合はそれをクリックしてパスワードを入力して管理アクセスに切り替えます。)

アップグレードが始まり、しばらくすると Output セクションに Upgrade prosece done と表示されます。

f:id:akira6592:20201225205814p:plain:w400
Upgrade prosece done

これで本体のアップグレードは完了です。


■ 3. イメージファイル ISO の差し替え

続いて、イメージファイル ISO の差し替えです。

最初にダウンロードした refplat_p-20201110-fcs.isoCML を動かしている仮想マシンの仮想ドライブに割り当てます。

■ 4. 確認

(Cockpit ではなく) CML の Web 画面にログインします。ログインでのバージョン表記が無事に 2.1.1-b19 となりました。

f:id:akira6592:20201225210020p:plain:w400
Version: 2.1.1-b19


おわりに

特に困ったことがあったわけではないですが、念の為アップグレードしておきました。

refplat のダウンロード以外はそこまで時間がかかりませんでした。

[Ansible] delegate_to: localhost や connection: local 指定時の ansible_host、inventory_hostname の関係

これは、Ansible Advent Calendar 2020 (Adventar版) の 25日目の記事です。

はじめに

ターゲットノードにログインするのではなく、Ansible コントロールロードからなにか処理(APIを叩くなど)をする場合、connection: localdelegate_to をあわせて指定する場合があります。

その際の変数 ansible_hostinventory_hostname がどうなるのか、意外とわかりにくかったりしまます。私だけでしょうか・・。

よく忘れるので、試した結果をまとめておきます。検証には debug モジュールを利用します。

  • 動作確認環境
    • ansible 2.9.14


前提とするインベントリファイル

以下のインベントリファイルを利用します。比較のため、sv02ansible_host 変数を明示しないでおきます。

[sv]
sv01 ansible_host=192.168.1.1
sv02


結果まとめ

先に、結果をまとめます。

パターン sv01 ansible_host sv01 inventory_hostname sv02 ansible_host sv02 inventory_hostname
Playbook1(特に指定なし) 192.168.1.1 sv01 sv02 sv02
Playbook2(connection: local) 192.168.1.1 sv01 sv02 sv02
Playbook3(delegate_to: localhost) 192.168.1.1 sv01 localhost sv02
Playbook4(connection: localdelegate_to: localhost) 192.168.1.1 sv01 localhost sv02

ポイントとしては、delegate_to: localhost を指定すると、明示的な ansible_host 変数を定義していないホストの場合は、 ansible_hostlocalhost となる点。

検証 Playbook1: 特に指定なし

とくに表題にあるような指定はしない場合です。ごく普通の Playbook です。

---
- hosts: sv
  gather_facts: false

  tasks:
    - name: debug
      debug:
        msg:
          ansible_host: "{{ ansible_host }}"
          inventory_hostname: "{{ inventory_hostname }}"

実行結果

普通の結果です。sv02 は、ansible_host を明示していなかったので inventory_hostname の値と同じになります。

ok: [sv01] => {
    "msg": {
        "ansible_host": "192.168.1.1",
        "inventory_hostname": "sv01"
    }
}
ok: [sv02] => {
    "msg": {
        "ansible_host": "sv02",
        "inventory_hostname": "sv02"
    }
}


検証 Playbook2: connection: local を指定

以下のように、Play に connection: local を指定します。

---
- hosts: sv
  gather_facts: false
  connection: local

  tasks:
    - name: debug
      debug:
        msg:
          ansible_host: "{{ ansible_host }}"
          inventory_hostname: "{{ inventory_hostname }}"

実行結果2

実行結果1 と同じですね。

ok: [sv01] => {
    "msg": {
        "ansible_host": "192.168.1.1",
        "inventory_hostname": "sv01"
    }
}
ok: [sv02] => {
    "msg": {
        "ansible_host": "sv02",
        "inventory_hostname": "sv02"
    }
}

検証 Playbook3: delegate_to: localhost を指定

以下のように、タスクに delegate_to: localhost を指定します。

---
- hosts: sv
  gather_facts: false
  
  tasks:
    - name: debug
      debug:
        msg:
          ansible_host: "{{ ansible_host }}"
          inventory_hostname: "{{ inventory_hostname }}"
      delegate_to: localhost

実行結果3

この結果が私にとってはやや変化球です。

sv02 は、ansible_host を明示していなかったのですが、delegate_to: localhost の指定によって ansible_hostlocalhost になります。少々ねじれを感じます。

ok: [sv01] => {
    "msg": {
        "ansible_host": "192.168.1.1",
        "inventory_hostname": "sv01"
    }
}
ok: [sv02] => {
    "msg": {
        "ansible_host": "localhost",
        "inventory_hostname": "sv02"
    }
}


検証 Playbook4: connection: localdelegate_to: localhost の両方を指定

以下のように、Play に connection: local を、タスクに delegate_to: localhost を指定します。

---
- hosts: sv
  gather_facts: false
  connection: local

  tasks:
    - name: debug
      debug:
        msg:
          ansible_host: "{{ ansible_host }}"
          inventory_hostname: "{{ inventory_hostname }}"
      delegate_to: localhost

実行結果4

実行結果3 と同じです。

ok: [sv01] => {
    "msg": {
        "ansible_host": "192.168.1.1",
        "inventory_hostname": "sv01"
    }
}
ok: [sv02] => {
    "msg": {
        "ansible_host": "localhost",
        "inventory_hostname": "sv02"
    }
}


おわりに

実際は ansible_host を明示的に指定することのほうが多いのでなかなか気が付きませんでしたが、指定しない場合の挙動が意外に感じました。

[Ansible] フロースタイルでマッピング(ディクショナリ)を定義する場合の注意

これは、Ansible Advent Calendar 2020 (Adventar版) の 23日目の記事です。

はじめに

YAMLマッピング(ディクショナリ)の書き方は、大きく分けて、ブロックスタイルフロースタイルの 2つあります。

ブロックスタイルで書くケースで書くことが多いかと思いますが、フロースタイルで書くこともあるのではないでしょうか。

フロースタイルで書くと、微妙にハマる点があるのでご紹介します。

フロースタイルだと : のあとのスペースがなくてもシンタックスエラーにならない

以下の Playbook をご覧ください。

---
- hosts: all
  gather_facts: false

  vars:
    msg1: { key1: value1, key2:value2 }

  tasks:
    - name:
      debug:
        msg: "{{ msg1 }}"

key2:value2 のところは、キーが key2 でバリューがvalue2 としたつもりの箇所です。よく見ると、key2:value2 は、: のあとにスペースがありません。

これでも YAML シンタックスエラーにはならずに、実行できてしまいます。

実行結果は以下のとおりです。このように、キーが key2:value2 でバリューが null という扱いになります。

TASK [debug] ************************************
ok: [localhost] => {
    "msg": {
        "key1": "value1",
        "key2:value2": null
    }
}

このように、意図しない変数定義のまま、シンタックスエラーにならずに実行できるのは、なかなか都合が悪いのではないのでしょうか。

ブロックスタイルだどシンタックススエラーになる

ブロックスタイルで先程の定義をそれっぽく再現させると、以下のようになります。

---
- hosts: all
  gather_facts: false

  vars:
    msg1:
      key1: value1
      key2:value2   # シンタックスエラー

  tasks:
    - name:
      debug:
        msg: "{{ msg1 }}"

これはシンタックスエラーになります。

% ansible-playbook -i localhost, style.yml
ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from each:
JSON: Expecting value: line 1 column 1 (char 0)

Syntax Error while loading YAML.
  could not find expected ':'

The error appears to be in '/Users/sakana/style.yml': line 10, column 3, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:


  tasks:
  ^ here

もし、キーが key2 でバリューが value2 を意図したのであれば、このようにいっそのことシンタックスエラーになってくれたほうが嬉しいように思います。

おまけ

もし、ブロックスタイルで、キーが key2:value2 でバリューが null を定義する場合は以下のようにします。

---
- hosts: all
  gather_facts: false

  vars:
    msg1:
      key1: value1
      key2:value2:

  tasks:
    - name:
      debug:
        msg: "{{ msg1 }}"

おわりに

微妙な違いですが、ハマるとなかなか厳しい気がします。