Давайте продолжим и погрузимся глубже в то, как выглядит парсер show lldp neighbors, как он работает и как нам нужно изменить существующий playbook, чтобы использовать парсеры Genie.
Genie Primer
Количество парсеров резко увеличилось за последние несколько месяцев и начинает включать больше поставщиков, что приятно видеть.
Genie использует классы Python для создания двух важных функций синтаксического анализа:
- Класс схемы: этот класс определяет схему, которой должен придерживаться структурированный вывод.
- Класс синтаксического анализатора: этот класс определяет фактические методы синтаксического анализа для конкретной команды.
Одно из ключевых различий между тем, что мы рассмотрели до сих пор, и синтаксическими анализаторами Genie — это возможность подключаться к устройствам и получать необходимый вывод, что является поведением по умолчанию, но также позволяет пользователям предоставлять вывод вместо этого, таким образом работая как большинство синтаксических анализаторов, которые отдельно от взаимодействия с устройством.
Еще одно ключевое отличие — это возможность использовать другие стратегии синтаксического анализа в Genie, такие, как TextFSM или Template Text Parser (TTP), но в этом посте мы рассмотрим RegEx.
Давайте углубимся в наш конкретный парсер.
"""show_lldp.py supported commands: * show lldp * show lldp entry * * show lldp entry [<WORD>] * show lldp interface [<WORD>] * show lldp neighbors * show lldp neighbors detail * show lldp traffic """ import re from genie.metaparser import MetaParser from genie.metaparser.util.schemaengine import Schema, \ Any, \ Optional, \ Or, \ And, \ Default, \ Use # import parser utils from genie.libs.parser.utils.common import Common
Мы видим, что этот парсер объявлен в show_lldp.py
модуле и поддерживает несколько вариантов команды show lldp . Затем он импортирует re
, который является встроенной библиотекой RegEx. Следующий импорт — MetaParser
это проверка того, что выходные данные парсеров с0щоответствуют определенной схеме. После MetaParser
импорта происходит связанный со схемой импорт, который помогает построить реальную схему, которую мы вскоре увидим. После этого импортируется класс Common, который предоставляет вспомогательные функции.
Класс схемы дает нам представление о том, как будет выглядеть результат. Давайте посмотрим на первоначальное определение.
class ShowLldpNeighborsSchema(MetaParser): """ Schema for show lldp neighbors """ schema = { 'total_entries': int, 'interfaces': { Any(): { 'port_id': { Any(): { 'neighbors': { Any(): { 'hold_time': int, Optional('capabilities'): list, } } } } } } }
Мы видим, что ShowLldpNeighborsSchema
это будет подкласс MetaParser
класса, импортированного в начале файла. Внутри ShowLldpNeighborsSchema
класса мы определяем наш schema
атрибут, используемый, чтобы убедиться, что наш вывод соответствует схеме, прежде чем возвращать его пользователю.
Схема является словарем и ожидает total_entries
ключ с целочисленным значением и interfaces
ключом, используемым для определения словаря. Каждый интерфейс будет ключом в interfaces
словаре, а данные, полученные из выходных данных, определены в нескольких других вложенных словарях. Каждая пара «ключ-значение» определяет ключ и type
значение, которым он должен быть. Также существуют Optional
ключи, которые не требуются для проверки схемы.
Теперь, когда мы видим схему, мы можем смоделировать, каким будет наш потенциальный результат.
{ "total_entries": 1, "interfaces": { "GigabitEthernet1": { "port_id": { "Gi1": { "neighbors": { "iosv-0": { "capabilities": [ "R" ], "hold_time": 120 } } } } } } }
Перейдем к определенному классу парсера.
class ShowLldpNeighbors(ShowLldpNeighborsSchema): """ Parser for show lldp neighbors """ CAPABILITY_CODES = {'R': 'router', 'B': 'mac_bridge', 'T': 'telephone', 'C': 'docsis_cable_device', 'W': 'wlan_access_point', 'P': 'repeater', 'S': 'station_only', 'O': 'other'} cli_command = ['show lldp neighbors']
Мы видим, что ShowLldpNeighbors
класс наследует ShowLldpNeighborsSchema
класс, который мы только что рассмотрели. Теперь есть сопоставление для коротких кодов возможностей формы, возвращаемых в выходных данных, когда соседи существуют, и для длинной формы, которую синтаксический анализатор хочет вернуть пользователю.
Следующая определенная переменная — это cli_command
. Он определяет команды, выполняемые синтаксическим анализатором, если выходные данные не предоставляются.
Давайте посмотрим на cli
метод, чтобы увидеть, что будет выполнено, когда пользователь укажет cli
синтаксический анализатор для show lldp neighbors
команды.
Каждый тип вывода будет указан как метод в классе парсера. Например, если устройство возвращает xml, будет
xml
метод, который будет анализировать и возвращать структурированные данные, которые придерживаются той же схемы, что иcli
выходные данные.
Давайте рассмотрим код по частям, чтобы показать, что происходит.
def cli(self, output=None): if output is None: cmd = self.cli_command[0] out = self.device.execute(cmd) else: out = output parsed_output = {}
Мы видим, что cli
метод принимает необязательный аргумент с именем output
, но по умолчанию None
. Первая логика определяет, предоставил ли пользователь команду output
или нужно ли синтаксическому анализатору выполнить команду в отношении устройства. Используемое парсером соединение обеспечивается библиотекой PyATS. Это означает, что никакой другой библиотеки, такой как netmiko
или napalm
для подключения к устройствам, не требуется.
# Total entries displayed: 4 p1 = re.compile(r'^Total\s+entries\s+displayed:\s+(?P<entry>\d+)$') # Device ID Local Intf Hold-time Capability Port ID # router Gi1/0/52 117 R Gi0/0/0 # 10.10.191.107 Gi1/0/14 155 B,T 7038.eeff.572d # d89e.f3ff.58fe Gi1/0/33 3070 d89e.f3ff.58fe p2 = re.compile(r'(?P<device_id>\S+)\s+(?P<interfaces>\S+)' r'\s+(?P<hold_time>\d+)\s+(?P<capabilities>[A-Z,]+)?' r'\s+(?P<port_id>\S+)')
parsed_output
Следующим шагом после создания переменной является определение выражений RegEx, используемых для поиска ценных данных в выходных данных устройства. Поскольку выходные данные представлены в виде таблицы, что означает, что они определены как таблица, все значения, которые нам нужны, будут в одной строке (строке) для каждого соседа.
Анализатор использует,
re.compile
чтобы заранее указать выражение RegEx для последующего использования в коде. Обычноre.compile
используется, когда одно и то же выражение используется несколько раз.
p1
предоставит total_entries
в нашей схеме, используя возможность именованных групп захвата в re
библиотеке. К счастью, Cisco предоставляет отличную документацию в коде, чтобы рассказать вам, что ожидает захватить каждое регулярное выражение. p2
определяет регулярное выражение, используемое для сбора информации о соседях. Мы видим, что он использует в основном те, \S+
которые захватывают любые непробельные символы, поскольку вывод является прямым. Но мы видим возможности названой группы захвата немного сложнее. Ожидается хотя бы одна заглавная буква или запятая, а затем ноль или одно из этого выражения RegEx. Это можно лучше объяснить на их примере, если мы посмотрим на столбец возможностей, он показывает, что он может фиксировать одну возможность, две возможности с запятой или нулевые возможности.
Теперь давайте посмотрим на оставшийся код, чтобы увидеть, как он использует эти выражения RegEx.
for line in out.splitlines(): line = line.strip() # Total entries displayed: 4 m = p1.match(line) if m: parsed_output['total_entries'] = int(m.groupdict()['entry']) continue # Device ID Local Intf Hold-time Capability Port ID # router Gi1/0/52 117 R Gi0/0/0 # 10.10.191.107 Gi1/0/14 155 B,T 7038.eeff.572d # d89e.f3ff.58fe Gi1/0/33 3070 d89e.f3ff.58fe m = p2.match(line) if m: group = m.groupdict() intf = Common.convert_intf_name(group['interfaces']) device_dict = parsed_output.setdefault('interfaces', {}). \ setdefault(intf, {}). \ setdefault('port_id', {}). \ setdefault(group['port_id'], {}).\ setdefault('neighbors', {}). \ setdefault(group['device_id'], {}) device_dict['hold_time'] = int(group['hold_time']) if group['capabilities']: capabilities = list(map(lambda x: x.strip(), group['capabilities'].split(','))) device_dict['capabilities'] = capabilities continue return parsed_output
Мы видим, что синтаксический анализатор выполняет for
цикл по выходным splitlines
данным, используя метод для предоставления списка каждой строки в выходных данных. Он удалит любые пробелы с обеих сторон строки.
Синтаксический анализатор попытается сопоставить p1
скомпилированный RegEx и, если он его захватит, добавит total_entries
в parsed_output
словарь, а затем continue
в следующую строку вывода.
Если синтаксический анализатор ничего не захватил p1
, он попытается сопоставить p2
. Если совпадение произойдет, groupdict()
метод вернет все названные группы захвата и их значения в виде словаря.
Теперь мы видим, что парсер использует convert_intf_name
метод из Common
класса, импортированного в верхней части файла.
После преобразования имени интерфейса синтаксический анализатор добавляет interfaces
словарь к parsed_output
переменной, извлекая захваченную информацию или по умолчанию в пустой словарь для любых не захваченных данных, а затем присваивая ее device_dict
переменной.
После того, device_dict
как указан, он добавляет к нему время удержания.
Следующим шагом является разделение возможностей на список и добавление к device_dict
переменной. Затем синтаксический анализатор перейдет к следующей строке вывода.
Как только все строки будут проанализированы, он вернет parsed_output
пользователю, если он пройдет проверку схемы.
Я считаю, что это легче понять, чем что-то вроде TextFSM, поскольку он написан на Python, а Python — популярный язык среди инженеров по автоматизации сети.
Давайте продолжим и еще раз рассмотрим топологию.
Топология .. Опять же
Ниже приведено изображение лабораторной топологии, которую мы используем для проверки соседей LLDP . Это простая топология с тремя маршрутизаторами Cisco IOS, соединенными вместе и с включенным протоколом LLDP.
Ansible Setup .. Опять же
Мы уже рассмотрели большую часть настройки Ansible, но мы объясним небольшие изменения, которые мы должны внести, чтобы использовать парсеры Genie в Ansible.
Вот взгляд на host var, который мы определили как освежитель, поскольку здесь нет никаких изменений.
--- approved_neighbors: - local_intf: "Gi0/0" neighbor: "iosv-1" - local_intf: "Gi0/1" neighbor: "iosv-2"
Теперь посмотрим на изменения в pb.validate.neighbors.yml
.
--- - hosts: "ios" connection: "ansible.netcommon.network_cli" gather_facts: "no" tasks: - name: "PARSE LLDP INFO INTO STRUCTURED DATA" ansible.netcommon.cli_parse: command: "show lldp neighbors" parser: name: ansible.netcommon.pyats set_fact: "lldp_neighbors" - name: "MANIPULATE THE DATA TO BE IN THE SAME FORMAT AS TEXTFSM TO PREVENT CHANGING FINAL ASSERTION TASK" set_fact: lldp_neighbors: "{{ lldp_neighbors | convert_data }}" - name: "ASSERT THE CORRECT NEIGHBORS ARE SEEN" assert: that: - "lldp_neighbors | selectattr('local_interface', 'equalto', item['local_intf']) | map(attribute='neighbor') | first == item['neighbor']" loop: "{{ approved_neighbors }}"
Используя
ansible.netcommon.pyats
анализатор требуетgenie
иpyats
должен быть установлен с помощьюpip install genie pyats
.
Есть несколько вещей, которые нужно проанализировать с помощью учебника. Сначала мы изменили парсер на ansible.netcommon.pyats
. Во-вторых, мы добавили еще одну задачу для управления данными, которые мы получаем от парсера, в формате, аналогичном второму сообщению в блоге в этой серии, поэтому нам не нужно изменять последнюю задачу. Я выполнил преобразование с помощью настраиваемого плагина фильтра из-за структуры данных и простоты обработки в Python. Вы увидите результат ниже, как только мы запустим нашу книгу.
❯ ansible-playbook pb.validate.neighbors.yml -k -vv ansible-playbook 2.10.3 config file = /Users/myohman/Documents/local-dev/blog-posts/ansible.cfg configured module search path = ['/Users/myohman/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] ansible python module location = /Users/myohman/.virtualenvs/3.8/main/lib/python3.8/site-packages/ansible executable location = /Users/myohman/.virtualenvs/3.8/main/bin/ansible-playbook python version = 3.8.6 (default, Nov 17 2020, 18:43:06) [Clang 12.0.0 (clang-1200.0.32.27)] Using /Users/myohman/Documents/local-dev/blog-posts/ansible.cfg as config file SSH password: redirecting (type: callback) ansible.builtin.yaml to community.general.yaml redirecting (type: callback) ansible.builtin.yaml to community.general.yaml Skipping callback 'default', as we already have a stdout callback. Skipping callback 'minimal', as we already have a stdout callback. Skipping callback 'oneline', as we already have a stdout callback. PLAYBOOK: pb.validate.neighbors.yml *********************************************************************** 1 plays in pb.validate.neighbors.yml PLAY [ios] ************************************************************************************************ META: ran handlers TASK [Parse LLDP info into structured data] *************************************************************** task path: /Users/myohman/Documents/local-dev/blog-posts/pb.validate.neighbors.yml:9 ok: [iosv-2] => changed=false ansible_facts: lldp_neighbors: interfaces: GigabitEthernet0/0: port_id: Gi0/1: neighbors: iosv-0: capabilities: - R hold_time: 120 GigabitEthernet0/1: port_id: Gi0/1: neighbors: iosv-1: capabilities: - R hold_time: 120 total_entries: 2 parsed: interfaces: GigabitEthernet0/0: port_id: Gi0/1: neighbors: iosv-0: capabilities: - R hold_time: 120 GigabitEthernet0/1: port_id: Gi0/1: neighbors: iosv-1: capabilities: - R hold_time: 120 total_entries: 2 stdout: |- Capability codes: (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other Device ID Local Intf Hold-time Capability Port ID iosv-1 Gi0/1 120 R Gi0/1 iosv-0 Gi0/0 120 R Gi0/1 Total entries displayed: 2 stdout_lines: <omitted> ok: [iosv-0] => changed=false ansible_facts: lldp_neighbors: interfaces: GigabitEthernet0/0: port_id: Gi0/0: neighbors: iosv-1: capabilities: - R hold_time: 120 GigabitEthernet0/1: port_id: Gi0/0: neighbors: iosv-2: capabilities: - R hold_time: 120 total_entries: 2 parsed: interfaces: GigabitEthernet0/0: port_id: Gi0/0: neighbors: iosv-1: capabilities: - R hold_time: 120 GigabitEthernet0/1: port_id: Gi0/0: neighbors: iosv-2: capabilities: - R hold_time: 120 total_entries: 2 stdout: |- Capability codes: (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other Device ID Local Intf Hold-time Capability Port ID iosv-2 Gi0/1 120 R Gi0/0 iosv-1 Gi0/0 120 R Gi0/0 Total entries displayed: 2 stdout_lines: <omitted> ok: [iosv-1] => changed=false ansible_facts: lldp_neighbors: interfaces: GigabitEthernet0/0: port_id: Gi0/0: neighbors: iosv-0: capabilities: - R hold_time: 120 GigabitEthernet0/1: port_id: Gi0/1: neighbors: iosv-2: capabilities: - R hold_time: 120 total_entries: 2 parsed: interfaces: GigabitEthernet0/0: port_id: Gi0/0: neighbors: iosv-0: capabilities: - R hold_time: 120 GigabitEthernet0/1: port_id: Gi0/1: neighbors: iosv-2: capabilities: - R hold_time: 120 total_entries: 2 stdout: |- Capability codes: (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other Device ID Local Intf Hold-time Capability Port ID iosv-2 Gi0/1 120 R Gi0/1 iosv-0 Gi0/0 120 R Gi0/0 Total entries displayed: 2 stdout_lines: <omitted> TASK [MANIPULATE THE DATA TO BE IN STANDARD FORMAT] ******************************************************* task path: /Users/myohman/Documents/local-dev/blog-posts/pb.validate.neighbors.yml:16 ok: [iosv-0] => changed=false ansible_facts: lldp_neighbors: - local_interface: Gi0/1 neighbor: iosv-2 - local_interface: Gi0/0 neighbor: iosv-1 ok: [iosv-1] => changed=false ansible_facts: lldp_neighbors: - local_interface: Gi0/1 neighbor: iosv-2 - local_interface: Gi0/0 neighbor: iosv-0 ok: [iosv-2] => changed=false ansible_facts: lldp_neighbors: - local_interface: Gi0/1 neighbor: iosv-1 - local_interface: Gi0/0 neighbor: iosv-0 TASK [Assert the correct neighbors are seen] ************************************************************** task path: /Users/myohman/Documents/local-dev/blog-posts/pb.validate.neighbors.yml:20 ok: [iosv-0] => (item={'local_intf': 'Gi0/0', 'neighbor': 'iosv-1'}) => changed=false ansible_loop_var: item item: local_intf: Gi0/0 neighbor: iosv-1 msg: All assertions passed ok: [iosv-2] => (item={'local_intf': 'Gi0/0', 'neighbor': 'iosv-0'}) => changed=false ansible_loop_var: item item: local_intf: Gi0/0 neighbor: iosv-0 msg: All assertions passed ok: [iosv-1] => (item={'local_intf': 'Gi0/0', 'neighbor': 'iosv-0'}) => changed=false ansible_loop_var: item item: local_intf: Gi0/0 neighbor: iosv-0 msg: All assertions passed ok: [iosv-0] => (item={'local_intf': 'Gi0/1', 'neighbor': 'iosv-2'}) => changed=false ansible_loop_var: item item: local_intf: Gi0/1 neighbor: iosv-2 msg: All assertions passed ok: [iosv-1] => (item={'local_intf': 'Gi0/1', 'neighbor': 'iosv-2'}) => changed=false ansible_loop_var: item item: local_intf: Gi0/1 neighbor: iosv-2 msg: All assertions passed ok: [iosv-2] => (item={'local_intf': 'Gi0/1', 'neighbor': 'iosv-1'}) => changed=false ansible_loop_var: item item: local_intf: Gi0/1 neighbor: iosv-1 msg: All assertions passed META: ran handlers META: ran handlers PLAY RECAP ************************************************************************************************ iosv-0 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 iosv-1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 iosv-2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Я запустил эту книгу с некоторой подробностью, чтобы показать, что возвращает каждая задача, и формат наших проанализированных данных.
Если мы внимательно посмотрим на вывод первой задачи, мы увидим под parsed
ключом, а также установив fact ( lldp_neighbors
), что у нас есть структурированные данные от запуска сырого вывода через Genie.
Вторая задача показывает цикл для каждого хоста и item
его использование во время цикла. Если вы посмотрите на нашу книгу, мы используем local_intf
и neighbor
для наших утверждений из нашей approved_neighbors
переменной.
Резюме
Мы преобразовали данные с помощью настраиваемого плагина фильтра, но мы могли бы легко скорректировать факты и окончательные утверждения, чтобы они соответствовали выходным данным, полученным от Genie. Также важно показать возможность использования единой книги воспроизведения с использованием любого синтаксического анализатора для выполнения рабочих утверждений. Если бы мы были в производстве, мы могли бы заставить convert_data
настраиваемый плагин фильтра переводить несколько различных форматов парсера в формат, независимый от парсера.
Для краткости, вот наш tree
результат, чтобы показать вам, как выглядит структура папок при использовании настраиваемого плагина фильтра.
❯ tree . ├── ansible.cfg ├── filter_plugins │ └── custom_filters.py ├── group_vars │ ├── all │ │ └── all.yml │ └── ios.yml ├── host_vars │ ├── iosv-0.yml │ ├── iosv-1.yml │ └── iosv-2.yml ├── inventory └── pb.validate.neighbors.yml
Наконец, содержимое файла custom_filters.py
.
# -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function __metaclass__ = type import re def convert_genie_data(data): intfs = [] for k,v in data['interfaces'].items(): intf_name = "Gi" + re.search(r'\d/\d', k).group(0) intf_dict = {} intf_dict['local_interface'] = intf_name neighbor_intf = list(v['port_id'].keys())[0] intf_dict['neighbor'] = list(v['port_id'][neighbor_intf]['neighbors'].keys())[0] intfs.append(intf_dict) return intfs class FilterModule: def filters(self): filters = { 'convert_data': convert_genie_data, } return filters
Надеюсь, вам понравился этот пост в блоге, и вы немного больше понимаете о парсерах Genie и о том, как использовать их с помощью Ansible.