うさラボ

お勉強と備忘録

ansible.utilsコレクションのフィルタ(keep_keys/remove_keys/replace_keys)を試す

この記事はアドベントカレンダー20日目の記事です

qiita.com

はじめに

最近showコマンドの結果をパースして、パースした結果をこねくり回すことが増えてきました。 そんな中で、特定のKeyのみを残す、特定のKeyを削除する必要があり自作フィルタを使っていました 。

ansible.utils 2.5.0(2022-01-31リリース)から追加されたxxx_keys系のフィルタが自分が作っていたものとほぼ同じ動きをするフィルタだったので動作確認をして置き換えましたので紹介します。

昨年の記事で試したrouteのDiffをとる前に、データ操作としてJinja2テンプレートをいじったり、カスタムフィルタを使ったりしましたが、別解のようなイメージです。

usage-automate.hatenablog.com

keep_keysフィルタ

文字通り、指定されたKeyのみを保持するフィルタです

サンプルプレイブック

---
- hosts: localhost
  gather_facts: false

  vars:
    routes:
      - network: "195.0.0.0"
        distance: "110"
        mask: "24"
        metric: "11"
        nexthop_if: "FastEthernet0/0.100"
        nexthop_ip: "194.0.0.2"
        protocol: "O"
        type: "IA"
        uptime: "00:05:45"
      - network: "0.0.0.0"
        distance: "110"
        mask: "0"
        metric: "1"
        nexthop_if: "FastEthernet0/0.100"
        nexthop_ip: "194.0.0.2"
        protocol: "O"
        type: "E2"
        uptime: "00:05:35"

  tasks:
    - name: keep_keys
      ansible.builtin.debug:
        msg: "{{ routes | ansible.utils.keep_keys(target=['network','distance']) }}"

フィルタの変数targetにリストで渡した文字列のKeyのみを保持します

targerはregrexやstarts_with/ends_withなど正規表現を使ってマッチさせることも可能です

keep_keys(target=['^ne','distance'], matching_parameter= 'starts_with')

実行結果

TASK [keep_keys] ************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "distance": "110",
            "network": "195.0.0.0"
        },
        {
            "distance": "110",
            "network": "0.0.0.0"
        }
    ]
}

targetsに指定したnetwork/distanceのみのデータに整形できました

remove_keysフィルタ

こちら指定したキーを削除するフィルタになります

uptimeを取り除く例

---
- hosts: localhost
  gather_facts: false

  vars:
    routes:
      - network: "195.0.0.0"
        distance: "110"
        mask: "24"
        metric: "11"
        nexthop_if: "FastEthernet0/0.100"
        nexthop_ip: "194.0.0.2"
        protocol: "O"
        type: "IA"
        uptime: "00:05:45"
      - network: "0.0.0.0"
        distance: "110"
        mask: "0"
        metric: "1"
        nexthop_if: "FastEthernet0/0.100"
        nexthop_ip: "194.0.0.2"
        protocol: "O"
        type: "E2"
        uptime: "00:05:35"

  tasks:
    - name: remove_keys
      ansible.builtin.debug:
        msg: "{{ routes | ansible.utils.remove_keys(target=['uptime']) }}"

keep_keysフィルタと同様に変数targetにリストで渡します
matching_parameterも同様に利用可能です

出力

TASK [remove_keys] *************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "distance": "110",
            "mask": "24",
            "metric": "11",
            "network": "195.0.0.0",
            "nexthop_if": "FastEthernet0/0.100",
            "nexthop_ip": "194.0.0.2",
            "protocol": "O",
            "type": "IA"
        },
        {
            "distance": "110",
            "mask": "0",
            "metric": "1",
            "network": "0.0.0.0",
            "nexthop_if": "FastEthernet0/0.100",
            "nexthop_ip": "194.0.0.2",
            "protocol": "O",
            "type": "E2"
        }
    ]
}

フィルタにかけた後のデータからuptimeが消えたことが確認できます

replace_keysフィルタ

replace_keysフィルタはKeyの名前を変えることができるフィルタです

---
- hosts: localhost
  gather_facts: false

  vars:
    routes:
      - network: "195.0.0.0"
        distance: "110"
        mask: "24"
        metric: "11"
        nexthop_if: "FastEthernet0/0.100"
        nexthop_ip: "194.0.0.2"
        protocol: "O"
        type: "IA"
        uptime: "00:05:45"
      - network: "0.0.0.0"
        distance: "110"
        mask: "0"
        metric: "1"
        nexthop_if: "FastEthernet0/0.100"
        nexthop_ip: "194.0.0.2"
        protocol: "O"
        type: "E2"
        uptime: "00:05:35"

  tasks:
    - name: replace_keys
      ansible.builtin.debug:
        msg: "{{ routes | ansible.utils.replace_keys(target=[{'before':'nexthop_if', 'after':'nexthop_interface'}]) }}"

フィルタの変数targetsには[{before: XXX, after: XXXZ}]とどのKeyを何に変えるかを定義する必要があります
matching_parameterはkeep_keys/remove_keysと同様に利用可能です

nexthop_ifのKeyをnexthop_interfaceに変更します。

出力

TASK [replace_keys] *********************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "distance": "110",
            "mask": "24",
            "metric": "11",
            "network": "195.0.0.0",
            "nexthop_interface": "FastEthernet0/0.100",
            "nexthop_ip": "194.0.0.2",
            "protocol": "O",
            "type": "IA",
            "uptime": "00:05:45"
        },
        {
            "distance": "110",
            "mask": "0",
            "metric": "1",
            "network": "0.0.0.0",
            "nexthop_interface": "FastEthernet0/0.100",
            "nexthop_ip": "194.0.0.2",
            "protocol": "O",
            "type": "E2",
            "uptime": "00:05:35"
        }
    ]
}

nexthop_ifをnexthop_interfaceに変更できました。

まとめ

ansible.utilsの3つのフィルタを紹介しました
コレクションで提供されている便利なフィルタは積極的に使っていきたいですね。

ちなみに

書いてる時に思い出しましたがすべて、json_queryでもできますね笑

---
- hosts: localhost
  gather_facts: false

  vars:
    routes:
      - network: "195.0.0.0"
        distance: "110"
        mask: "24"
        metric: "11"
        nexthop_if: "FastEthernet0/0.100"
        nexthop_ip: "194.0.0.2"
        protocol: "O"
        type: "IA"
        uptime: "00:05:45"
      - network: "0.0.0.0"
        distance: "110"
        mask: "0"
        metric: "1"
        nexthop_if: "FastEthernet0/0.100"
        nexthop_ip: "194.0.0.2"
        protocol: "O"
        type: "E2"
        uptime: "00:05:35"

  tasks:
    - name: json_query(keep_keys)
      ansible.builtin.debug:
        msg: "{{ routes | community.general.json_query(query_string) }}"
      vars:
        query_string: "[*].{network: network, distance: distance}"

    - name: json_query(remove_keys)
      ansible.builtin.debug:
        msg: "{{ routes | community.general.json_query(query_string) }}"
      vars:
        query_string: "[*].{network: network,
                            distance: distance,
                            mask: mask,
                            metric: metric,
                            nexthop_if: nexthop_if,
                            nexthop_ip: nexthop_ip,
                            protocol: protocol,
                            type: type}"

    - name: json_query(replace_keys)
      ansible.builtin.debug:
        msg: "{{ routes | community.general.json_query(query_string) }}"
      vars:
        query_string: "[*].{network: network,
                            distance: distance,
                            mask: mask,
                            metric: metric,
                            nexthop_interface: nexthop_if,
                            nexthop_ip: nexthop_ip,
                            protocol: protocol, 
                            type: type}"

実行結果

TASK [json_query(keep_keys)] ***************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "distance": "110",
            "network": "195.0.0.0"
        },
        {
            "distance": "110",
            "network": "0.0.0.0"
        }
    ]
}

TASK [json_query(remove_keys)] **************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "distance": "110",
            "mask": "24",
            "metric": "11",
            "network": "195.0.0.0",
            "nexthop_if": "FastEthernet0/0.100",
            "nexthop_ip": "194.0.0.2",
            "protocol": "O",
            "type": "IA"
        },
        {
            "distance": "110",
            "mask": "0",
            "metric": "1",
            "network": "0.0.0.0",
            "nexthop_if": "FastEthernet0/0.100",
            "nexthop_ip": "194.0.0.2",
            "protocol": "O",
            "type": "E2"
        }
    ]
}

TASK [json_query(replace_keys)] ***************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "distance": "110",
            "mask": "24",
            "metric": "11",
            "network": "195.0.0.0",
            "nexthop_interface": "FastEthernet0/0.100",
            "nexthop_ip": "194.0.0.2",
            "protocol": "O",
            "type": "IA"
        },
        {
            "distance": "110",
            "mask": "0",
            "metric": "1",
            "network": "0.0.0.0",
            "nexthop_interface": "FastEthernet0/0.100",
            "nexthop_ip": "194.0.0.2",
            "protocol": "O",
            "type": "E2"
        }
    ]
}

Ansibleのカスタムフィルターを作る

この記事はエーピーコミュニケーションズ Advent Calendar 2022の16日目の記事です qiita.com

やりたいこと

Xmasといったらサンタですね。

大人になってからというものサンタにあっていません。

豊富なモジュールやフィルタがあるAnsibleでも
サンタに会えるモジュール・フィルタはありませんでした。

せっかくなので
自作のフィルタ(カスタムフィルター)を作成してサンタと会いたいと思います。

スクリプト書く

いきなりAnsibleのフィルタにするのではなく、まずはPythonスクリプトでJinja2Templateを呼び出し
その中で自作したXmasフィルタを動かします。

xmas.py

/で区切ったYYYY/MM/DDのデータを受け取り
MMが12、DDが25の場合のみサンタに会えるフィルタを作成します。

import datetime

def Xmas(*args, **kwargs):
  split_arg = args[0].split('/')
  try:
    date = datetime.date(int(split_arg[0]),int(split_arg[1]),int(split_arg[2]))
    if date.month == 12 and date.day == 25:
      res = '''
           [ 内緒 ]
            '''
      return res
    else:
      res = '''
        サンタかと思ったか?馬鹿め!それは残像だ!
         -= ∧ ∧
        -=と( ・∀・)
         -=/ と_ノ
        -=_//⌒ソ
      '''
      return res
  except IndexError:
    return args[0]

処理は簡単に、受け取った文字列を/で分割して12月25日を判断しています。
[ 内緒 ]の部分にはサンタのAAが入っていますが、最後の表示の楽しみということにします。

script.py

from jinja2 import Environment, FileSystemLoader
from xmas import Xmas

if __name__ == '__main__':
  j2_env = Environment(loader=FileSystemLoader('template/',encoding='utf-8'))
  j2_env.filters['Xmas'] = Xmas

  template = j2_env.get_template('stdout.j2')
  render = template.render(data="2022/12/15")

  print(render)

j2_env.filter[<フィルタ名>] = 関数と定義することで自作のフィルターをテンプレートで利用可能となります
dataは"2022/12/15"のため、まだサンタに会えないのが想定になります

template/stdout.j2

{{ data | Xmas }}

ディレクトリ内は以下のような配置になっています

.
├── script.py
├── template
│   └── stdout.j2
└── xmas.py

スクリプトを実行する


まだサンタには会えていませんが、自作したフィルタが動くことが確認できました。

カスタムフィルターにする

続いて、作成したカスタムフィルタをAnsibleで読み込めるようにします

xmas.py

import datetime

def Xmas(*args, **kwargs):
  split_arg = args[0].split('/')
  try:
    date = datetime.date(int(split_arg[0]),int(split_arg[1]),int(split_arg[2]))
    if date.month == 12 and date.day == 25:
      res = '''
           [ 内緒 ]
            '''
      return res
    else:
      res = '''
        サンタかと思ったか?馬鹿め!それは残像だ!
         -= ∧ ∧
        -=と( ・∀・)
         -=/ と_ノ
        -=_//⌒ソ
      '''
      return res
  except IndexError:
    return args[0]

class FilterModule(object):
  def filters(self):
      return {
          'Xmas': Xmas,
      }

class FilterModuleを追記します。
関数filtersの戻り値でJinja2で利用可能なフィルタ名とスクリプトないの関数を紐つけています。
書き方はbuiltinのcore.pyを参考にしています
docs.ansible.com

github.com

Ansibleで実行する

さて、これでカスタムフィルターは作成できました
続けて実際にPlaybook内で自作したフィルターを呼び出してみたいと思います。

---
- name: Xmas?
  hosts: localhost
  connection: local
  gather_facts: false
  vars:
    date: 2022/12/15

  tasks:
    - name: Xmas?
      debug:
        msg: "{{ date | Xmas }}"

実行してみる

エラーになりました、 自作したカスタムフィルタを使うにはansible.cfgでfilter_pluginsを指定する必要があります

[defaults]
filter_plugins = .

再実行


カスタムフィルターは動きましたが、一行で表示されているためせっかくのAAが確認できません

せっかくなのでstdout_callback_pluginをYAMLにします
yamlコールバックプラグインcommunity.generalコレクションに入っています。ない場合は事前にインストールします

[defaults]
filter_plugins = .
stdout_callback = community.general.yaml

再々実行
バッチリですね

インプットは12/15なのでクリスマスではなくサンタには会えませんでした。

幸せになる

未来を先取りして、変数を12/25にします

---
- name: Xmas?
  hosts: localhost
  connection: local
  gather_facts: false
  vars:
    date: 2022/12/25

  tasks:
    - name: Xmas?
      debug:
        msg: "{{ date | Xmas }}"

実行してみます

   サンタに会えました。最高です。

おまけ

  • ansible.cfgをいじりたくない場合はデフォルトでfilter参照するパスに直接配置します
ansible-config dumps
DEFAULT_FILTER_PLUGIN_PATH(default) = ['/Users/kouta/.ansible/plugins/filter', '/usr/share/ansible/plugins/filter']

フィルターを配置

(v_ansible) MBA:CustomFilter_2022 kouta$ cp xmas.py /Users/kouta/.ansible/plugins/filter/xmas.py
  • collectionにして配布したい ansible-collectionにして配布したい場合は、ansible-galaxy collection init <コレクション名>で生成されたディレクトリのplugins/filter配下に自作のフィルターを配置します
    usalab.xmasコレクションを作成し、スクリプトを配置しました

コレクションを配置したパスをansible.cfgで指定します。 指定したパスのディレクトは以下のような構成になっている必要があるようです
<指定したパス>/ansible_collection/<自作したコレクション>

[defaults]
filter_plugins = filter_plugins
stdout_callback = community.general.yaml
collections_paths = collections

せっかくなので、filterに引数が追加で渡されるパターンで拡張してみます 利用イメージとしては、以下のようにフィルターを使うにあたって2つの引数がある(1つめdate,2つめ'santa') {{ date | Xmas('santa') }}

第2引数の文字列がsantaもしくはreindeer(トナカイ)の文字列に応じて返す値を変えます

import datetime

def Xmas(*args, **kwargs):
  split_arg = args[0].split('/')
  key = args[1]
  try:
    date = datetime.date(int(split_arg[0]),int(split_arg[1]),int(split_arg[2]))
    if date.month == 12 and date.day == 25:
      if key == 'santa':
        res = '''
              [内緒]
              '''
      elif key == 'reindeer':
        res = '''
              [内緒]
              '''
      return res
    else:
      res = '''
        サンタかと思ったか?馬鹿め!それは残像だ!
         -= ∧ ∧
        -=と( ・∀・)
         -=/ と_ノ
        -=_//⌒ソ
      '''
      return res
  except IndexError:
    return args[0]

class FilterModule(object):
  def filters(self):
      return {
          'Xmas': Xmas,
      }

プレイブックは以下のように記載します usalab.xmasにあるXmasフィルタを呼び出します

---
- name: Xmas?
  hosts: localhost
  connection: local
  gather_facts: false
  vars:
    date: 2022/12/25

  tasks:
    - name: Xmas?_サンタ
      debug:
        msg: "{{ date | usalab.xmas.Xmas('santa') }}"

    - name: Xmas?_トナカイ
      debug:
        msg: "{{ date | usalab.xmas.Xmas('reindeer') }}"

バッチリ動きますね

※ansible.cfgでcollections_pathを指定した場合、指定したパスに存在するコレクションのみが対象になるようです、コールバックプラグインで利用しているcommunity.generalコレクションもcollections_pathで指定したディレクトリに含める必要があります。

え!!AnsibleでRouteのdiffを!?できらぁ!

2021Ansible Advent Calendar21日目の記事です

qiita.com

RoutingのDiffを取りたい

ansibleでNW機器の設定変更を行った前後のルートの差分を見たい。こんな時どうしていますか? 取得した結果をシンプルに比較してしまうと、ルートのアップタイムが差分として表示され純粋に増えた/減った箇所が分かりにくい場合があります。

そこで、ansibleとntc-templates/カスタムフィルターなどを利用してルートの差分を取得する方法を紹介します。

まず、使ったものを動かすと下記のように、時間を除いたルートの増減のみを表示させることができます。

TASK [route diff (json)] ************************************************************************************************************
--- before
+++ after
@@ -9,0 +10 @@
+{'PROTOCOL': 'S', 'TYPE': '', 'NETWORK': '172.16.0.0', 'MASK': '24', 'DISTANCE': '1', 'METRIC': '0', 'NEXTHOP_IP': '10.9.0.1', 'NEXTHOP_IF': ''}

changed: [localhost]

※ この記事では、ntc-templateでパースした結果で差分を取得することを前提に考えます。

環境

filesにはshow ip routeの実行結果が格納されています。
templatesにはntc-templateのios_show_ip_route.textfsmios_show_ip_route.textfsmを少しだけカスタムしたものを格納しています。

├── ansible.cfg
├── filter_plugins
│   └──  routediff.py
├── files
│   ├── after_ip_route.txt
│   └── before_ip_route.txt
├── templates
│   ├── ios_show_ip_route.textfsm
│   └── ios_show_ip_route_custom.textfsm
└── route_diff.yml

ansible.cfg

ansible.utils.fact_diffを利用するため、CONTEXTを0に設定します。 カスタムフィルタを使う方法もあるので、filter_pluginsに対象のパスを書きます

[diff]
CONTEXT = 0

[defaults]
filter_plugins = filter_plugins
ansible [core 2.12.0]

Collection        Version
----------------- -------
ansible.netcommon 2.4.0  
ansible.utils     2.4.2  

何も考えずにDiffをした時の悩み

何も考えずにshow ip routeの出力結果が記載されているファイルを読み込んでfact_diffで差分を比較してみます。

- name: route diff (text)
  hosts: localhost
  gather_facts: false

  tasks:
    - name: route diff (text)
      ansible.utils.fact_diff:
        before: "{{ lookup('file', playbook_dir + '/files/before_ip_route.txt') }}"
        after: "{{ lookup('file', playbook_dir + '/files/after_ip_route.txt') }}"

実行結果

PLAY [route diff (text)] ********************************************************************************************************************************************************************************************

TASK [route diff (text)] ********************************************************************************************************************************************************************************************
--- before
+++ after
@@ -18 +18 @@
-O        10.2.0.0/24 [110/2] via 10.9.0.1, 00:00:03, GigabitEthernet0/0
+O        10.2.0.0/24 [110/2] via 10.9.0.1, 00:02:10, GigabitEthernet0/0
@@ -22 +22,3 @@
-L        10.100.0.2/32 is directly connected, GigabitEthernet0/2
\ No newline at end of file
+L        10.100.0.2/32 is directly connected, GigabitEthernet0/2
+      172.16.0.0/24 is subnetted, 1 subnets
+S        172.16.0.0 [1/0] via 10.9.0.1
\ No newline at end of file

changed: [localhost]

ルートは変わっていないのに、ダイナミックルーティングで登録されているルートは時間の部分が差分として表示されしまいます。 今回はルートが少ないですがルートが大量にあると時間の差分が大量に出てしまい、純粋に追加や削除になっているルートが分かりにくいですね。
出力が階層のようになってしまっているルートがある場合はもはや何が起きてるのか謎です

@@ -43,2 +44,2 @@
-           [110/20] via 10.62.4.29, 12:55:19, TenGigabitEthernet1/15
-           [110/20] via 10.62.3.29, 12:55:19, TenGigabitEthernet1/16
+           [110/20] via 10.62.4.29, 12:59:19, TenGigabitEthernet1/15
+           [110/20] via 10.62.3.29, 12:59:19, TenGigabitEthernet1/16

ルートの比較だけを取得する

それでは、ここからルート部分の比較をする方法を検討します。

  1. ntc-templatesをカスタムすればできらぁ!
  2. Jinja2で頑張ればできらぁ!
  3. Custom Filter使えばできらぁ!

1. ntc-templatesをカスタムすればできらぁ!

ntc-templatesでshow ip routeをパースした結果を利用して差分を比較します。 しかし、元のテンプレートだとUPTIMEといったデータも保存されるため、このまま比較してしまっては不要な差分が表示されてしまいます。

そこで、もともと存在していたValue UPTIMEをデータとして保存されにようにしてしまいましょう。 UPTIMEとしてマッチしてる正規表現

Value UPTIME (\d[\w:\.]+)

${UPTIME}をすべて\d[\w:\.]+に直接書き換え(乱暴)

カスタム済みテンプレートios_show_ip_route_custom.textfsm

Value Filldown PROTOCOL (\w)
Value Filldown TYPE (\w{0,2})
Value Required,Filldown NETWORK (\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})
Value Filldown MASK (\d{1,2})
Value DISTANCE (\d+)
Value METRIC (\d+)
Value NEXTHOP_IP (\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})
Value NEXTHOP_IF ([A-Z][\w\-\.:/]+)

Start
  ^Gateway.* -> Routes
  # Capture time-stamp if vty line has command time-stamping turned on
  ^Load\s+for\s+
  ^Time\s+source\s+is

Routes
  # For "is (variably )subnetted" line, capture mask, clear all values.
  ^\s+\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}\/${MASK}\sis -> Clear
  #
  # Match directly connected route with explicit mask
  ^${PROTOCOL}(\s|\*)${TYPE}\s+${NETWORK}\/${MASK}\sis\sdirectly\sconnected,\s${NEXTHOP_IF} -> Record
  #
  # Match directly connected route (mask is inherited from "is subnetted")
  ^${PROTOCOL}(\s|\*)${TYPE}\s+${NETWORK}\sis\sdirectly\sconnected,\s${NEXTHOP_IF} -> Record
  #
  # Match regular routes, with mask, where all data in same line
  ^${PROTOCOL}(\s|\*)${TYPE}\s+${NETWORK}\/${MASK}\s\[${DISTANCE}/${METRIC}\]\svia\s${NEXTHOP_IP}(,\s\d[\w:\.]+)?(,\s${NEXTHOP_IF})? -> Record
  #
  # Match regular route, all one line, where mask is learned from "is subnetted" line
  ^${PROTOCOL}(\s|\*)${TYPE}\s+${NETWORK}\s\[${DISTANCE}\/${METRIC}\]\svia\s${NEXTHOP_IP}(,\s\d[\w:\.]+)?(,\s${NEXTHOP_IF})? -> Record
  #
  # Match route with no via statement (Null via protocol)
  ^${PROTOCOL}(\s|\*)${TYPE}\s+${NETWORK}\/${MASK}\s\[${DISTANCE}/${METRIC}\],\s\d[\w:\.]+,\s${NEXTHOP_IF} -> Record
  #
  # Match "is a summary" routes (often Null0)
  ^${PROTOCOL}(\s|\*)${TYPE}\s+${NETWORK}\/${MASK}\sis\sa\ssummary,\s\d[\w:\.]+,\s${NEXTHOP_IF} -> Record
  #
  # Match regular routes where the network/mask is on the line above the rest of the route
  ^${PROTOCOL}(\s|\*)${TYPE}\s+${NETWORK}\/${MASK} -> Next
  #
  # Match regular routes where the network only (mask from subnetted line) is on the line above the rest of the route
  ^${PROTOCOL}(\s|\*)${TYPE}\s+${NETWORK} -> Next
  #
  # Match the rest of the route information on line below network (and possibly mask)
  ^\s+\[${DISTANCE}\/${METRIC}\]\svia\s${NEXTHOP_IP}(,\s\d[\w:\.]+)?(,\s${NEXTHOP_IF})? -> Record
  #
  # Match load-balanced routes
  ^\s+\[${DISTANCE}\/${METRIC}\]\svia\s${NEXTHOP_IP} -> Record
  #
  # Clear all variables on empty lines
  ^\s* -> Clearall

EOF

これを使ってパースすると、UPTIMEがパース後の結果に含まれなくなります。

(~snip~)
            {
                "DISTANCE": "110",
                "MASK": "24",
                "METRIC": "20",
                "NETWORK": "10.6.234.0",
                "NEXTHOP_IF": "Serial0/0/0:0",
                "NEXTHOP_IP": "10.64.4.90",
                "PROTOCOL": "O",
                "TYPE": "E2"
            },
(~snip~)

つまり、カスタムしたテンプレートを使ってパースした2つのshow ip routeの差分を見ると、ルートの増減のみ確認できます。

- name: Parse2(Change Textfsm)
  hosts: localhost
  gather_facts: false

  tasks:
    - name: parse (before)
      ansible.utils.cli_parse:
        text: "{{ lookup('file', playbook_dir + '/files/before_ip_route.txt') }}"
        parser:
          name: ansible.utils.textfsm
          template_path: "{{ playbook_dir }}/templates/ios_show_ip_route_custom.textfsm"
      register: parser_before_ip_route

    - name: parse (after)
      ansible.utils.cli_parse:
        text: "{{ lookup('file', playbook_dir + '/files/after_ip_route.txt') }}"
        parser:
          name: ansible.utils.textfsm
          template_path: "{{ playbook_dir }}/templates/ios_show_ip_route_custom.textfsm"
      register: parser_after_ip_route

    - name: route diff (json)
      ansible.utils.fact_diff:
        before: "{{ parser_before_ip_route.parsed }}"
        after: "{{ parser_after_ip_route.parsed }}"

実行

PLAY [Parse2(Change Textfsm)] *********************************************************************************************************

TASK [parse (before)] **************************************************************************************************************
ok: [localhost]

TASK [parse (after)] **************************************************************************************************************
ok: [localhost]

TASK [route diff (json)] ************************************************************************************************************
--- before
+++ after
@@ -9,0 +10 @@
+{'PROTOCOL': 'S', 'TYPE': '', 'NETWORK': '172.16.0.0', 'MASK': '24', 'DISTANCE': '1', 'METRIC': '0', 'NEXTHOP_IP': '10.9.0.1', 'NEXTHOP_IF': ''}

changed: [localhost]

これで、ルートの差分が取れました
元々あるテンプレートに手を入れる方法なので、変更点が分かりにくくなる。
これを許してしまうと無限にカスタムテンプレートが出来上がってしまう。といった課題はあるのものの手段として使えそうです。

2. Jinja2で頑張ればできらぁ!

続いて、Jinja2を使って同じことをする方法を紹介します。
Jinja2を使ってパースしてもよいのですが、ntc-templateでパースした構造化データのUPTIMEをjinja2で取り除き差分を取ります。

- name: Parse3(Jinja2)
  hosts: localhost
  gather_facts: false

  tasks:
    - name: parse before_ip_route
      ansible.utils.cli_parse:
        text: "{{ lookup('file', playbook_dir + '/files/before_ip_route.txt') }}"
        parser:
          name: ansible.utils.textfsm
          template_path: "{{ playbook_dir }}/templates/ios_show_ip_route.textfsm"
      register: parser_before_ip_route

    - name: parse after_ip_route
      ansible.utils.cli_parse:
        text: "{{ lookup('file', playbook_dir + '/files/after_ip_route.txt') }}"
        parser:
          name: ansible.utils.textfsm
          template_path: "{{ playbook_dir }}/templates/ios_show_ip.textfsm"
      register: parser_after_ip_route


    - name: set_fact
      set_fact:
        fact_parser_before_ip_route: |
          {% set _ = [] %}
          {% for i in parser_before_ip_route.parsed %}
          {% set __ = i.pop('UPTIME') %}
          {% set _ = _.append(i) %}
          {% endfor %}
          {{ _ }}
        fact_parser_after_ip_route: |
          {% set _ = [] %}
          {% for i in parser_after_ip_route.parsed %}
          {% set __ = i.pop('UPTIME') %}
          {% set _ = _.append(i) %}
          {% endfor %}
          {{ _ }}

    - name: route diff (json)
      ansible.utils.fact_diff:
        before: "{{ fact_parser_before_ip_route_simple }}"
        after: "{{ fact_parser_after_ip_route_simple }}"

ちょっと無茶しやがって。。感がありますが、単純にpopUPTIMEを取り除いた結果を再格納しています。

PLAY [Parse3(Jinja2)] ***********************************************************************************************************************************************************************************************

TASK [parse before_ip_route] *********************************************************************************************************************************************************************************
ok: [localhost]

TASK [parse after_ip_route] **********************************************************************************************************************************************************************************
ok: [localhost]

TASK [set_fact] *****************************************************************************************************************************************************************************************************
ok: [localhost]

TASK [route diff (json)] ************************************************************************************************************
--- before
+++ after
@@ -9,0 +10 @@
+{'PROTOCOL': 'S', 'TYPE': '', 'NETWORK': '172.16.0.0', 'MASK': '24', 'DISTANCE': '1', 'METRIC': '0', 'NEXTHOP_IP': '10.9.0.1', 'NEXTHOP_IF': ''}

changed: [localhost]

Playbookでの工夫で回避できるので、一番簡単な方法かもしれません。
ただJinja2をべた書きされていると、Playbookの可読性が下がってしまいますね。

3. Custom Filter使えばできらぁ!

最後に、カスタムフィルターを使った方法を紹介します。
やっている内容12と同じく、ntc-templateでパースされたデータからUPTIMEを取り除いているだけです。

#!/usr/bin/python

class FilterModule(object):
  def pop_uptime(self, input_value):
    output = list()
    for i in input_value:
      i.pop('UPTIME')
      output.append(i)
    return output

  def filters(self):
      return {
          'pop_uptime': self.pop_uptime,
      }

複雑な処理は裏に隠してしまおうといったイメージですね。カスタムフィルターを使うと柔軟性は跳ね上がります。
例えばsortなどしたい!と思ったときにはカスタムフィルターの機能を追加すれば対応できます。
今回は限定的な処理なので、role化しfilter_pluginsディレクトリにおいて管理するほうがいいかなぁと思います。

pop_uptimeの利用

~(parseは省略)~
- name: route diff (diff)
  ansible.utils.fact_diff:
    before: "{{ parser_before_ip_route.parsed \
                             | pop_uptime() }}"
    after: "{{ parser_after_ip_route.parsed \
                             | pop_uptime() }}"

利用方法がとてもシンプルになります。 カスタムフィルターを作成/メンテする必要があり、Pythonを書けないと作れないのが課題ですかね。

ここがむずいよ

最初はテキストから単純に時間の部分をreplaceすればいいかな?と考えていたんですが、思ったよりも面倒でした。

時間の表記方法が多彩

多分基本形

O       172.20.1.1 [110/11] via 194.0.0.2, 00:05:45, FastEthernet0/0.100

日付(d)と時間(h)パターン

O E2    4.4.0.0 [110/20] via 194.0.0.2, 1d18h, FastEthernet0/0.100

え!?改行!?

B       6.6.0.0 [200/0] via 195.0.0.1, 00:00:04
     172.16.0.0/26 is subnetted, 1 subnets

すべてのパターンに対応するように正規表現を書いてreplaceすれば、時間部分を消した情報で比較もできると思いましたが、大変そうだったのでやめました。

まとめ

多少強引ではあったものの、ルートの差分をとることができました。
テンプレートのカスタム、Jinja2、カスタムフィルターと違ったアプローチで紹介しました。

個人的にはカスタムフィルターが好きです。

差分がないことを確認したい場合は紹介した内容がそのまま使えるのではないでしょうか。
ただ、出力された差分が想定通りであるか?の確認をする場合は追加の検討が必要です。時間があるときそちらの検討もしたいと思います。

本記事は、辞書から特定のキーを抜き出すことがポイントになっています。
下記の記事でも紹介がされています。

qiita.com

利用した出力結果

after_ip_route.txt

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, 9 subnets, 3 masks
C        10.0.10.0/31 is directly connected, GigabitEthernet0/3
L        10.0.10.0/32 is directly connected, GigabitEthernet0/3
C        10.1.0.0/24 is directly connected, GigabitEthernet0/1
L        10.1.0.1/32 is directly connected, GigabitEthernet0/1
O        10.2.0.0/24 [110/2] via 10.9.0.1, 00:02:10, GigabitEthernet0/0
C        10.9.0.0/31 is directly connected, GigabitEthernet0/0
L        10.9.0.0/32 is directly connected, GigabitEthernet0/0
C        10.100.0.0/24 is directly connected, GigabitEthernet0/2
L        10.100.0.2/32 is directly connected, GigabitEthernet0/2
      172.16.0.0/24 is subnetted, 1 subnets
S        172.16.0.0 [1/0] via 10.9.0.1

before_ip_route.txt

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, 9 subnets, 3 masks
C        10.0.10.0/31 is directly connected, GigabitEthernet0/3
L        10.0.10.0/32 is directly connected, GigabitEthernet0/3
C        10.1.0.0/24 is directly connected, GigabitEthernet0/1
L        10.1.0.1/32 is directly connected, GigabitEthernet0/1
O        10.2.0.0/24 [110/2] via 10.9.0.1, 00:00:03, GigabitEthernet0/0
C        10.9.0.0/31 is directly connected, GigabitEthernet0/0
L        10.9.0.0/32 is directly connected, GigabitEthernet0/0
C        10.100.0.0/24 is directly connected, GigabitEthernet0/2
L        10.100.0.2/32 is directly connected, GigabitEthernet0/2

TTPでパース後の要素が一つだけでもリスト形式にする

usage-automate.hatenablog.com

こちらの記事で、わからなかった下記について実装方法が分かったので書き残します

パース後の要素が2個以上ある場合は自動的にリストになるようです。
要素が1個の場合リストにならないため、パース後に呼び出す際に注意が必要です。

前回のおさらい

作成したテンプレートがこちら

<group name="dynamic_nat">
{{ protocol }} {{ inside_global_ip }}:{{ inside_global_port }} {{ inside_local_ip }}:{{ inside_local_port }} {{ outside_local_ip }}:{{ outside_local_port }} {{ outside_global_ip }}:{{ outside_global_port }}
</group>

<group name="static_nat">
{{ protocol }} {{ inside_global_ip | re("(\S+)(?!.*:)") }} {{ inside_local_ip | re("(\S+)(?!.*:)") }} {{ outside_local_ip | re("(\S+)(?!.*:)") }} {{ outside_global_ip | re("(\S+)(?!.*:)") }}
</group>

たとえばパース後の要素が一つしかできないインプットで実行すると

Pro Inside global      Inside local       Outside local      Outside global
tcp 10.9.0.0:51776     10.1.0.2:51776     10.2.0.2:21        10.2.0.2:21
--- 10.9.0.0     10.1.0.2     ---        ---

アウトプットは辞書になります。

        "dynamic_nat": {
            "inside_global_ip": "10.9.0.0",
            "inside_global_port": "51776",
            "inside_local_ip": "10.1.0.2",
            "inside_local_port": "51776",
            "outside_global_ip": "10.2.0.2",
            "outside_global_port": "21",
            "outside_local_ip": "10.2.0.2",
            "outside_local_port": "21",
            "protocol": "tcp"
        },
        "static_nat": {
            "inside_global_ip": "10.9.0.0",
            "inside_local_ip": "10.1.0.2",
            "outside_global_ip": "---",
            "outside_local_ip": "---",
            "protocol": "---"
        }

パース後の要素が複数個になるインプットで実行すると

Pro Inside global      Inside local       Outside local      Outside global
tcp 10.9.0.0:51776     10.1.0.2:51776     10.2.0.2:21        10.2.0.2:21
tcp 10.9.0.0:51778     10.1.0.2:51778     10.2.0.2:21        10.2.0.2:21
--- 10.9.0.0     10.1.0.2     ---        ---

アウトプットは自動的にリストになります。

        "dynamic_nat": [
            {
                "inside_global_ip": "10.9.0.0",
                "inside_global_port": "51776",
                "inside_local_ip": "10.1.0.2",
                "inside_local_port": "51776",
                "outside_global_ip": "10.2.0.2",
                "outside_global_port": "21",
                "outside_local_ip": "10.2.0.2",
                "outside_local_port": "21",
                "protocol": "tcp"
            },
            {
                "inside_global_ip": "10.9.0.0",
                "inside_global_port": "51778",
                "inside_local_ip": "10.1.0.2",
                "inside_local_port": "51778",
                "outside_global_ip": "10.2.0.2",
                "outside_global_port": "21",
                "outside_local_ip": "10.2.0.2",
                "outside_local_port": "21",
                "protocol": "tcp"
            }
        ],
        "static_nat": {
            "inside_global_ip": "10.9.0.0",
            "inside_local_ip": "10.1.0.2",
            "outside_global_ip": "---",
            "outside_local_ip": "---",
            "protocol": "---"
        }
    }

せっかく構造化したのに、要素が一つの時と複数の時で呼び出し方が変わってしまうと厄介です

要素一個
static_nat.inside_global_ip

要素複数個
static_nat[0].isndie_global_ip

解決方法

テンプレートの<group name="dynamic_nat">*を追加する

<group name="dynamic_nat*">

<group name="dynamic_nat*">
{{ protocol }} {{ inside_global_ip }}:{{ inside_global_port }} {{ inside_local_ip }}:{{ inside_local_port }} {{ outside_local_ip }}:{{ outside_local_port }} {{ outside_global_ip }}:{{ outside_global_port }}
</group>

<group name="static_nat">
{{ protocol }} {{ inside_global_ip | re("(\S+)(?!.*:)") }} {{ inside_local_ip | re("(\S+)(?!.*:)") }} {{ outside_local_ip | re("(\S+)(?!.*:)") }} {{ outside_global_ip | re("(\S+)(?!.*:)") }}
</group>

dynamic_natのほうだけ*を付けました。

これで、パース後の要素が一つのみのインプットで試してみます。

Pro Inside global      Inside local       Outside local      Outside global
tcp 10.9.0.0:51776     10.1.0.2:51776     10.2.0.2:21        10.2.0.2:21
--- 10.9.0.0     10.1.0.2     ---        ---

要素が一つだけですが、*を付けたdynamic_natのほうはリストになりました。

        "dynamic_nat": [
            {
                "inside_global_ip": "10.9.0.0",
                "inside_global_port": "51776",
                "inside_local_ip": "10.1.0.2",
                "inside_local_port": "51776",
                "outside_global_ip": "10.2.0.2",
                "outside_global_port": "21",
                "outside_local_ip": "10.2.0.2",
                "outside_local_port": "21",
                "protocol": "tcp"
            }
        ],
        "static_nat": {
            "inside_global_ip": "10.9.0.0",
            "inside_local_ip": "10.1.0.2",
            "outside_global_ip": "---",
            "outside_local_ip": "---",
            "protocol": "---"
        }
    }

まとめ

TTPを使い始めて地味に困っていた課題を解決できました、いろいろな機能があるのでもう少し勉強をしたいと思います。

ansible-navigator入門

ansible-navigatorとは

ansible-navigator は、インベントリ、プレイブック、コレクションなどのAnsibleコンテンツを作成、確認、トラブルシューティングするためのコマンドベースのツールです。
今回は公式ドキュメントを参考に入門してみました

ansible-navigator.readthedocs.io

インストール

pip install ansible-navigator

インストールをするとansible-runnerもインストールされます、ansible実行はrunnerが行っているようです。

ansible-navigator==1.1.0
ansible-runner==2.1.1

実行

設定ファイルの準備

ansible.cfgの指定やEEの選択などの設定をYAML形式で記載します。
ansible-navigatorを実行するディレクトリに配置しておけば自動的に読み込んでくれます。

ansible-navigator.yml

---
ansible-navigator:
  ansible:
    config: ansible.cfg

  execution-environment:
    container-engine: docker
    image: test
    pull-policy: never
    environment-variables:
      pass:
        - ACI_HOST
        - ACI_USERNAME
        - ACI_PASSWORD
        - ACI_PORT
        - ACI_VALIDATE_CERTS

  playbook-artifact:
    enable: false
    replay: ./artifact/replay.json
    save-as: ./artifact/artifact.json

  # mode: stdout

※今回利用するtest(EE)は事前ansible-builderに作成しています。

ファイルの構成は以下のようにしました

├── ansible.cfg
├── ansible-navigator.yml ★設定ファイル
├── artifact
├── example01.yml
├── example02.yml
├── files
│   └── stdout.txt
├── hosts.ini
└── host_vars
    ├── ios01.yml
    ├── ios02.yml
    └── ios03.yml

コマンドの実行

利用可能なイメージの確認

ansible-navigator images
imagesのサブコマンドを指定することで、EEとして利用可能なimagesが表示されます。
f:id:usage_automate:20211212001203p:plain

番号を指定するとさらに情報が見れます f:id:usage_automate:20211212001247p:plain

  • [0] Image information
    f:id:usage_automate:20211212001446p:plain

  • [1] General information
    f:id:usage_automate:20211212001605p:plain

  • [2] Ansible version and collections f:id:usage_automate:20211212001339p:plain

  • [3] Python packages f:id:usage_automate:20211212001806p:plain

  • [4] Operating system packages f:id:usage_automate:20211212001849p:plain

  • [5] Everything f:id:usage_automate:20211212001922p:plain

inventoryの確認

ansible-navigator inventory -i hosts.ini
inventoryの検索や調査にも便利なinventoryサブコマンドも存在しています。
利用する際は-iでhostsファイルを指定します。
f:id:usage_automate:20211212002234p:plain

  • [0]Browse groups
    f:id:usage_automate:20211212002303p:plain

  • [1] Browse hosts
    f:id:usage_automate:20211212002543p:plain

hostの番号でhost_varsも確認可能
f:id:usage_automate:20211212002447p:plain

モジュールのドキュメントを確認する

ansible-navigator doc ansible.utils.fact_diff
モジュールのオプションを確認するにはdocサブコマンドを利用します。
ドキュメントを確認したいモジュール名を指定し実行します。 f:id:usage_automate:20211212003006p:plain

Collectionを確認する

ansible-navigator collections
EEにインストールされているCollectionを確認するにはcollectionsサブコマンドを利用します。 f:id:usage_automate:20211212003048p:plain

コレクションを選択

f:id:usage_automate:20211212003134p:plain

モジュールを選択

f:id:usage_automate:20211212003156p:plain

Playbookを実行する

ansible-navigator run example01.yml -i hosts.ini
Playbookの実行はrunサブコマンドを利用します。
ansibleで利用できるオプションはどこまで有効か不明ですが--tagを試してみたところ問題なく動作しました。

f:id:usage_automate:20211212004328p:plain

Play name番号を指定する

f:id:usage_automate:20211212004359p:plain

Result番号を指定する

f:id:usage_automate:20211212004420p:plain

ちなみに

intaractiveモードでは、:の後にいろいろなキーワードを指定することができます。

:helpと打ち込むことで、ヘルプを表示できます。 f:id:usage_automate:20211212004601p:plain

:f ^aなど正規表現も可能で検索が柔軟にできそうです(これが結構よさそう)

実行時にできたartifact.jsonから実行ログを見る

ansible-navigator replay artifact/artifact.json

artifactディレクトリに生成したartifact.jsonから実行ログをリプレイすることができます。(接続などは行われません) f:id:usage_automate:20211212004906p:plain

実行するとansible-navigator runを実行したときと同じような操作が可能です。

表示の調整

デフォルトではintaractiveモードとなり、今までのAnsibleの実行ログとはかなり違った表示になっています。 ansibleコマンドを実行したときと同じような表示にするには、modeパラメータをstdoutに変更します。 ansible-navigator.ymlに記載するか、実行時にオプションとして渡すことで変更が可能です。 ansible-navigator.ymlに記載する場合、下記のような表記になります。

ansible-navigator:
  mode: stdout

オプションで変更する場合-m stdoutをコマンド実行時に追加します。

ansible-navigator replay artifact/artifact.json -m stdout

f:id:usage_automate:20211212005231p:plain

見慣れた表示になりました。

collections/inventoryなどはstdoutモードのまま実行するとエラーになります、 stdoutのモードはreplay/runサブコマンドを利用する場合でのみ使うのがよさそうです。

まとめ

ansible-navigatorに入門してみました。個人的にはかなり便利と感じています。
実行のリプレイやEEの設定確認、結果の文字列検索など絶妙にかゆいところに手が届いている感覚です。

今後、ansible-runnerがansibleの実行環境としてスタンダードになっていくことを想像すると使い方を覚えておくといいツールだなと感じました。 (runner実行するより簡単&わかりやすい気がしました)

NW機器SSH/Telnet接続PythonライブラリのScrapliを試してみた

エーピーコミュニケーションズ Advent Calendar 2021 11日目の記事です

qiita.com

Scrapliとは

ネットワーク機器へTelnetまたはSSHの接続するためのPythonライブラリです。
特徴としては、

  1. 簡単に始められる
  2. 速い
  3. 開発が簡単
  4. 拡張性がある(プラグ可能)

トランスポート部分のカスタマイズができるのも特徴です、ssh2やparamiko,telnetなど柔軟に変更ができるようです。しかし、トランスポートはよっぽどの要件がない限り変更しないで十分なようです。

Scrapliのメインリポジトリ github.com

サポートするドライブ(コア)を拡張し様々な機器に対応するためのscrapli_community github.com

Netconf対応すためのscrapli_netconf github.com

Nornirのプライグインとして利用するnornir_scrapli github.com

テストのために「実際の」ネットワークデバイスのように見えるセミインタラクティブSSHサーバーを作成するためのツールscrapli_replay github.com

NW機器の対応状況(2021/12/11時点)

scrapli

scrapli_community

  • aethra
  • alcatel
  • edgecore
  • eltex
  • fortinet
  • hp
  • huawei
  • mikrotik
  • nokia
  • paloalto
  • ruckus
  • simens

Netmikoとの違い

似たようなライブラリにNetmikoがあります。メソッドの構成は似ています(send_command,send_commandsなど)

違いとしては、サポートしているデバイスの数、Netconfのサポートの有無でしょうか。 Netmikoはサポートしているデバイスの数がとても多いです。 github.com

scrapliではまだやり方が分かっていないだけなんですが、Netmikoではredispatchを使ってNW機器の踏み台アクセスもできます。 (SVを踏み台にするのはScrapliでも可能)

どちらも機能としてパーサーと連携もできるため、どちらを使うか?は利用しているデバイスの種類や設定変更によって決めることになりそうです。処理自体もScrapliのほうが速いようです。
手元で比較したときは、close(切断)の処理がNetmikoは数秒かかってしまうのでスクリプトの終了まではScrapliのほうが速いといった結果になりました。

Netmikoは公式ドキュメントがとてもおしゃれです。

環境

Python 3.9.2

scrapli==2021.7.30

インストール

最低限のインストール

pip install scrapli

他の機能を使う場合に、ライブラリをまとめてインストール

pip install scrapli[full]
pip install scrapli[paramiko]
pip install scrapli[ssh2]
pip install scrapli[asyncssh]
pip install scrapli[textfsm]
pip install scrapli[genie]
pip install scrapli[ttp]

使ってみる

ログイン/ログアウト

from scrapli import Scrapli

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

conn = Scrapli(**device)
conn.open()
print(conn.get_prompt())
conn.close()

platformを指定することで対応するDriverを選択してくれるようです。
platformを指定せずにIOSXEDriver()を利用しても、同じ挙動になります。
接続はopen()でログアウトはclose()で実施します。

get_prompt()はその名の通りプロンプトを取得してくれます

Scrapli# python example.py 
csr1000v-1#

showコマンドの実行

続いて、ログイン後showコマンドを実行してみます。 send_command()/send_commandsを利用します。 ter len 0など表示に関する設定は自動的に行われるため、showコマンドのみを指定します。

send_command: 1コマンド実行

from scrapli import Scrapli
import pprint

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res = conn.send_command("show ip interface brief")


print(res.start_time)
print(res.result)
print(res.finish_time)

resultに実行結果が格納されているので結果の確認はres.resultを表示させます。

# python show.py 
2021-12-11 12:21:18.694178
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES NVRAM  up                    up
GigabitEthernet2       192.168.1.1     YES other  up                    up
GigabitEthernet3       unassigned      YES NVRAM  administratively down down
Loopback1              56.56.56.56     YES manual up                    up
2021-12-11 12:21:18.943480

send_commands: 複数コマンド実行 複数のコマンドを実行するにはsend_commands()を利用します。
commands引数にリスト形式でshowコマンドを指定します。

from scrapli import Scrapli
import pprint

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res = conn.send_commands(commands=["show ip interface brief",
                                      "show version"])
print(res.result) # すべて表示
print(res[0].result) # 1つ目の要素(show ip interface brief)を表示
print(res[1].result) # 2つ目の要素(show version)を表示

実行結果も、リストになっているためX番目の結果を表示するにはres[X].resultと指定します。

python show2.py 
# すべて表示
show ip interface brief <- 実行したコマンドも含まれる
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES NVRAM  up                    up
GigabitEthernet2       192.168.1.1     YES other  up                    up
GigabitEthernet3       unassigned      YES NVRAM  administratively down down
Loopback1              56.56.56.56     YES manual up                    upshow version <- 実行したコマンドも含まれる
Cisco IOS XE Software, Version 17.03.01a
Cisco IOS Software [Amsterdam], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.3.1a, RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2020 by Cisco Systems, Inc.
Compiled Wed 12-Aug-20 00:16 by mcpre


Cisco IOS-XE software, Copyright (c) 2005-2020 by cisco Systems, Inc.
All rights reserved.  Certain components of Cisco IOS-XE software are
licensed under the GNU General Public License ("GPL") Version 2.0.  The
software code licensed under GPL Version 2.0 is free software that comes
with ABSOLUTELY NO WARRANTY.  You can redistribute and/or modify such
GPL code under the terms of GPL Version 2.0.  For more details, see the
documentation or "License Notice" file accompanying the IOS-XE software,
or the applicable URL provided on the flyer accompanying the IOS-XE
software.


ROM: IOS-XE ROMMON
csr1000v-1 uptime is 1 day, 3 hours, 57 minutes
Uptime for this control processor is 1 day, 3 hours, 58 minutes
System returned to ROM by reload
System image file is "bootflash:packages.conf"
Last reload reason: reload



This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html

If you require further assistance please contact us by sending email to
export@cisco.com.

License Level: ax
License Type: N/A(Smart License Enabled)
Next reload license Level: ax

The current throughput level is 1000 kbps


Smart Licensing Status: UNREGISTERED/No Licenses in Use

cisco CSR1000V (VXE) processor (revision VXE) with 715705K/3075K bytes of memory.
Processor board ID 9ESGOBARV9D
Router operating mode: Autonomous
3 Gigabit Ethernet interfaces
32768K bytes of non-volatile configuration memory.
3978420K bytes of physical memory.
6188032K bytes of virtual hard disk at bootflash:.

Configuration register is 0x2102
# 1つ目の要素(show ip interface brief)を表示
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES NVRAM  up                    up
GigabitEthernet2       192.168.1.1     YES other  up                    up
GigabitEthernet3       unassigned      YES NVRAM  administratively down down
Loopback1              56.56.56.56     YES manual up                    up

# 2つ目の要素(show version)を表示
Cisco IOS XE Software, Version 17.03.01a
Cisco IOS Software [Amsterdam], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.3.1a, RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2020 by Cisco Systems, Inc.
Compiled Wed 12-Aug-20 00:16 by mcpre


Cisco IOS-XE software, Copyright (c) 2005-2020 by cisco Systems, Inc.
All rights reserved.  Certain components of Cisco IOS-XE software are
licensed under the GNU General Public License ("GPL") Version 2.0.  The
software code licensed under GPL Version 2.0 is free software that comes
with ABSOLUTELY NO WARRANTY.  You can redistribute and/or modify such
GPL code under the terms of GPL Version 2.0.  For more details, see the
documentation or "License Notice" file accompanying the IOS-XE software,
or the applicable URL provided on the flyer accompanying the IOS-XE
software.


ROM: IOS-XE ROMMON
csr1000v-1 uptime is 1 day, 3 hours, 57 minutes
Uptime for this control processor is 1 day, 3 hours, 58 minutes
System returned to ROM by reload
System image file is "bootflash:packages.conf"
Last reload reason: reload



This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html

If you require further assistance please contact us by sending email to
export@cisco.com.

License Level: ax
License Type: N/A(Smart License Enabled)
Next reload license Level: ax

The current throughput level is 1000 kbps


Smart Licensing Status: UNREGISTERED/No Licenses in Use

cisco CSR1000V (VXE) processor (revision VXE) with 715705K/3075K bytes of memory.
Processor board ID 9ESGOBARV9D
Router operating mode: Autonomous
3 Gigabit Ethernet interfaces
32768K bytes of non-volatile configuration memory.
3978420K bytes of physical memory.
6188032K bytes of virtual hard disk at bootflash:.

Configuration register is 0x2102

パーサーと組み合わせる

CLIのテキストデータを構造化するために、パースすることもできます。

  • textfsm_parse_output
  • ttp_parse_output
  • genie_parse_output
from scrapli import Scrapli
import pprint

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res = conn.send_commands(commands=["show ip interface brief",
                                      "show version"])


[ pprint.pprint(i.textfsm_parse_output()) for i in res]
[ pprint.pprint(i.genie_parse_output()) for i in res]

textfsmパーサとgenieパーサを試します。事前にntc-templates,genieのライブラリをインストールしておきます。 この2つのパーサはテンプレートを指定せずにすぐに利用が可能です。

TTPは利用するテンプレートを指定する必要があるためひと手間必要なので割愛します。

## textfsm(ntc-template): show interface brief
[{'intf': 'GigabitEthernet1',
  'ipaddr': '10.10.20.48',
  'proto': 'up',
  'status': 'up'},
 {'intf': 'GigabitEthernet2',
  'ipaddr': '192.168.1.1',
  'proto': 'up',
  'status': 'up'},
 {'intf': 'GigabitEthernet3',
  'ipaddr': 'unassigned',
  'proto': 'down',
  'status': 'administratively down'},
 {'intf': 'Loopback1', 'ipaddr': '56.56.56.56', 'proto': 'up', 'status': 'up'}]

## textfsm(ntc-template): show version
[{'config_register': '0x2102',
  'hardware': ['CSR1000V'],
  'hostname': 'csr1000v-1',
  'mac': [],
  'reload_reason': 'reload',
  'restarted': '',
  'rommon': 'IOS-XE',
  'running_image': 'packages.conf',
  'serial': ['9ESGOBARV9D'],
  'uptime': '1 day, 4 hours, 36 minutes',
  'uptime_days': '1',
  'uptime_hours': '4',
  'uptime_minutes': '36',
  'uptime_weeks': '',
  'uptime_years': '',
  'version': '17.3.1a'}]

## genie: show interface brief
{'interface': {'GigabitEthernet1': {'interface_is_ok': 'YES',
                                    'ip_address': '10.10.20.48',
                                    'method': 'NVRAM',
                                    'protocol': 'up',
                                    'status': 'up'},
               'GigabitEthernet2': {'interface_is_ok': 'YES',
                                    'ip_address': '192.168.1.1',
                                    'method': 'other',
                                    'protocol': 'up',
                                    'status': 'up'},
               'GigabitEthernet3': {'interface_is_ok': 'YES',
                                    'ip_address': 'unassigned',
                                    'method': 'NVRAM',
                                    'protocol': 'down',
                                    'status': 'administratively down'},
               'Loopback1': {'interface_is_ok': 'YES',
                             'ip_address': '56.56.56.56',
                             'method': 'manual',
                             'protocol': 'up',
                             'status': 'up'}}}

## genie: show version
{'version': {'chassis': 'CSR1000V',
             'chassis_sn': '9ESGOBARV9D',
             'compiled_by': 'mcpre',
             'compiled_date': 'Wed 12-Aug-20 00:16',
             'curr_config_register': '0x2102',
             'disks': {'bootflash:.': {'disk_size': '6188032',
                                       'type_of_disk': 'virtual hard disk'}},
             'hostname': 'csr1000v-1',
             'image_id': 'X86_64_LINUX_IOSD-UNIVERSALK9-M',
             'image_type': 'production image',
             'label': 'RELEASE SOFTWARE (fc3)',
             'last_reload_reason': 'reload',
             'license_level': 'ax',
             'license_type': 'N/A(Smart License Enabled)',
             'main_mem': '715705',
             'mem_size': {'non-volatile configuration': '32768',
                          'physical': '3978420'},
             'next_reload_license_level': 'ax',
             'number_of_intfs': {'Gigabit Ethernet': '3'},
             'os': 'IOS-XE',
             'platform': 'Virtual XE',
             'processor_type': 'VXE',
             'returned_to_rom_by': 'reload',
             'rom': 'IOS-XE ROMMON',
             'router_operating_mode': 'Autonomous',
             'rtr_type': 'CSR1000V',
             'system_image': 'bootflash:packages.conf',
             'uptime': '1 day, 4 hours, 36 minutes',
             'uptime_this_cp': '1 day, 4 hours, 38 minutes',
             'version': '17.3.1a',
             'version_short': '17.3',
             'xe_version': '17.03.01a'}}

パースも簡単にできます。

設定変更

send_configsで設定を送ることができます。 configureモードへの変更などはドライバー側で入力してくれるため、設定のみを指定します。

from scrapli import Scrapli
import pprint

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res_configure = conn.send_configs(["interface loopback100", "description configured by scrapli"])

print(res_configure.result)

resultには実行したコマンドと実行時の標準出力が格納されています。

python configure1.py 
interface loopback100
description configured by scrapli

存在しないリソースを消そうとした。などでエラーメッセージが出た場合もresultに格納されます

python configure_back.py  # loopback100がない状態で削除をしようとした
no interface loopback100
                                        ^
% Invalid input detected at '^' marker.

ここまでの要素を組み合わせて、 参照→変更→参照の流れを実施します。 設定前と設定後のshow interfaceを取得しパースしてテーブルにします。 テーブル作成のため、numpyとtabulateを利用します。

from scrapli import Scrapli
import tabulate
import numpy as np

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res_before = conn.send_command("show interfaces")
   res_configure = conn.send_configs(["interface loopback100", "description configured by scrapli"])
   res_after = conn.send_command("show interfaces")

parsed_before = res_before.genie_parse_output()
parsed_after = res_after.genie_parse_output()


## NOTE: 出力結果をテーブルにする処理
after_interface_list = list()
after_description_list = list()
before_interface_list = list()
before_description_list = list()

[before_interface_list.append(i) for i in parsed_before]
[before_description_list.append(parsed_after[i]['description']) for i in parsed_before]

[after_interface_list.append(i) for i in parsed_after]
[after_description_list.append(parsed_after[i]['description']) for i in parsed_after]

headers = ["interface", "description"]
table_before = [before_interface_list, before_description_list]
table_after= [after_interface_list, after_description_list]

res_table_before = tabulate.tabulate(np.array(table_before).transpose(), headers,tablefmt="grid")
res_table_after = tabulate.tabulate(np.array(table_after).transpose(), headers,tablefmt="grid")

## NOTE: テーブルの表示
print("==BEFORE==")
print(res_table_before)

print("==AFTER==")
print(res_table_after)

実行します

python configure.py 
==BEFORE==
+------------------+---------------------------------------+
| interface        | description                           |
+==================+=======================================+
| GigabitEthernet1 | MANAGEMENT INTERFACE - DON'T TOUCH ME |
+------------------+---------------------------------------+
| GigabitEthernet2 | Configured by RESTCONF                |
+------------------+---------------------------------------+
| GigabitEthernet3 | Network Interface                     |
+------------------+---------------------------------------+
| Loopback1        | This is a test                        |
+------------------+---------------------------------------+
==AFTER==
+------------------+---------------------------------------+
| interface        | description                           |
+==================+=======================================+
| GigabitEthernet1 | MANAGEMENT INTERFACE - DON'T TOUCH ME |
+------------------+---------------------------------------+
| GigabitEthernet2 | Configured by RESTCONF                |
+------------------+---------------------------------------+
| GigabitEthernet3 | Network Interface                     |
+------------------+---------------------------------------+
| Loopback1        | This is a test                        |
+------------------+---------------------------------------+
| Loopback100      | configured by scrapli                 |
+------------------+---------------------------------------+

変更前と変更後のテーブルを表示してみました、Loopback100が追加されたことも確認できます。

Netconf

最後にScrapliの特徴の一つでもあるNetconfにドライバーも試してみます。

事前にscrapli-netconfをインストールしておきます。

pip install scrapli-netconf

スクリプト作成

from scrapli_netconf.driver import NetconfDriver

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "port": 830,
}

INTERFACE_FILTER = """
          <if:interfaces xmlns:if="urn:ietf:params:xml:ns:yang:ietf-interfaces">
          </if:interfaces>
          """

with NetconfDriver(**device) as nc_conn:
  response= nc_conn.get(filter_=INTERFACE_FILTER)

print(response.result)

Interfaeの情報を取得します。

実行

python netconf_get.py 
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
  <data>
    <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
      <interface>
        <name>GigabitEthernet1</name>
        <description>MANAGEMENT INTERFACE - DON'T TOUCH ME</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
          <address>
            <ip>10.10.20.48</ip>
            <netmask>255.255.255.0</netmask>
          </address>
        </ipv4>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>GigabitEthernet2</name>
        <description>Configured by RESTCONF</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
          <address>
            <ip>192.168.1.1</ip>
            <netmask>255.255.255.252</netmask>
          </address>
        </ipv4>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>GigabitEthernet3</name>
        <description>Network Interface</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>
        <enabled>false</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>Loopback1</name>
        <description>This is a test</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:softwareLoopback</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
          <address>
            <ip>56.56.56.56</ip>
            <netmask>255.255.255.255</netmask>
          </address>
        </ipv4>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>Loopback100</name>
        <description>configured by scrapli</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:softwareLoopback</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>Loopback101</name>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:softwareLoopback</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
          <address>
            <ip>101.101.101.101</ip>
            <netmask>255.255.255.255</netmask>
          </address>
        </ipv4>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
    </interfaces>
  </data>
</rpc-reply>

情報取得できることを確認できました。

設定変更(edit)やコンフィグ取得(get_config)などのメソッドも用意されています。

まとめ

Scrapliに入門してみました。Netmikoを触ったことがあったので抵抗なく始めることができました、それくらい似ていると思います。
DriverにはAsync用のものもあり非同期処理にも対応しているようです。 標準ライブラリconcurrent.futuresを使って平行処理も実装してみましたが、軽量でサクサク動くので便利です。

興味があればぜひ試してみてください。

Golangで書かれたScrapligoもあるようです。 github.com

GitlabRunnerインストールしてGitlabCIを始める

この記事はGitLab Advent Calendar 2021の10日目の記事です

qiita.com

やりたかったこと

自動化関連の仕事を始めて、
AnsibleのPlaybookを管理するのにGitlabを利用することが増えてきました。
ただ、GitlabCIはあまり利用できていなかったので、勉強もかねて構築してみました。

環境

サーバ(AWSで構築)

Gitlab

  • gitlab/gitlab-ee: 14.4.2-ee
  • gitlab/gitlab-runner: 14.4.0

サーバーにGitlabのみ構築している状態です。
f:id:usage_automate:20211210091214p:plain

GitlabRunnerインストール

GitlabRunnerをインストールします。 公式ドキュメントを参考にdocker runを実施します。

# docker run -d --name gitlab-runner --restart always \
>      -v /srv/gitlab-runner/config:/etc/gitlab-runner \
>      -v /var/run/docker.sock:/var/run/docker.sock \
>      gitlab/gitlab-runner:latest

起動していることの確認

docker ps
CONTAINER ID   IMAGE                                 COMMAND                    CREATED        STATUS                    PORTS                       NAMES
23e7207ac5a8   gitlab/gitlab-runner:latest   "/usr/bin/dumb-init …"   3 weeks ago   Up 34 minutes                                         gitlab-runner

GitlabRunner登録

続いて、GitlabRunnerをGitlabに登録します。
f:id:usage_automate:20211210091647p:plain

事前にGitlab側でトークンを確認します。
管理者エリアのrunnerの設定にトークンが記載されています。

f:id:usage_automate:20211210085650p:plain

docker runコマンドでGitlab RunnerからGitlabへの登録を実施します。
登録時に、runner名やタイプ(docker,shellなど)、Dockerの場合は利用するImageなどを対話形式で入力します。 ★がついてるものが入力したもの (コマンド実行時にオプションで指定することもできるようです。)

# docker run --rm -it -v /srv/gitlab-runner/config:/etc/gitlab-runner gitlab/gitlab-runner register
Runtime platform                                    arch=amd64 os=linux pid=7 revision=4b9e985a version=14.4.0
Running in system-mode.                            
                                                   
Enter the GitLab instance URL (for example, https://gitlab.com/):
http://XXXXXXXXXXX ★
Enter the registration token:
XXXXXXXXXXXXXXXXXX ★
Enter a description for the runner:
[fa6965628a91]: 
Enter tags for the runner (comma-separated):

Registering runner... succeeded                     runner=KxYfjwgb
Enter an executor: custom, docker, docker-ssh, shell, docker+machine, docker-ssh+machine, parallels, ssh, virtualbox, kubernetes:
docker ★
Enter the default Docker image (for example, ruby:2.6):
ubuntu ★
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded! 

successfullyが表示され登録が完了すると、Gitlabの管理者エリアのRunnerの設定(トークンを確認したページ)にRunnerが表示されます。 f:id:usage_automate:20211210085712p:plain

Runnerをクリックして、利用するプロジェクトを設定します。 f:id:usage_automate:20211210085817p:plain

有効化後 f:id:usage_automate:20211210085845p:plain

Gitlab runner設定変更

Runnerの登録が完了しましたが、このままCIを実行すると失敗してしまいます。 CIをするコンテナ(CIコンテナ)からGitlabのExternal IPへアクセスできないことが原因でした。 f:id:usage_automate:20211210090832p:plain

runnerの設定ファイルを編集します。

# cd /srv/gitlab-runner/config
# cp config.toml config.toml .org
# vi config.toml

clone_urlとpull_policyの設定を追加しました。
clone_urlは文字通りcloneする際に利用するurlになります。今回はこちらを内部IPに変更しました。
pull_policyはneverにすることでローカルのイメージを利用できます。

concurrent = 1
check_interval = 0

[session_server]
  session_timeout = 1800

[[runners]]
  name = "fa6965628a91"
  url = "http://XXXXXXXXXXXXX"
  token = "XXXXXXXXXXXXXXX"
  executor = "docker"
  clone_url = "http://YYYYYYYY" ★追記
  [runners.custom_build_dir]
  [runners.cache]
    [runners.cache.s3]
    [runners.cache.gcs]
    [runners.cache.azure]
  [runners.docker]
    tls_verify = false
    image = "ubuntu"
    privileged = false
    disable_entrypoint_overwrite = false
    oom_kill_disable = false
    disable_cache = false
    volumes = ["/cache"]
    shm_size = 0
    pull_policy = "never" ★追記

これでRunnerの設定は完了です。cloneも問題なくできるようになりました。 f:id:usage_automate:20211210090403p:plain

Dockerイメージの作成

ここからはDockerのイメージを作成し、GitlabCIを動かしてみます。

下記Dockerfileを用意したのでビルドします。

FROM ubuntu
RUN apt-get update -y
RUN apt-get install python-pip -y
RUN pip install ansible-core
RUN pip install ansible-lint
RUN pip install yamllint

ビルドのログ

# docker build . -t test_image
Sending build context to Docker daemon   25.6kB
Step 1/6 : FROM ubuntu
 ---> ba6acccedd29
Step 2/6 : RUN apt-get update -y
 ---> Using cache
~snip~
Successfully built fe72d7eaaf20
Successfully tagged test_image:latest

# docker images
REPOSITORY             TAG       IMAGE ID       CREATED          SIZE
test_image             latest    fe72d7eaaf20   10 seconds ago   473MB
~ snip ~

ビルドが完了しローカルにDockerイメージがあることを確認出来たらgitlabにアップするリポジトリ内に.gitlan-ci.ymlを書いていきます。
.gitlab-ci.ymlはテストの内容を記載したYamlファイルです、stageなどを利用し分割したり条件分岐させたりすることも可能なようです。

.gitlab-ci.yml

image: test_image

stages:
  - lint

lint-job:
  stage: lint
  script:
    - echo "$GITLAB_USER_LOGIN!"

準備が完了したので、gitlabのプロジェクトを更新しCIが動くかを確認します。

f:id:usage_automate:20211210093810p:plain

プロジェクト -> CI/CD -> パイプラインを確認し、ステータスが成功し処理が動いていることを確認できました。

ちなみに、config.tomlでclone_urlの設定をしていないとExternal_IPにアクセスして失敗しているログは以下です。 f:id:usage_automate:20211210094316p:plain

clone_urlの設定見つけるまで時間がかかってしまって苦労しました・・
(そもそもちゃんとexternal_ipの設定をちゃんとしていないのも悪い)

まとめ

GitlabCIを動かしてみる!といった入門的な内容でしたが、それなりに苦労しました。
そもそも、GitlabRunnerはコンテナでさらにCIを動かす別のコンテナがあることも理解ができていなかったですが、作ってみることで少しは理解できたかなぁといったところです。

参考にさせていただいたサイト

今見るとやってることほぼ参考にさせてもらってる神ブログ

zaki-hmkc.hatenablog.com

公式ドキュメント

docs.gitlab.com

docs.gitlab.com