てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] ios_config モジュールで save_when: modified 指定時に常に changed になる原因

はじめに

Cisco IOS 機器に設定コンフィグを流し込む、ios_config モジュールには、コンフィグの保存(copy running-config startup-config)する条件を指定する、save_whenをいうオプションがあります。

ここで、modified を指定すると「running-config と startup-config に差分があるときだけ copy する」という動作になります。

ところが先日、差分がないであろうタイミングでも毎回 changed になってしまう現象に遭遇しました。always ではないのに。

その原因を調べました。結果としては、よく見たら差分がありました。

  • 動作環境
  • Ansible 2.9.9
  • Cisco IOS XE Software, Version 17.01.01

現象

たとえば、以下のようなタスクを実行したときです。

    - name: save_when test
      ios_config:
        save_when: modified

1回目の実行で、コンフィグ差分があれば changed (保存した)になるのは納得できるのですが、続いて実行したときも毎回 changed (保存した)になりました。

TASK [save_when test] ********************************************************************
changed: [rt01]

※この例で、linessrc オプションの指定がないことからも分かるに、コンフィグを投入しなくても、コンフィグ保存の動作が起こるとすると changed になります。そのため「このモジュールに冪等性がなく、毎回コンフィグを投入してるのではないか」という印象になるかもしれませんがそうではありません。linessrc オプションが指定され場合は、指定されたコンフィグがすでにあるかないかを事前に調べてから投入の有無を決定します。このあたりは注意点があります。

原因

よく見たら、コンフィグ保存直後にも関わらず、running-config と startup-config に差分がありました。

意味合い的には変わらないでしょうが、証明書情報をコンフィグ内に埋め込んでいるか、NVRAM 内のファイルを参照するかの違いでした。

  • running-config
...(略)...
crypto pki certificate chain TP-self-signed-1813660109
 certificate self-signed 01
  30820330 30820218 A0030201 02020101 300D0609 2A864886 F70D0101 05050030 
...(略)...
  • startup-config
...(略)...
crypto pki certificate chain TP-self-signed-1813660109
 certificate self-signed 01 nvram:IOS-Self-Sig#1.cer
...(略)...

コンフィグによっては、他の箇所が原因になることも有り得そうです。

補足

なお、running-configと startup-config は他にも出力が異なる箇所があります。

たとえば、冒頭部分です。

  • running-config
rt01#sh running-config 
Building configuration...

Current configuration : 4356 bytes
!
! Last configuration change at 14:19:27 UTC Thu May 28 2020 by ec2-user
!
version 16.12
...(略)...
  • startup-config
rt01#sh startup-config 
Using 2439 out of 33554432 bytes
!
! Last configuration change at 14:05:56 UTC Thu May 28 2020
!
version 16.12
...(略)...

このあたりは、比較対象外にする文字列として、Ansible 內部で定義されているようです。

github.com

他にも、 ! で始まる行も比較前に除外されます。

たとえば、

Building configuration...

Current configuration : 4344 bytes
!
! Last configuration change at 14:05:56 UTC Thu May 28 2020
!
version 16.12
service timestamps debug datetime msec
...(略)...

version 16.12
service timestamps debug datetime msec
...(略)...

に、予め整形されます。

おわりに

最初は linessrc オプションと併用していたときに毎回 chagned になってしまったため、コンフィグの書き方などに気を取られていました。

今回のようなケースで困る場合は、modified ではなく changed を検討すると良さそうです。

[Ansible/AWX] ジョブテンプレートやワークフロージョブテンプレートを一括削除するワンライナー

はじめに

awx コマンドを利用すると、API を通じて Ansible Tower / AWX 上の 様々な操作ができます。

少し組み合わせて、ジョブテンプレートやワークフロージョブテンプレートを一括削除するワンライナーをご紹介します。

awx コマンドのインストールや接続情報の設定については、以下の記事を参照してください。

tekunabe.hatenablog.jp

また、json をフィルターする関係で jq をインストールしておく必要があります。

  • 動作確認環境
    • AWX 11.0.0
    • awx コマンド 11.2.0

ジョブテンプレートの一括削除

awx job_template listname 一覧を取得して、各 nameawx job_template delete します。

for jt in $(awx job_template list -f jq --filter ".results[] | .name"); do awx job_template delete ${jt}; done

ワークフロージョブテンプレートの一括削除

awx workflow_job_templates listname 一覧を取得して、各 nameawx workflow_job_templates delete します。

for wf in $(awx workflow_job_templates list -f jq --filter ".results[] | .name"); do awx workflow_job_templates delete ${wf}; done 

補足: 一覧を確認したい場合

削除する前に、一覧を確認したい場合は、以下のように実行します。

  • 例1
$ awx job_template list -f jq --filter ".results[] | .name"
jt_01_show
jt_02_debug
jt_03_debug
  • 例2
$ awx job_template list -f human
id name        
== =========== 
28 jt_01_show  
26 jt_02_debug 
27 jt_03_debug 

ワークフロージョブテンプレートの場合は、job_templateworkflow_job_templates に読み替えてください。

おわりに

両方とも、特に確認やログ出力などなく削除するため、十分にご注意ください。

jq のフィルターを工夫すると、特定の条件にマッチしたものだけ削除といった応用もできると思います。

[Ansible] コマンド版で使用している Playbook を Ansible Tower / AWX に載せる前にチェックしたいポイント

はじめに

Ansible Tower / AWX は、GUI からジョブという実行単位を経由して、Playbook を実行します。

そのため、CLI からの対話的な操作は受け付けられません。

Ansible Tower / AWX に載せる前に他の方法に変える必要があります。

vars_prompt による対話的な変数設定は Survey へ

vars_prompt、は CLI からの対話的な入力を必要とします。Ansible Tower / AWX ではそのままでは利用できません。

その代わりに、Survey という機能で、GUI で変数の設定ができます。

参考: 26. Best Practices — Ansible Tower User Guide v3.7.0

無期限 の pause は使わない

pause という、処理を止めるモジュールがあります。

CLI からは止まってる途中でも Ctrl+C で強制終了できますが、Ansible Tower / AWX ではできません。

止める必要がある場合は、タイムアウトを設定します。

参考: 26. Best Practices — Ansible Tower User Guide v3.7.0

Playbook (ジョブ)を分割してもよいのであれば、2つのジョブの間に、期限なしの Approval Node を挟むワークフローにしても良いともいます。(Ansible Tower 3.6 以降)

参考: How to add approval steps to Ansible Tower workflows

Playbook Debugger

Playbook Debugger は、Playbook の実行を途中でとめて、変数の値を表示、変更したりできるデバッガです。 これも CLI からの入力を必要とします。

あくまで、Debugger は CLI から使うという役割分担となります。

[Ansible] つまずきながら進める Ansible 【Part2】ふりかえり

はじめに

2020/05/23 に、YouTube Live でつまずいきながら進める Ansible 【Part2】という配信をしました。 実際に作業しながらエラーと戦って進めるシリーズです。

前回の Part1(動画ふりかえりブログ)では、Ansible のインストール、インベントリファイルの作成、簡単な Playbook の作成を行いました。

Playbook は Cisco IOS の機器の、show ip route コマンドの実行コマンドを、ファイルに保存するというものでした。しかし、まだ余計な情報が多かったり、思った形式になっていませんでした。

また、いきなり Playbook を作成してしまいましたが、その前に疎通確認のステップをしたほうが良かったように思います。

Part 2 では、疎通確認と、Playbook の改善をしました。

つまずいたエラーと原因、対処をふりかえります。

動画

www.youtube.com


■ 疎通確認

ping モジュールを疎通確認として利用できない

ping モジュールは、ターゲットノード(接続先)で Python が利用できるか確認するモジュールです。

よく ansible コマンド(アドホックコマンド)と併用して、Ansible としての疎通確認(ICMP ではなく、ssh + python)に利用されます。

Cisco IOSping モジュールを利用したところ、接続情報が誤っているのにもかかからず ok となってしまった。

$ ansible -i inventory.ini all -m ping
rt01 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "ping": "pong"
}

原因

Ansible 2.9 からの仕様。

ネットワーク機器向けのコネクションプラグイン利用時に ping モジュールを使っても、ターゲットノードに接続しにいかないため、接続確認として機能しない。

参考: [Ansible] ネットワーク機器に接続する条件はバージョンやコネクションブラグインによって異なる - てくなべ (tekunabe)

対処

ping モジュールではなく、実際ににネットワーク機器に接続しに行くモジュールを利用する。

$ ansible -i inventory.ini all -m ios_facts
[WARNING]: default value for `gather_subset` will be changed to `min` from `!config` v2.11 onwards
rt01 | SUCCESS => {
    "ansible_facts": {
        "ansible_net_all_ipv4_addresses": [
            "192.168.1.11",
         // ...(略)...
        ],
        "ansible_net_system": "ios",
        "ansible_net_version": "15.8(3)M2",
        "ansible_network_resources": {},
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false
}

コマンドは任意だがここでは show version

$ ansible -i inventory.ini all -m ios_command -a "commands='show version'"
rt01 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/libexec/platform-python"
    },
    "changed": false,
    "stdout": [
        // ...(略)...
    ],
    "stdout_lines": [
        [
            "Cisco IOS Software, IOSv Software (VIOS-ADVENTERPRISEK9-M), Version 15.8(3)M2, RELEASE SOFTWARE (fc2)",
            "Technical Support: http://www.cisco.com/techsupport",
            "Copyright (c) 1986-2019 by Cisco Systems, Inc.",
            "Compiled Thu 28-Mar-19 14:06 by prod_rel_team",
            "",
           // ...(略)...
            "Configuration register is 0x0"
        ]
    ]
}

参考: Network Debug and Troubleshooting Guide — Ansible Documentation

gather_facts モジュールでも可能。

setup モジュールは、ネットワーク機器に接続しにいかないため、疎通確認としては利用不可。


■ コマンド実行結果の整形

コマンド実行結果のファイルに余計な情報がは入ってしまう

{"changed": false, "stdout": ["Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP\n       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area \n       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2\n       E1 - OSPF external type 1, E2 - OSPF external type 2\n       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2\n       ia - IS-IS inter area, * - candidate default, U - per-user static route\n       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP\n       a - application route\n       + - replicated route, % - next hop override, p - overrides from PfR\n\nGateway of last resort is not set\n\n      10.0.0.0/8 is variably subnetted, 3 subnets, 2 masks\nC        10.0.0.0/24 is directly connected, GigabitEthernet0/3\nL        10.0.0.1/32 is directly connected, GigabitEthernet0/3\nC        10.255.255.1/32 is directly connected, Loopback0\n      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks\nC        192.168.1.0/24 is directly connected, GigabitEthernet0/0\nL        192.168.1.11/32 is directly connected, GigabitEthernet0/0"], "stdout_lines": [["Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP", "       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area ", "       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2", "       E1 - OSPF external type 1, E2 - OSPF external type 2", "       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2", "       ia - IS-IS inter area, * - candidate default, U - per-user static route", "       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP", "       a - application route", "       + - replicated route, % - next hop override, p - overrides from PfR", "", "Gateway of last resort is not set", "", "      10.0.0.0/8 is variably subnetted, 3 subnets, 2 masks", "C        10.0.0.0/24 is directly connected, GigabitEthernet0/3", "L        10.0.0.1/32 is directly connected, GigabitEthernet0/3", "C        10.255.255.1/32 is directly connected, Loopback0", "      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks", "C        192.168.1.0/24 is directly connected, GigabitEthernet0/0", "L        192.168.1.11/32 is directly connected, GigabitEthernet0/0"]], "failed": false}

原因

register で保存したコマンド実行結果には、コマンド実行そのもの以外にもメタ情報が含まれるため。

対処

result の内容を確認して、必要な箇所である、result.stdout[0] を指定する。

Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP
       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area 
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2
       ia - IS-IS inter area, * - candidate default, U - per-user static route
       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      10.0.0.0/8 is variably subnetted, 3 subnets, 2 masks
C        10.0.0.0/24 is directly connected, GigabitEthernet0/3
L        10.0.0.1/32 is directly connected, GigabitEthernet0/3
C        10.255.255.1/32 is directly connected, Loopback0
      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks
C        192.168.1.0/24 is directly connected, GigabitEthernet0/0
L        192.168.1.11/32 is directly connected, GigabitEthernet0/0

調べる際は、Playbook Debugger で試行錯誤できるのが便利。

    - name: save to file
      copy:
        content: "{{ result.stdout[0] }}"
        dest: "show_ip_route_{{ inventory_hostname }}.log"
      debugger: always    # タスク実行時に無条件で Playbook Debugger を起動

Playbook Debugger の使い方は、書籍「Ansible 実践ガイド 第3版」のP368「7-4-4 Playbook Debugger」にも掲載されています(宣伝)。

book.impress.co.jp


■ show コマンドの追加

show running-config コマンドが実行できない

原因

権限不足。 ログインユーザーが一般ユーザーだったため、show ip route は実行できても、how running-config は実行できない。

対処

変数定義ファイル(今回は group_vars/ios.yml)に、権限昇格のための以下の変数を追加。

ansible_become: true 
ansible_become_method: enable
ansible_become_password: secret 

参考: IOS Platform Options — Ansible Documentation


Part 3 にむけて

次回の Part 3 では、以下のアンケートで2番目に多かった、ルーティングの設定をしてみたいと思います。

[Ansible] Ansible Tower 3.7.0 でワークフローノード作成モジュール(awx.awx.tower_workflow_job_template_node)が使えるようになった

はじめに

先日、Ansible Tower 3.7 がリリースされました。

ワークフロージョブテンプレートに対して、ワークフローノードを定義する awx.awx.tower_workflow_job_template_node が、Asnible Tower 3.6 系に対しては使えなかったのですが、3.7 に試したら使えるようになってました。(もともとの使い方がうまくなかったのであればすみません・・)

もともと AWX には使えていたので、同等の実装が入ったのだと思います。

簡単なサンプルで検証します。

利用するファイル

ワークフローを定義する変数ファイルと、それを利用する Playbook です。

  • 変数定義ファイル(抜粋)
workflow_templates:
  - name: wf_show
    desicription: show command workflow
    nodes:
      - identifier: node101   # 内部識別ID
        unified_job_template: jt_show   # 実行するジョブテンプレート
        success_nodes:  # 成功時に進めるノード(緑のリンク)
          - node201
          - node202
        failure_nodes:  # 障害発生時に進めるノード(赤のリンク)
          - node209
      - identifier: node201
        unified_job_template: jt_show
        success_nodes:
          - node301
      - identifier: node202
        unified_job_template: jt_show
        success_nodes:
          - node301
      - identifier: node209
        unified_job_template: jt_show
      - identifier: node301
        unified_job_template: jt_show
  • Playbook(タスク抜粋)
    - name: create workflow templates node
      awx.awx.tower_workflow_job_template_node:
        identifier: "{{ item.1.identifier }}"
        unified_job_template: "{{ item.1.unified_job_template | default(omit) }}"  # 実行するジョブテンプレート
        workflow_job_template: "{{ item.0.name }}"        # 関連付けるワークフロージョブテンプレート名
      loop: "{{ workflow_templates | subelements('nodes') }}"
      loop_control:
        label: "{{ item.1.identifier }}"

Ansible Tower 3.6 までのエラー

3.6 までは、こんなエラーになっていました。(抜粋)

TASK [create workflow templates node] ******************************************
failed: [tower36] (item=node101) => {"ansible_loop_var": "item", "changed": false, "item": [{"allow_simultaneous": true, "desicription": "show command workflow", "extra_vars": {"wf_var1": "hello_wf1"}, "name": "wf_show", "nodes": [{"failure_nodes": ["node209"], "identifier": "node101", "success_nodes": ["node201", "node202"], "unified_job_template": "jt_show"}, {"identifier": "node201", "success_nodes": ["node301"], "unified_job_template": "jt_show"}, {"identifier": "node202", "success_nodes": ["node301"], "unified_job_template": "jt_show"}, {"identifier": "node209", "unified_job_template": "jt_show"}, {"all_parents_must_converge": true, "identifier": "node301", "unified_job_template": "jt_show"}], "survey": {"description": "", "name": "", "spec": [{"default": "new_username", "max": 1024, "min": 0, "question_description": "追加ユーザ名を入力してください", "question_name": "Enter new user name", "required": true, "type": "text", "variable": "new_username"}, {"default": "password", "max": 32, "min": 0, "question_description": "パスワードを入力してください", "question_name": "Enter new user password", "required": true, "type": "password", "variable": "new_password"}, {"choices": "operator\nadministrator", "default": "operator", "question_description": "役割を選択してください", "question_name": "Enter new user role", "required": true, "type": "multiplechoice", "variable": "new_role"}]}, "survey_enabled": true}, {"failure_nodes": ["node209"], "identifier": "node101", "success_nodes": ["node201", "node202"], "unified_job_template": "jt_show"}], "msg": "Got a 400 response when trying to get one from workflow_job_template_nodes, detail: WorkflowJobTemplateNode has no field named 'identifier'"}

おそらく、WorkflowJobTemplateNode has no field named 'identifier' がポイントで、3.6 には API 仕様としてまだこのキーがなかったということだと思います。

Ansible Tower 3.7.0 の場合(正常)

3.7.0 は以下のように正常に実行できました。

TASK [create workflow templates node] ********************************************************************
changed: [tower37] => (item=node101)
changed: [tower37] => (item=node201)
changed: [tower37] => (item=node202)
changed: [tower37] => (item=node209)
changed: [tower37] => (item=node301)

参考までに、作成されたワークフローを掲載します。(実際は、前述のタスクのあとにノード間のリンクを定義するタスクを実行済み)

f:id:akira6592:20200521213953p:plain
作成されたワークフロー

おわりに

Ansible Tower の設定を自動化できる箇所が増えてよかったです。

[Ansible] 特定バージョンの collection をインストールするには

基本コマンド

collection をインストールするコマンドは ansible-galaxy collection install です。

たとえば、cisco.ios であれば以下のとおりです。

ansible-galaxy collection install cisco.ios

この場合、最新の安定版リリースがインストールされます。

古いバージョンや、バージョン番号に dev がつくような開発版をインストールするにはバージョン指定が必要です。

cisco.ios を例にして、2つの方法をご紹介します。


■ 方法1: コマンドラインで バージョン指定する

手軽な方法です。

バージョン 0.0.1 をインストールする場合

ansible-galaxy collection install cisco.ios:0.0.1

開発バージョン 0.0.3-dev78 をインストールする場合

ansible-galaxy collection install cisco.ios:==0.0.3-dev78 


■ 方法2: requirements.yml でバージョン指定する

requirements.yml という定義ファイルに collection 名やバージョンを指定して読み込む方法です。

開発バージョン 0.0.3-dev78 をインストールする場合

  • requirements.yml
collections:
  - name: cisco.ios
    version: 0.0.3-dev78

コマンド

ansible-galaxy collection install -r requirements.yml


余談

雰囲気で pip コマンドのように cisco.ios==0.0.1 と指定してうまく行かず、調べました。 : が必要なのですね。

参考

docs.ansible.com

AWS 上の Cisco CSR1000V インスタンスにユーザーデータを指定する

はじめに

AWS で EC2 インスタンス起動時にスクリプト実行させるユーザーデータという機能があります。

サーバーが対象であれば、普通にスクリプトを書くようにコマンドを羅列すればよいわけですが、仮想ルーターなどの仮想アプライアンスの場合は、個別の指定方法があるようです。

ここでは、Cisco CSR1000V のインスタンス作成時にユーザーデータを指定する方法を試します。

インスタンス作成

Maketplace で Cisco CSR1000V を選択、インスタンスタイプを選択後「インスタンスの詳細の設定」をクリックします(確認と作成ではなく)。

f:id:akira6592:20200519172936p:plain
インスタンスタイプの選択

続いて「インスタンスの詳細の設定」画面の下の方のユーザーデータ欄にユーザーデータを入力します。

f:id:akira6592:20200519172958p:plain
ユーザーデータ入力欄

今回は、Cisco 公式ドキュメントの書き方に従って以下を入力します。

hostname="sakana"
ios-config-1="username operator privilege 1 password testpass99"
ios-config-2="username admin privilege 15 password testpass99"

ホスト名の設定と、コンフィグを2行指定しています。 デフォルトでは、ec2-user というユーザーのみなので、もう2つユーザーを追加します。

その後、通常通りインスタンス作成を進めます。

確認

とりえず、デフォルトの ec2-user でログインします。

$ ssh ec2-user@IPアドレス -i hogehoge.pem

sakana#

ユーザーデータで指定したとおり、ホスト名が sakana になりました。

一旦ログアウトして、追加したユーザーでもログインできるか確認します。

  • ユーザー: admin
$ ssh admin@IPアドレス
Password:   # 指定したパスワードを入力


sakana#
sakana#
sakana#exit
  • ユーザー: operator
$ ssh operator@IPアドレス
Password:   # 指定したパスワードを入力


sakana>
sakana>

無事にログインできました。

おわりに

この手の仮想アプライアンスはユーザーデータを指定できないと思っていたのですが、ちゃんと調べたやり方がありました。 調べてみるものですね。

www.cisco.com