LoginSignup
7

More than 1 year has passed since last update.

posted at

updated at

pyATS と Python辞書 が楽しくなる Dq(Data Query) を極める!!!

この記事はシスコの有志による Cisco Systems Japan Advent Calendar 2020 2枚目の 11 日目として投稿しています。

2020年版: https://qiita.com/advent-calendar/2020/cisco (<<<今年)
2020年版(2枚目): https://qiita.com/advent-calendar/2020/cisco2 (<<<今年)
2019年版: https://qiita.com/advent-calendar/2019/cisco
2018年版: https://qiita.com/advent-calendar/2018/cisco
2017年版: https://qiita.com/advent-calendar/2017/cisco

はじめに

pyATS 開発チームに所属する東村(@tahigash3)です。今回は Cisco が開発しているネットワーク自動化ツール pyATSDq(Data Query) という機能を紹介したいと思います。タイトルにも書いた通り、pyATS のために生まれた機能ですが、Python辞書を扱うのにも便利な機能となっています。pyATS コミュニティでも Dq の評判は良く、人気が高いため今回は Dq のみに特化してみたいと思います!

今年のアドベントカレンダー1枚目のおいて、下記記事を書いているので、そちらも合わせてご覧ください。
- DevNet Sandbox を使って pyATS/XPRESSO を CML2 と始めよう
今回は上記の記事で紹介をした DevNet Sandbox の Devbox 上で Dq を使っていきます。

過去の記事でも pyATS について書いているので、こちらも合わせて参照ください。
- pyATS|Genieの隠れ必殺技Blitzを用いて高速自動化テスト作成
- pyATS/GenieとRobot Framework で簡単にマルチベンダーネットワークをチェックする

Python で Python辞書 を扱う場合の悩み

Python辞書は構造化データを格納することができますが、そのデータの中身にアクセスしようと考えた場合に少し苦労が発生します。下記ネットワーク機器のデバイス名、IP、ユーザ名、パスワードを Python 辞書で書いてみました。(書き方は色々あるかと思いますが、一例として見てください)

python辞書
{
    'device': {
        'router1': {
            'ip': '10.1.1.1',
            'username': 'apple',
            'password': 'elppa',
        },
        'router2': {
            'ip': '10.2.2.2',
            'username': 'banana',
            'password': 'ananab',
        }
    }
}

見てもらうと分かりますが、複数レベルの階層になっています。この場合に例えば、router2ip を探したいとした場合、ループを使って下記のようにすることができます。

In [21]: device_dict = {
    ...:     'device': {
    ...:         'router1': {
    ...:             'ip': '10.1.1.1',
    ...:             'username': 'apple',
    ...:             'password': 'elppa',
    ...:         },
    ...:         'router2': {
    ...:             'ip': '10.2.2.2',
    ...:             'username': 'banana',
    ...:             'password': 'ananab',
    ...:         }
    ...:     }
    ...: }

In [22]: for device in device_dict['device']:
    ...:     if 'router2' == device:
    ...:         print("router2 ip: {}".format(device_dict['device'][device]['ip']))
    ...:
router2 ip: 10.2.2.2

見てもらうと分かるように、for ループをして if 文で条件判定して、辞書の特定の値を表示するというコードになります。上の例では比較的まだシンプルな Python辞書 のため、そこまで苦労しませんが、階層が多く、深くなってくると多段でループすることになり、コードが複雑化していきます。

それでは実際に pyATS の Genie Parser で得られる構造化データ(Python辞書)を見てみましょう。
DevNet Sandbox の Devbox (10.10.20.50) へログインして、1枚目の記事 を参考に用意されている testbed yaml でデバイス dist-rtr01 から show ip route の結果を取得してみます。(下記では ipython もインストールしています。ipython をインストールしている場合は、pyats shell コマンドで標準のPythonインタープリタではなく ipython が使われるようになります。)

$ ssh developer@10.10.20.50
developer@10.10.20.50's password:
(py3venv) [developer@devbox ~]$ cd py3venv/
(py3venv) [developer@devbox py3venv]$ pip install ipython
(py3venv) [developer@devbox py3venv]$ pyats shell --testbed-file multi-platform-network.yaml
/home/developer/py3venv/lib/python3.6/site-packages/IPython/core/history.py:226: UserWarning: IPython History requires SQLite, your history will not be saved
  warn("IPython History requires SQLite, your history will not be saved")
Welcome to pyATS Interactive Shell
==================================
Python 3.6.8 (default, Sep 14 2019, 14:33:46)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)]

>>> from genie.testbed import load
>>> testbed = load('multi-platform-network.yaml')
-------------------------------------------------------------------------------
In [1]: device = testbed.devices['dist-rtr01']

In [2]: device.connect()
(snip)
In [3]: output = device.parse('show ip route')

2020-12-09 18:30:13,838: %UNICON-INFO: +++ dist-rtr01: executing command 'show ip route' +++
show ip route
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, m - OMP
       n - NAT, Ni - NAT inside, No - NAT outside, Nd - NAT DIA
       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
       H - NHRP, G - NHRP registered, g - NHRP registration summary
       o - ODR, P - periodic downloaded static route, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      172.16.0.0/16 is variably subnetted, 21 subnets, 4 masks
O        172.16.101.0/24 [110/41] via 172.16.252.9, 00:34:56, GigabitEthernet5
                         [110/41] via 172.16.252.1, 00:37:08, GigabitEthernet4
O        172.16.102.0/24 [110/41] via 172.16.252.9, 00:36:58, GigabitEthernet5
                         [110/41] via 172.16.252.1, 00:34:56, GigabitEthernet4
O        172.16.103.0/24 [110/41] via 172.16.252.9, 00:34:51, GigabitEthernet5
                         [110/41] via 172.16.252.1, 00:34:51, GigabitEthernet4
O        172.16.104.0/24 [110/41] via 172.16.252.9, 00:34:51, GigabitEthernet5
                         [110/41] via 172.16.252.1, 00:34:51, GigabitEthernet4
O        172.16.105.0/24 [110/41] via 172.16.252.9, 00:34:51, GigabitEthernet5
                         [110/41] via 172.16.252.1, 00:34:51, GigabitEthernet4
C        172.16.252.0/30 is directly connected, GigabitEthernet4
L        172.16.252.2/32 is directly connected, GigabitEthernet4
O        172.16.252.4/30 [110/2] via 172.16.252.18, 00:40:11, GigabitEthernet6
C        172.16.252.8/30 is directly connected, GigabitEthernet5
L        172.16.252.10/32 is directly connected, GigabitEthernet5
O        172.16.252.12/30
           [110/2] via 172.16.252.18, 00:40:11, GigabitEthernet6
C        172.16.252.16/30 is directly connected, GigabitEthernet6
L        172.16.252.17/32 is directly connected, GigabitEthernet6
C        172.16.252.20/30 is directly connected, GigabitEthernet2
L        172.16.252.21/32 is directly connected, GigabitEthernet2
C        172.16.252.24/30 is directly connected, GigabitEthernet3
L        172.16.252.25/32 is directly connected, GigabitEthernet3
O        172.16.252.28/30
           [110/2] via 172.16.252.22, 00:37:23, GigabitEthernet2
           [110/2] via 172.16.252.18, 00:40:11, GigabitEthernet6
O        172.16.252.32/30
           [110/2] via 172.16.252.26, 00:37:31, GigabitEthernet3
           [110/2] via 172.16.252.18, 00:40:11, GigabitEthernet6
O        172.16.252.36/30
           [110/2] via 172.16.252.26, 00:37:31, GigabitEthernet3
           [110/2] via 172.16.252.22, 00:36:48, GigabitEthernet2
O        172.16.253.0/29 [110/2] via 172.16.252.26, 00:37:31, GigabitEthernet3
                         [110/2] via 172.16.252.22, 00:37:23, GigabitEthernet2
O E2  172.31.0.0/16 [110/20] via 172.16.252.26, 00:37:31, GigabitEthernet3
                    [110/20] via 172.16.252.22, 00:37:23, GigabitEthernet2
dist-rtr01#

上記で変数 outputshow ip route を Genie Parser でパースした構造化データを入れました。json ライブラリを使って、どのような構造体になっているかを確認します。

In [4]: import json

In [5]: print(json.dumps(output, indent=2, sort_keys=True))
{
  "vrf": {
    "default": {
      "address_family": {
        "ipv4": {
          "routes": {
            "172.16.101.0/24": {
              "active": true,
              "metric": 41,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.9",
                    "outgoing_interface": "GigabitEthernet5",
                    "updated": "00:34:56"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.1",
                    "outgoing_interface": "GigabitEthernet4",
                    "updated": "00:37:08"
                  }
                }
              },
              "route": "172.16.101.0/24",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.102.0/24": {
              "active": true,
              "metric": 41,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.9",
                    "outgoing_interface": "GigabitEthernet5",
                    "updated": "00:36:58"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.1",
                    "outgoing_interface": "GigabitEthernet4",
                    "updated": "00:34:56"
                  }
                }
              },
              "route": "172.16.102.0/24",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.103.0/24": {
              "active": true,
              "metric": 41,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.9",
                    "outgoing_interface": "GigabitEthernet5",
                    "updated": "00:34:51"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.1",
                    "outgoing_interface": "GigabitEthernet4",
                    "updated": "00:34:51"
                  }
                }
              },
              "route": "172.16.103.0/24",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.104.0/24": {
              "active": true,
              "metric": 41,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.9",
                    "outgoing_interface": "GigabitEthernet5",
                    "updated": "00:34:51"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.1",
                    "outgoing_interface": "GigabitEthernet4",
                    "updated": "00:34:51"
                  }
                }
              },
              "route": "172.16.104.0/24",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.105.0/24": {
              "active": true,
              "metric": 41,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.9",
                    "outgoing_interface": "GigabitEthernet5",
                    "updated": "00:34:51"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.1",
                    "outgoing_interface": "GigabitEthernet4",
                    "updated": "00:34:51"
                  }
                }
              },
              "route": "172.16.105.0/24",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.252.0/30": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet4": {
                    "outgoing_interface": "GigabitEthernet4"
                  }
                }
              },
              "route": "172.16.252.0/30",
              "source_protocol": "connected",
              "source_protocol_codes": "C"
            },
            "172.16.252.10/32": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet5": {
                    "outgoing_interface": "GigabitEthernet5"
                  }
                }
              },
              "route": "172.16.252.10/32",
              "source_protocol": "local",
              "source_protocol_codes": "L"
            },
            "172.16.252.12/30": {
              "active": true,
              "metric": 2,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.18",
                    "outgoing_interface": "GigabitEthernet6",
                    "updated": "00:40:11"
                  }
                }
              },
              "route": "172.16.252.12/30",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.252.16/30": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet6": {
                    "outgoing_interface": "GigabitEthernet6"
                  }
                }
              },
              "route": "172.16.252.16/30",
              "source_protocol": "connected",
              "source_protocol_codes": "C"
            },
            "172.16.252.17/32": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet6": {
                    "outgoing_interface": "GigabitEthernet6"
                  }
                }
              },
              "route": "172.16.252.17/32",
              "source_protocol": "local",
              "source_protocol_codes": "L"
            },
            "172.16.252.2/32": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet4": {
                    "outgoing_interface": "GigabitEthernet4"
                  }
                }
              },
              "route": "172.16.252.2/32",
              "source_protocol": "local",
              "source_protocol_codes": "L"
            },
            "172.16.252.20/30": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet2": {
                    "outgoing_interface": "GigabitEthernet2"
                  }
                }
              },
              "route": "172.16.252.20/30",
              "source_protocol": "connected",
              "source_protocol_codes": "C"
            },
            "172.16.252.21/32": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet2": {
                    "outgoing_interface": "GigabitEthernet2"
                  }
                }
              },
              "route": "172.16.252.21/32",
              "source_protocol": "local",
              "source_protocol_codes": "L"
            },
            "172.16.252.24/30": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet3": {
                    "outgoing_interface": "GigabitEthernet3"
                  }
                }
              },
              "route": "172.16.252.24/30",
              "source_protocol": "connected",
              "source_protocol_codes": "C"
            },
            "172.16.252.25/32": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet3": {
                    "outgoing_interface": "GigabitEthernet3"
                  }
                }
              },
              "route": "172.16.252.25/32",
              "source_protocol": "local",
              "source_protocol_codes": "L"
            },
            "172.16.252.28/30": {
              "active": true,
              "metric": 2,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.22",
                    "outgoing_interface": "GigabitEthernet2",
                    "updated": "00:37:23"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.18",
                    "outgoing_interface": "GigabitEthernet6",
                    "updated": "00:40:11"
                  }
                }
              },
              "route": "172.16.252.28/30",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.252.32/30": {
              "active": true,
              "metric": 2,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.26",
                    "outgoing_interface": "GigabitEthernet3",
                    "updated": "00:37:31"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.18",
                    "outgoing_interface": "GigabitEthernet6",
                    "updated": "00:40:11"
                  }
                }
              },
              "route": "172.16.252.32/30",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.252.36/30": {
              "active": true,
              "metric": 2,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.26",
                    "outgoing_interface": "GigabitEthernet3",
                    "updated": "00:37:31"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.22",
                    "outgoing_interface": "GigabitEthernet2",
                    "updated": "00:36:48"
                  }
                }
              },
              "route": "172.16.252.36/30",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.252.4/30": {
              "active": true,
              "metric": 2,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.18",
                    "outgoing_interface": "GigabitEthernet6",
                    "updated": "00:40:11"
                  }
                }
              },
              "route": "172.16.252.4/30",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.16.252.8/30": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet5": {
                    "outgoing_interface": "GigabitEthernet5"
                  }
                }
              },
              "route": "172.16.252.8/30",
              "source_protocol": "connected",
              "source_protocol_codes": "C"
            },
            "172.16.253.0/29": {
              "active": true,
              "metric": 2,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.26",
                    "outgoing_interface": "GigabitEthernet3",
                    "updated": "00:37:31"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.22",
                    "outgoing_interface": "GigabitEthernet2",
                    "updated": "00:37:23"
                  }
                }
              },
              "route": "172.16.253.0/29",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O"
            },
            "172.31.0.0/16": {
              "active": true,
              "metric": 20,
              "next_hop": {
                "next_hop_list": {
                  "1": {
                    "index": 1,
                    "next_hop": "172.16.252.26",
                    "outgoing_interface": "GigabitEthernet3",
                    "updated": "00:37:31"
                  },
                  "2": {
                    "index": 2,
                    "next_hop": "172.16.252.22",
                    "outgoing_interface": "GigabitEthernet2",
                    "updated": "00:37:23"
                  }
                }
              },
              "route": "172.31.0.0/16",
              "route_preference": 110,
              "source_protocol": "ospf",
              "source_protocol_codes": "O E2"
            }
          }
        }
      }
    }
  }
}

なかなか深い階層の構造体になりましたね。でも恐れることはありません、私たちには Dq(Data Query) があります。それでは次から Dq を使って、このルーティングテーブルの構造体から特定の情報を抽出/確認する方法を紹介していきます。

Dq の基本的な使い方

Dq の使い方ですが、Python 辞書のデータ(上の場合では output) があれば、モジュールをインポートして、Dq に Python 辞書を渡すと同時に、Query のチェーンアクションを繋げて使います。

Python 辞書に対して Dq を使う

output に既に上のルーティングテーブルの構造体が入っているものとして進めます。Dq をインポートして、Dqoutput を渡し、チェーンアクション containsget_values を用いてキーワード ospfroutes を入れて実行して見ます。

In [7]: from genie.utils import Dq

In [8]: Dq(output).contains('ospf').get_values('routes')
Out[8]:
['172.16.101.0/24',
 '172.16.102.0/24',
 '172.16.103.0/24',
 '172.16.104.0/24',
 '172.16.105.0/24',
 '172.16.252.4/30',
 '172.16.252.12/30',
 '172.16.252.28/30',
 '172.16.252.32/30',
 '172.16.252.36/30',
 '172.16.253.0/29',
 '172.31.0.0/16']

見てもらうと何となく分かるかと思いますが、Dq(output) でこれから、output を Dq で解析する、contains('ospf')ospf をいうキーワードが含まれるものを検索し、get_values('routes') で、contains での検索結果から routes の値を取得せよという実行になります。チェーンアクションと言われる由縁は、上記のように連結して結果を絞ることができるからです。

もっと連結した例を見てみましょう。

In [22]: Dq(output).contains('next_hop').contains('GigabitEthernet2').get_values('routes')
Out[22]:
['172.16.252.20/30',
 '172.16.252.21/32',
 '172.16.252.28/30',
 '172.16.252.36/30',
 '172.16.253.0/29',
 '172.31.0.0/16']

contains を二つ繋げてみました。最初の containsnext_hop というキーワードで絞り込み、次にインタフェース名で絞り、該当する routes を取得しています。

複数のチェーンアクションを連結して、検索結果を絞り込むことができることを見せましたが、上記の例は 'outgoing_interface': 'GigabitEthernet2' という構造体の場所にマッチするように想定したものですが、contains にマッチするものは構造体のどこにあっても構わないため、意図しない箇所でヒットするかもしれません。outgoing_interface で確実にマッチさせたい場合はキーと値を指定できる contains_key_value() というものがあるため、そちらを使う方が実際には正確に絞り込め、スマートに書けます。

In [23]: Dq(output).contains_key_value('outgoing_interface', 'GigabitEthernet2').get_values('routes')
Out[23]:
['172.16.252.20/30',
 '172.16.252.21/32',
 '172.16.252.28/30',
 '172.16.252.36/30',
 '172.16.253.0/29',
 '172.31.0.0/16']

先の contains 二つ使った場合と、contains_key_value で結果が同じになっていることが確認できるかと思います。
ここまでが Python 辞書に対して Dq を使う場合の方法になります。

Genie Parser の結果に対して Dq を使う

Genie Parser でパース結果を得た時は、パーサーから返される辞書に既に Dq が組み込まれており、パーサーからの結果を格納した変数から Dq の機能を直接使えます。

In [24]: output = device.parse('show ip route')

2020-12-09 19:07:27,147: %UNICON-INFO: +++ dist-rtr01: executing command 'show ip route' +++
show ip route
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, m - OMP
       n - NAT, Ni - NAT inside, No - NAT outside, Nd - NAT DIA
       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
       H - NHRP, G - NHRP registered, g - NHRP registration summary
       o - ODR, P - periodic downloaded static route, l - LISP
       a - application route
       + - replicated route, % - next hop override, p - overrides from PfR

Gateway of last resort is not set

      172.16.0.0/16 is variably subnetted, 21 subnets, 4 masks
O        172.16.101.0/24 [110/41] via 172.16.252.9, 01:12:09, GigabitEthernet5
                         [110/41] via 172.16.252.1, 01:14:21, GigabitEthernet4
O        172.16.102.0/24 [110/41] via 172.16.252.9, 01:14:11, GigabitEthernet5
                         [110/41] via 172.16.252.1, 01:12:09, GigabitEthernet4
O        172.16.103.0/24 [110/41] via 172.16.252.9, 01:12:04, GigabitEthernet5
                         [110/41] via 172.16.252.1, 01:12:04, GigabitEthernet4
O        172.16.104.0/24 [110/41] via 172.16.252.9, 01:12:04, GigabitEthernet5
                         [110/41] via 172.16.252.1, 01:12:04, GigabitEthernet4
O        172.16.105.0/24 [110/41] via 172.16.252.9, 01:12:04, GigabitEthernet5
                         [110/41] via 172.16.252.1, 01:12:04, GigabitEthernet4
C        172.16.252.0/30 is directly connected, GigabitEthernet4
L        172.16.252.2/32 is directly connected, GigabitEthernet4
O        172.16.252.4/30 [110/2] via 172.16.252.18, 01:17:24, GigabitEthernet6
C        172.16.252.8/30 is directly connected, GigabitEthernet5
L        172.16.252.10/32 is directly connected, GigabitEthernet5
O        172.16.252.12/30
           [110/2] via 172.16.252.18, 01:17:24, GigabitEthernet6
C        172.16.252.16/30 is directly connected, GigabitEthernet6
L        172.16.252.17/32 is directly connected, GigabitEthernet6
C        172.16.252.20/30 is directly connected, GigabitEthernet2
L        172.16.252.21/32 is directly connected, GigabitEthernet2
C        172.16.252.24/30 is directly connected, GigabitEthernet3
L        172.16.252.25/32 is directly connected, GigabitEthernet3
O        172.16.252.28/30
           [110/2] via 172.16.252.22, 01:14:36, GigabitEthernet2
           [110/2] via 172.16.252.18, 01:17:24, GigabitEthernet6
O        172.16.252.32/30
           [110/2] via 172.16.252.26, 01:14:44, GigabitEthernet3
           [110/2] via 172.16.252.18, 01:17:24, GigabitEthernet6
O        172.16.252.36/30
           [110/2] via 172.16.252.26, 01:14:44, GigabitEthernet3
           [110/2] via 172.16.252.22, 01:14:01, GigabitEthernet2
O        172.16.253.0/29 [110/2] via 172.16.252.26, 01:14:44, GigabitEthernet3
                         [110/2] via 172.16.252.22, 01:14:36, GigabitEthernet2
O E2  172.31.0.0/16 [110/20] via 172.16.252.26, 01:14:44, GigabitEthernet3
                    [110/20] via 172.16.252.22, 01:14:36, GigabitEthernet2
dist-rtr01#

In [25]: output.q.contains_key_value('outgoing_interface', 'GigabitEthernet2').get_values('routes')
Out[25]:
['172.16.252.20/30',
 '172.16.252.21/32',
 '172.16.252.28/30',
 '172.16.252.36/30',
 '172.16.253.0/29',
 '172.31.0.0/16']

違いが分かりましたでしょうか? 先の例では Dq(Python辞書).contains()... としていましたが、パーサー結果を格納した変数output に直接 .q.contains_key_values()... として Dq が実行できていることが確認できると思います。
もちろん output は Python辞書 と同じようにも使えますし、.q.<チェーンアクション>Dq をその変数に直接実行することも可能です。

実際には QDict というオブジェクトになっており、辞書と同じようにも扱えます。辞書に Dq が便利に使える機能を付与したものと考えてもらえれば良いかと思います。

In [32]: type(output)
Out[32]: genie.conf.base.utils.QDict

In [33]: output.keys()
Out[33]: dict_keys(['vrf'])

In [34]: output['vrf']['default']['address_family']['ipv4']['routes'].keys()
Out[34]: dict_keys(['172.16.101.0/24', '172.16.102.0/24', '172.16.103.0/24', '172.16.104.0/24', '172.16.105.0/24', '172.16.252.0/30', '172.16.252.2/32', '172.16.252.4/30', '172.16.252.8/30', '172.16.252.10/32', '172.16.252.12/30', '172.16.252.16/30', '172.16.252.17/32', '172.16.252.20/30', '172.16.252.21/32', '172.16.252.24/30', '172.16.252.25/32', '172.16.252.28/30', '172.16.252.32/30', '172.16.252.36/30', '172.16.253.0/29', '172.31.0.0/16'])

Dq の簡単な使い方をまとめてみます。

  • Python辞書に対してチェーンアクションを使って絞り込み、特定のデータを抽出できる
  • チェーンアクションは名前の通り、連結させて構造化データをどんどん絞りこめる
  • Genie Parser の結果は辞書+Dqなオブジェクトになっており、output.q.contains() と Dq をパース結果に対してシームレスに使える

それでは Dq が持つチェーンアクションについて紹介していきたいと思います。

様々なチェーンアクション

ネットワークの自動化ツール pyATS のために生まれただけに、チェーンアクションはネットワーク自動化を行う上で必要となるようなものが用意されています。

contains

既に紹介しましたが、contains は単純に Python 辞書の中から指定されたキーワードを検索してマッチしたもので絞り込みます。

下記は特定の経路 172.16.102.0/24 のメトリックを取得する例です。

In [36]: output.q.contains('172.16.102.0/24').get_values('metric')
Out[36]: [41]

また、regex=Truecontains の中に追加することで、キーワードに正規表現が使えるようになります。

In [39]: output.q.contains('.*/24', regex=True).get_values('routes')
Out[39]:
['172.16.101.0/24',
 '172.16.102.0/24',
 '172.16.103.0/24',
 '172.16.104.0/24',
 '172.16.105.0/24']

contains には level というものも指定できます。contains マッチしたキーワードにマッチするもので構造体を絞り込みます。すなわち、マッチする場所によっては、絞り込みによって捨てられてしまう場合があります。

{
  "vrf": {
    "default": {
      "address_family": {
        "ipv4": {
          "routes": {
            "172.16.252.17/32": {
              "active": true,
              "next_hop": {
                "outgoing_interface": {
                  "GigabitEthernet6": {
                    "outgoing_interface": "GigabitEthernet6"
                  }
                }
              },
              "route": "172.16.252.17/32",
              "source_protocol": "local",
              "source_protocol_codes": "L"
            },

上記経路の構造体を考えた時に、contains('local') とすると、下記のように絞り込みが行われます。

{
  "vrf": {
    "default": {
      "address_family": {
        "ipv4": {
          "routes": {
            "172.16.252.17/32": {
              "source_protocol": "local",
            },

そのため、active な経路を取得しようとしても、絞り込みで存在しなくなっているため、抽出できません。

In [7]: output.q.contains('local').contains('active').get_values('routes')
Out[7]: []

ローカル経路でアクティブな状態の経路という条件で検索するのができない事になります。この場合に使えるのが level です。level=-1 と書くとマッチした場所の一段階上の階層に戻って絞り込み結果を持つことができます。そのため、絞り込みを行いながら、並列に存在するデータを抽出することができるようになり、データ抽出の柔軟性が高まります。

In [6]: output.q.contains('local', level=-1).contains('active').get_values('routes')
Out[6]:
['172.16.252.2/32',
 '172.16.252.10/32',
 '172.16.252.17/32',
 '172.16.252.21/32',
 '172.16.252.25/32']

not_contains

名前の通り、contains の反対となります。除外したいキーワードを指定します。例えば、/24 以外の経路を抽出したい場合の例です。

In [12]: output.q.not_contains('.*/24', regex=True).get_values('routes')
Out[12]:
['172.16.252.0/30',
 '172.16.252.2/32',
 '172.16.252.4/30',
 '172.16.252.8/30',
 '172.16.252.10/32',
 '172.16.252.12/30',
 '172.16.252.16/30',
 '172.16.252.17/32',
 '172.16.252.20/30',
 '172.16.252.21/32',
 '172.16.252.24/30',
 '172.16.252.25/32',
 '172.16.252.28/30',
 '172.16.252.32/30',
 '172.16.252.36/30',
 '172.16.253.0/29',
 '172.31.0.0/16']

levelregex オプションは contains と同じく使うことができます。

NOTE: levelcontains/not_contains のみでサポートされます。

get_values

ここまででも使ってきていますが、チェーンアクションの最後に付け、マッチするものをリストで返します。
例えば、contains などを使わずに直接使うことも可能です。

In [13]: output.q.get_values('routes')
Out[13]:
['172.16.101.0/24',
 '172.16.102.0/24',
 '172.16.103.0/24',
 '172.16.104.0/24',
 '172.16.105.0/24',
 '172.16.252.0/30',
 '172.16.252.2/32',
 '172.16.252.4/30',
 '172.16.252.8/30',
 '172.16.252.10/32',
 '172.16.252.12/30',
 '172.16.252.16/30',
 '172.16.252.17/32',
 '172.16.252.20/30',
 '172.16.252.21/32',
 '172.16.252.24/30',
 '172.16.252.25/32',
 '172.16.252.28/30',
 '172.16.252.32/30',
 '172.16.252.36/30',
 '172.16.253.0/29',
 '172.31.0.0/16']

また、リストの番号を指定することで、リストではなく値そのものを返すことも可能です。(抽出されるものが一つだと分かりきっている場合などに有効です。リストから取り出す操作が必要なくなります。)

In [14]: output.q.get_values('routes', 0)
Out[14]: '172.16.101.0/24'

In [15]: output.q.get_values('routes', 1)
Out[15]: '172.16.102.0/24'

Python リストのスライス形式での抽出もできます。

In [17]: output.q.get_values('routes', '[4:6]')
Out[17]: ['172.16.105.0/24', '172.16.252.0/30']

contains_key_value

前述で少し触れましたが、contains が構造体のどこの場所にでもキーワードがヒットしてしまい、絞り込みを行うのに対し、contains_key_valueはキーと値のペアで絞り込みができるため、構造体の中で特定のキーと値のペアで絞り込みたい時に使います。

In [19]: output.q.contains_key_value('outgoing_interface', 'GigabitEthernet6').get_values('routes')
Out[19]:
['172.16.252.4/30',
 '172.16.252.12/30',
 '172.16.252.16/30',
 '172.16.252.17/32',
 '172.16.252.28/30',
 '172.16.252.32/30']

contains と同じくキーと値のキーワードに正規表現が使えますが、キーと値の両方があるため、それぞれ key_regex=Truevalue_regex=True を指定することで、キーワード内で正規表現が使えるようになります。

In [22]: output.q.contains_key_value('outgoing_.*', 'GigabitEthernet[456]', key_regex=True, value_regex=True).get_values('routes')
Out[22]:
['172.16.101.0/24',
 '172.16.102.0/24',
 '172.16.103.0/24',
 '172.16.104.0/24',
 '172.16.105.0/24',
 '172.16.252.0/30',
 '172.16.252.2/32',
 '172.16.252.4/30',
 '172.16.252.8/30',
 '172.16.252.10/32',
 '172.16.252.12/30',
 '172.16.252.16/30',
 '172.16.252.17/32',
 '172.16.252.28/30',
 '172.16.252.32/30']

not_contains_key_value

contains_key_value の反対で、除外したいキーと値のペアを指定します。また同様に key_regex=Truevalue_regex=True でキーワードに正規表現が使えます。

value_operator

演算子(==, !=, >=, <=, >, <) を使って絞り込みができます。
例えば、アドミニストレイティブディスタンスが 100 より高い値の経路のみを抽出したい場合時の例です。

In [24]: output.q.value_operator('route_preference', '>', 100).get_values('routes')
Out[24]:
['172.16.101.0/24',
 '172.16.102.0/24',
 '172.16.103.0/24',
 '172.16.104.0/24',
 '172.16.105.0/24',
 '172.16.252.4/30',
 '172.16.252.12/30',
 '172.16.252.28/30',
 '172.16.252.32/30',
 '172.16.252.36/30',
 '172.16.253.0/29',
 '172.31.0.0/16']

今回の例では OSPF (AD: 110) 経路のみが表示されているのが分かります。

count

チェーンアクションで絞り込んだ結果の数を返してくれます。get_values と同様にチェーンアクションの最後に付けます。

In [25]: output.q.value_operator('route_preference', '>', 100).count()
Out[25]: 12

raw

Python辞書のように階層を指定して辞書の要素にアクセスできます。

In [35]: output.q.raw('[vrf][default][address_family][ipv4][routes][172.31.0.0/16')
Out[35]:
{'route': '172.31.0.0/16',
 'active': True,
 'metric': 20,
 'route_preference': 110,
 'source_protocol_codes': 'O E2',
 'source_protocol': 'ospf',
 'next_hop': {'next_hop_list': {1: {'index': 1,
    'next_hop': '172.16.252.26',
    'updated': '01:45:55',
    'outgoing_interface': 'GigabitEthernet3'},
   2: {'index': 2,
    'next_hop': '172.16.252.22',
    'updated': '01:45:47',
    'outgoing_interface': 'GigabitEthernet2'}}}}

reconstruct

チェーンアクションでマッチした構造体を辞書の形で得ることができます。通常、チェーンアクションを実施した場合は下記のように Dq オブジェクトが返されます。

In [36]: output.q.contains('172.31.0.0/16')
Out[36]: Dq(paths=[DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'route'), value='172.31.0.0/16'), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'active'), value=True), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'metric'), value=20), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'route_preference'), value=110), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'source_protocol_codes'), value='O E2'), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'source_protocol'), value='ospf'), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'next_hop', 'next_hop_list', 1, 'index'), value=1), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'next_hop', 'next_hop_list', 1, 'next_hop'), value='172.16.252.26'), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'next_hop', 'next_hop_list', 1, 'updated'), value='01:45:55'), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'next_hop', 'next_hop_list', 1, 'outgoing_interface'), value='GigabitEthernet3'), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'next_hop', 'next_hop_list', 2, 'index'), value=2), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'next_hop', 'next_hop_list', 2, 'next_hop'), value='172.16.252.22'), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'next_hop', 'next_hop_list', 2, 'updated'), value='01:45:47'), DictItem(path=('vrf', 'default', 'address_family', 'ipv4', 'routes', '172.31.0.0/16', 'next_hop', 'next_hop_list', 2, 'outgoing_interface'), value='GigabitEthernet2')])

Python辞書として取得したい場合には reconstruct() を最後に付けます。

In [37]: output.q.contains('172.31.0.0/16').reconstruct()
Out[37]:
{'vrf': {'default': {'address_family': {'ipv4': {'routes': {'172.31.0.0/16': {'route': '172.31.0.0/16',
       'active': True,
       'metric': 20,
       'route_preference': 110,
       'source_protocol_codes': 'O E2',
       'source_protocol': 'ospf',
       'next_hop': {'next_hop_list': {1: {'index': 1,
          'next_hop': '172.16.252.26',
          'updated': '01:45:55',
          'outgoing_interface': 'GigabitEthernet3'},
         2: {'index': 2,
          'next_hop': '172.16.252.22',
          'updated': '01:45:47',
          'outgoing_interface': 'GigabitEthernet2'}}}}}}}}}}

絞り込んだ結果で Python で何らかの処理をしたい場合にも有用ですし、絞り込んだ結果がどのようなものであるか Dq の動作を把握したり、確認したりするのにも使えます。

番外編

pyATS の testbed.yaml を読み込んだ場合には、testbed オブジェクトに raw_config というアトリビュートがあり、testbed の内容を Python辞書 で確認できます。

In [58]: testbed.raw_config
Out[58]:
{'testbed': {'name': 'Sandbox-Multi-Platform-Network',
  'credentials': {'default': {'username': 'cisco', 'password': 'cisco'}},
  'testbed_file': 'multi-platform-network.yaml'},
 'devices': {'terminal_server': {'os': 'linux',
   'type': 'linux',
   'credentials': {'default': {'username': 'developer',
     'password': 'C1sco12345'}},
   'connections': {'cli': {'protocol': 'ssh', 'ip': '10.10.20.161'}}},
  'internet-rtr01': {'os': 'iosxe',
   'type': 'router',
   'series': 'csr1000v',
   'connections': {'cli': {'protocol': 'telnet',
     'proxy': 'terminal_server',
     'command': 'open /e65a3c/n0/0'}}},
(snip)

この testbed.raw_configDq を使うことにより、読み込まれている testbed オブジェクトの中から簡単に os: nxos なデバイス名を抽出するということも可能です。

In [60]: Dq(testbed.raw_config).contains_key_value('os', 'nxos').get_values('devices')
Out[60]: ['dist-sw01', 'dist-sw02']

上記のアイデアは私が作成した API get_devices (testbed.yaml から特定の条件でデバイスを抽出するAPI) でも使っているので、興味のある方は GitHub 上のソースコードも見てみてください。

他にも YAML を読み込んで、中身を Dq で検索するなど、可能性は無限大です。

まとめ

今回は pyATS で用意されている Dq(Data Query) に絞って深く紹介してみました。ちょっとネットワーク自動化・・・からは遠ざかった内容になっていますが、ネットワーク自動化を行う裏側ではこういったライブラリが存在し、Python コードの中で駆使することで便利に処理を進められるということが何となくでも理解していただけるものになったなら幸いです。

実際に最近の私のコードは Python辞書 をループでなく Dq で処理してしまうというケースも多くあったりします。それぐらい DqpyATS だけでなく Python コーディングにも有用なものとなっています。

それでは Dq よって皆さんの pyATS, Python ライフが楽になりますように。

参考リンク

免責事項

本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本Webサイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本Web サイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7