В этом посте мы подробнее рассмотрим использование шаблонов NTC для синтаксического анализа неструктурированных данных в пригодные для использования структурированные данные. Шаблоны NTC используют TextFSM под капотом, чтобы иметь возможность анализировать данные, полученные от традиционных сетевых устройств, с помощью регулярных выражений (RegEx). Мы рассмотрим, как выглядит шаблон TextFSM, как он работает и как мы можем использовать шаблон в Ansible для выполнения утверждений топологии относительно топологии нашей лаборатории.
TextFSM Primer
TextFSM был создан Google для преобразования полуструктурированных данных с сетевых устройств в структурированные данные, к которым можно легко получить программный доступ. TextFSM — это язык, специфичный для домена (DSL), использующий под капотом RegEx для анализа данных. Это означает, что требуются некоторые знания RegEx.
Давайте взглянем на шаблон TextFSM, а затем разберем его, чтобы лучше понять, как работает TextFSM. Ниже представлен шаблон из NTC Templates, который мы будем использовать для этого поста.
Value Required NEIGHBOR (\S{0,20}) Value Required LOCAL_INTERFACE (\S+) Value CAPABILITIES (\S*) Value Required NEIGHBOR_INTERFACE (\S+) Start ^Device.*ID -> LLDP # Capture time-stamp if vty line has command time-stamping turned on ^Load\s+for\s+ ^Time\s+source\s+is LLDP ^${NEIGHBOR}\s*${LOCAL_INTERFACE}\s+\d+\s+${CAPABILITIES}\s+${NEIGHBOR_INTERFACE} -> Record ^${NEIGHBOR} ^\s+${LOCAL_INTERFACE}\s+\d+\s+${CAPABILITIES}\s+${NEIGHBOR_INTERFACE} -> Record
Мы рассмотрим этот шаблон более подробно ниже, но я хочу показать вам, как выглядят необработанные данные и как они выглядят после того, как они были проанализированы с помощью TextFSM.
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 S2 Fa0/13 120 B Gi0/13 Cisco-switch-1 Gi1/0/7 120 Gi0/1 Juniper-switch1 Gi2/0/1 120 B,R 666 Juniper-switch1 Gi1/0/1 120 B,R 531 Total entries displayed: 4
Вот результат, возвращенный после анализа полуструктурированных данных с использованием шаблонов NTC.
--- parsed_sample: - capabilities: "B" local_interface: "Fa0/13" neighbor: "S2" neighbor_interface: "Gi0/13" - capabilities: "" local_interface: "Gi1/0/7" neighbor: "Cisco-switch-1" neighbor_interface: "Gi0/1" - capabilities: "B,R" local_interface: "Gi2/0/1" neighbor: "Juniper-switch1" neighbor_interface: "666" - capabilities: "B,R" local_interface: "Gi1/0/1" neighbor: "Juniper-switch1" neighbor_interface: "531"
Значения
Как видно из полученных нами выходных данных, Value
в шаблоне используется как ключ (столбец) в каждом возвращаемом словаре (строке).
Есть несколько ключевых слов, которые могут изменить способ обработки значения, например следующие:
- Обязательно: запись (строка) сохраняется в таблице только в том случае, если это значение совпадает.
- Заполнение: ранее сопоставленное значение сохраняется для последующих записей (если оно явно не очищено или не сопоставлено снова). Другими словами, последнее совпавшее значение копируется в новые строки, если оно не будет найдено снова.
- Список: значение представляет собой список, добавляемый к каждому совпадению. Обычно совпадение перезаписывает любое предыдущее значение в этой строке.
- Ключ: объявляет, что содержимое поля способствует уникальному идентификатору строки. Это можно использовать для связывания данных из нескольких шаблонов в единую структуру.
- Заполнение: аналогично заполнению, но заполняется вверх, пока не будет найдена непустая запись. Несовместимо с Required.
В конце строки мы укажем RegEx, которое будет соответствовать нашему полуструктурированному тексту для этого конкретного значения. В зависимости от данных, это может быть как общий \S+
, если данные хорошо известны, так и сложный, если необходимо.
\S+
соответствует любому непробельному символу, который требует, чтобы данные были либо контролируемыми, либо хорошо известными, как указано выше.
Напомним, что мы только что обсуждали, здесь пробой Значение строки:.Value {KEYWORD} {VALUE_NAME} (RegEx)
Состояния
В государственных определениях заимствованы после Значения определений и отделяются от значений пустой строки. Строки с отступом после каждого состояния — это правила состояния, которые указаны в соответствии с определениями значений, указанными в начале шаблона. Состояния помогают разбить ваш шаблон на более легкие для чтения фрагменты, если полуструктурированные данные сложны. Верхнего предела количества состояний в шаблоне TextFSM нет, но всегда требуется Start.
Государственные правила
Правила определяют строки, которые мы хотим захватить, с определениями значений в начале шаблона. Каждая линия правила должна начинаться с символа карата ( ^
). Правила необязательно должны заканчиваться ->
действием правила, но оно может потребоваться в зависимости от данных. ->
Обозначает действие правила и говорит TextFSM, что делать с данными, полученными до этого момента. Вскоре мы обсудим действия правила. Значения обозначаются ${VALUE_NAME}
в правилах состояния, которые будут расширены с помощью RegEx из определения значения.
Имейте в виду, что вся строка необязательно должна быть RegEx или values ( ${NEIGHBOR}
), но также может быть обычным текстом для сопоставления. Незаметно TextFSM преобразует каждое правило в полную строку RegEx. Если мы посмотрим на первую строку под LLDP
состоянием, за кадром она будет выглядеть следующим образом:^(\S{0,20})\s*(\S+)\s+\d+\s+(\S*)\s+(\S+)
Правило состояния необязательно должно соответствовать всей строке, которую мы видим в нашем шаблоне с расширением `. ^ Загрузить \ s + для \ s + , which will match any line that starts with
Загрузить для `.
Правило и линейные действия
Действия правила могут применяться для каждой строки правила состояния, но они влияют на поведение, и размещение в пределах состояний должно быть тщательно продумано. Есть строковые действия, которые сообщают TextFSM, что нужно делать с текущей строкой во время ее обработки, а затем действия правил сообщают TextFSM, что делать с захваченными значениями. Согласно действию по умолчанию, любая строка, не содержащая символа ->
, равна Next.NoRecord
. Чтобы лучше понять это, давайте рассмотрим, какие варианты у нас есть, когда дело доходит до использования линии и действий правил, указав ->
.
Действия линии
- Далее (по умолчанию) : закончить строку ввода, прочитать следующую строку и снова начать сопоставление с начала состояния. Это поведение по умолчанию, если действие строки не указано.
- Продолжить: сохранить текущую строку и не возобновлять сопоставление с первого правила состояния. Продолжить обработку правил, как если бы совпадение не произошло (присвоение значений все еще происходит).
Использование действия «Продолжить строку» не является распространенным вариантом использования при построении шаблона, показывает вариант использования, когда вы хотите захватить несколько значений, находящихся в одной строке.
Вот пример шаблона:
Value List INTERFACES ([\w\./]+) ..omitted for brevity VLANS ^\d+ -> Continue.Record ^${VLAN_ID}\s+${NAME}\s+${STATUS}\s*$$ ^${VLAN_ID}\s+${NAME}\s+${STATUS}\s+${INTERFACES},* -> Continue ^\d+\s+(?:\S+\s+){3}${INTERFACES},* -> Continue ^\d+\s+(?:\S+\s+){4}${INTERFACES},* -> Continue ^\d+\s+(?:\S+\s+){5}${INTERFACES},* -> Continue ^\d+\s+(?:\S+\s+){6}${INTERFACES},* -> Continue ^\d+\s+(?:\S+\s+){7}${INTERFACES},* -> Continue
Вот пример частично структурированных данных, которые будут проанализированы:
50 VLan50 active Fa0/1, Fa0/2, Fa0/3, Fa0/4, Fa0/5, Fa0/6, Fa0/7, Fa0/8, Fa0/9 Fa0/10, Fa0/11, Fa0/12
Используя Continue для каждой строки, мы можем сохранить захваченное значение, а также строку, которую оно обрабатывает в данный момент, а затем перейти к следующему правилу состояния в State, чтобы захватить дополнительные значения в строке.
Это означает, что наши структурированные данные будут выглядеть следующим образом:
---
parsed_sample:
- vlan_id: "50"
name: "VLan50"
status: "active"
interfaces:
- "Fa0/1"
- "Fa0/2"
- "Fa0/3"
- "Fa0/4"
- "Fa0/5"
- "Fa0/6"
- "Fa0/7"
- "Fa0/8"
- "Fa0/9"
- "Fa0/10"
- "Fa0/11"
- "Fa0/12"
Правило Действия
- NoRecord (по умолчанию) : ничего не делать. Это поведение по умолчанию, если действие записи не указано.
- Запись: Запишите собранные значения до строки в возвращаемых данных. Значения без заполнения очищаются. Примечание. Никакая запись не выводится, если есть неназначенные «Обязательные» значения.
- Очистить: очистить значения без заполнения.
- Clearall : очистить все значения.
- Состояние: переход в другое состояние.
- Ошибка: это встроенное состояние, которое отбрасывает все захваченные значения и возвращает исключение.
Мы используем действие правила ошибки, чтобы помочь устранить неполадки в наших шаблонах и убедиться, что в наших шаблонах учитываются правильные данные. Вот как мы его используем: `^. -> Ошибка`, которая выдаст исключение для строки, не соответствующей какому-либо определенному правилу состояния.
`-> Continue.State` не может предотвращать зацикливание в TextFSM.
Если мы посмотрим на шаблон под LLDP
состоянием, мы увидим в нем две -> Record
опции. Это позволяет нам захватывать соответствующие значения, но путем синтаксического анализа немного отличающегося вывода.
Мы также можем комбинировать линейное действие с действием правила. Синтаксис для этого LineAction.RuleAction
.
Надеюсь, этот обзор TextFSM дает более четкую картину при расшифровке шаблона или когда вы начинаете создавать свой собственный. Давайте перейдем к краткому обзору топологии, а затем сразу перейдем к нашей инструкции по проверке.
Топология
Ниже приведено изображение топологии, которую мы будем использовать для проверки соседей LLDP в нашей лабораторной топологии. Это простая топология с тремя маршрутизаторами Cisco IOS, которые соединены вместе и имеют включенный LLDP.
Установка Ansible
К счастью, наша топология и последующая инвентаризация будут простыми. У нас есть маршрутизаторы в группе под названием, ios
которая затем имеет соответствующий ios.yml
файл в group_vars
папке, в которой есть переменные, относящиеся к подключению к этим устройствам, которые я вскоре покажу. Затем у нас есть {hostname}.yml
файлы для каждого маршрутизатора, содержащие approved_neighbors
переменную, которую мы будем использовать для проверки соседей, которые мы видим из наших проанализированных LLDP
данных. Ниже представлено дерево нашего каталога, которое содержит сборник пьес и инвентарь Ansible.
❯ tree
.
├── ansible.cfg
├── group_vars
│ └── ios.yml
├── host_vars
│ ├── iosv-0.yml
│ ├── iosv-1.yml
│ └── iosv-2.yml
├── inventory
└── pb.validate.neighbors.yml
2 directories, 7 files
Вот inventory
файл, который связывает наши маршрутизаторы с ios
группой.
[ios]
iosv-0 ansible_host=10.188.1.56
iosv-1 ansible_host=10.188.1.54
iosv-2 ansible_host=10.188.1.55
Вот содержимое ios.yml
файла. Это указывает пользователя, которого мы хотим подключиться к маршрутизаторам, а также ansible_network_os
чтобы сообщить Ansible, к какому типу сетевого устройства мы будем подключаться.
Если вы заметили, это отличается от синтаксиса Ansible <= 2.9 и использует путь к, cisco.ios.ios
а не просто ios
. Это связано с тем, что мы запускаем Ansible 2.10 и переходим на использование синтаксиса, который Ansible будет применять в будущем. Вы также заметите некоторые различия с использованием Ansible 2.10 в нашей инструкции.
---
ansible_user: "cisco"
ansible_network_os: "cisco.ios.ios"
Вот взгляд на host var, который мы определили.
---
approved_neighbors:
- local_intf: "Gi0/0"
neighbor: "iosv-1"
- local_intf: "Gi0/1"
Теперь посмотрим на 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.ntc_templates
set_fact: "lldp_neighbors"
- 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 }}"
Плейбук начинается с определения наших хостов как ios
группы в нашем inventory
файле, которая состоит из наших трех маршрутизаторов IOS. В методе подключения используется синтаксис> = Ansible 2.10, network_cli
и мы отключили сбор фактов.
Давайте разберем две задачи, которые мы используем, чтобы подтвердить, что наши рабочие данные для соседей LLDP соответствуют определенной approved_neighbors
переменной, которая у нас есть для каждого хоста.
Первая задача использует ansible.netcommon.cli_parse
модуль для запуска команды на устройстве, а затем анализирует данные с помощью нашего определенного ansible.netcommon.ntc_templates
синтаксического анализатора. Этот вывод сохраняется в lldp_neighbors
соответствии с set_fact
директивой задачи.
Если вы хотите запустить тот же сценарий, убедитесь, что вы ntc-templates
установили через pip install ntc-templates
.
Следующий цикл задачи будут над нашим approved_neighbors
переменным , а затем попытаться найти соответствие в наших проанализированных данных путем поиска записи , которая имеет значение для ключа , local_interface
который соответствует тому , что мы установили для local_intf
в approved_neighbors
и что neighbor
ключ также соответствует нашей neighbor
ценности. Наша playbook потерпит неудачу, если какой-либо из соседей не соответствует тому, что мы определили approved_neighbors
.
То, как развивается Ansible, и методологии, которые мы использовали в playbook, не ограничивают потенциал playbook одними только ios
и мы фактически можем поменять ios
определение хостов для all
любого количества групп и хостов, которые являются мультивендерами. Это связано с закулисной магией, которая ansible.netcommon.cli_parse
выполняется с ansible_network_os
переменной, которую мы установили в group vars. Он использует эту переменную, чтобы определить, какой nos_command
модуль запустить для подключения к устройству и какой шаблон использовать для анализа возвращенных данных.
Выходные данные Playbook
Давайте продолжим, запустим playbook и посмотрим, какой результат мы получим.
❯ ansible-playbook -i inventory pb.validate.neighbors.yml -vv -k
ansible-playbook 2.10.2
config file = /Users/myohman/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/main-3.8/lib/python3.8/site-packages/ansible
executable location = /Users/myohman/.virtualenvs/main-3.8/bin/ansible-playbook
python version = 3.8.6 (default, Oct 16 2020, 21:27:09) [Clang 12.0.0 (clang-1200.0.32.2)]
Using /Users/myohman/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
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/local-dev/blog-posts/pb.validate.neighbors.yml:10
ok: [iosv-0] => changed=false
ansible_facts:
lldp_neighbors:
- capabilities: R
local_interface: Gi0/1
neighbor: iosv-2
neighbor_interface: Gi0/0
- capabilities: R
local_interface: Gi0/0
neighbor: iosv-1
neighbor_interface: Gi0/0
parsed:
- capabilities: R
local_interface: Gi0/1
neighbor: iosv-2
neighbor_interface: Gi0/0
- capabilities: R
local_interface: Gi0/0
neighbor: iosv-1
neighbor_interface: Gi0/0
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:
- capabilities: R
local_interface: Gi0/1
neighbor: iosv-2
neighbor_interface: Gi0/1
- capabilities: R
local_interface: Gi0/0
neighbor: iosv-0
neighbor_interface: Gi0/0
parsed:
- capabilities: R
local_interface: Gi0/1
neighbor: iosv-2
neighbor_interface: Gi0/1
- capabilities: R
local_interface: Gi0/0
neighbor: iosv-0
neighbor_interface: Gi0/0
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>
ok: [iosv-2] => changed=false
ansible_facts:
lldp_neighbors:
- capabilities: R
local_interface: Gi0/1
neighbor: iosv-1
neighbor_interface: Gi0/1
- capabilities: R
local_interface: Gi0/0
neighbor: iosv-0
neighbor_interface: Gi0/1
parsed:
- capabilities: R
local_interface: Gi0/1
neighbor: iosv-1
neighbor_interface: Gi0/1
- capabilities: R
local_interface: Gi0/0
neighbor: iosv-0
neighbor_interface: Gi0/1
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>
TASK [Assert the correct neighbors are seen] **************************************************************************
task path: /Users/myohman/local-dev/blog-posts/pb.validate.neighbors.yml:17
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-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/0', 'neighbor': 'iosv-0'}) => changed=false
ansible_loop_var: item
item:
local_intf: Gi0/0
neighbor: iosv-0
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=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
iosv-1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
iosv-2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Если мы более внимательно посмотрим на вывод первой задачи, мы увидим под parsed
ключом, а также установив fact ( lldp_neighbors
), что у нас есть структурированные данные, через которые проходит необработанный вывод NTC Templates
.
Вторая задача показывает цикл для каждого хоста и то, item
что он использует во время цикла. Если вы посмотрите на нашу книгу, мы используем local_intf
и neighbor
для наших утверждений из нашей approved_neighbors
переменной.
Резюме
Надеюсь, вам понравился этот пост в блоге, и вы немного больше понимаете о TextFSM, шаблонах NTC и о том, насколько легко их использовать с Ansible. Простота использования не является уникальной для Ansible, поскольку этого также можно легко достичь с помощью Netmiko или необработанного Python, но с использованием Ansible из-за промышленного принятия Ansible.