Автоматизация сети с помощью Ansible: модуль command

в 15:10, , рубрики: agile, Ansible, devops, open source, red hat, Блог компании Red Hat

Говоря о типовых сценариях автоматизации сети, никак не обойтись без набора модулей command. Благодаря этим модулям, Ansible позволяет запускать команды на сетевом оборудовании так, как будто вы вводите их прямо с консоли. При этом вывод команд не просто проскакивает в окне терминала, чтобы кануть в лету, а может быть сохранен и использован в дальнейшем. Его можно записать в переменные, парсить для использования в последующих задачах или же сохранить на будущее в переменных хоста.

Автоматизация сети с помощью Ansible: модуль command - 1

Цель этого поста – показать, что любую повторяющуюся задачу по управлению сетью можно автоматизировать, и что Ansible не просто позволяет управлять конфигурациями, а помогает избавиться от рутины и сэкономить время.

Разберем базовые способы использования сетевых модулей command, включая сохранение вывода команд с помощью параметра register. Также рассмотрим, как выполнять масштабирование на несколько сетевых устройств с помощью hostvars и как организовать условное выполнение с помощью параметра wait_for и еще трех связанных параметров: interval, retries и match.

Для различных сетевых платформ есть свои модули command, причем все они поддерживаются на уровне расширения Red Hat Ansible Engine Networking Add-on:

Сетевые платформы Модули *os_command
Arista EOS eos_command
Cisco IOS / IOS-XE ios_command
Cisco IOS-XR iosxr_command
Cisco NX-OS nxos_command
Juniper Junos junos_command
VyOS vyos_command

Основы работы с модулями command

Рассмотрим плейбук, который просто запускает команду show version с помощью модуля eos_command:

---
- name: COMMAND MODULE PLAYBOOK
  hosts: eos
  connection: network_cli

  tasks:
   - name: EXECUTE ARISTA EOS COMMAND
     eos_command:
       commands: show version
     register: output

   - name: PRINT OUT THE OUTPUT VARIABLE
     debug:
       var: output               

Здесь у нас две задачи и первая использует модуль eos_command с единственным параметром commands. Поскольку мы запускаем только одну команду – show version – ее можно указать в той же строке, что и сам параметр commands. Если команд две и больше, то каждую их них надо размещать на отдельной строке после commands:. В этом примере мы используем ключевое слово register, чтобы сохранить вывод команды show version. Параметр register (его можно использовать в любой задаче Ansible) задает переменную, куда будет сохранен вывод нашей задачи, чтобы им можно было воспользоваться позже. В нашем примере эта переменная называется output.

Вторая задача в нашем примере использует модуль debug, чтобы вывести на экран содержимое только что созданной переменой output. То есть, это те же данные, что вы увидели бы в интерфейсе командной строки на устройстве EOS, если бы ввели там “show version”. Отличие в том, что наш плейбук покажет их в окне терминала, на котором вы его запускаете. Как видите, модуль debug позволяет легко проверить переменные Ansible.

Вот как выглядит вывод нашего плейбука:

PLAY [eos] *************************************************************************

TASK [execute Arista eos command] **************************************************
ok: [eos]

TASK [print out the output variable] ***********************************************
ok: [eos] => {
    "output": {
        "changed": false,
        "failed": false,
        "stdout": [
            "Arista vEOSnHardware version:    nSerial number:       nSystem MAC address:  0800.27ec.005ennSoftware image version: 4.20.1FnArchitecture:           i386nInternal build version: 4.20.1F-6820520.4201FnInternal build ID:      790a11e8-5aaf-4be7-a11a-e61795d05b91nnUptime:                 1 day, 3 hours and 23 minutesnTotal memory:           2017324 kBnFree memory:            1111848 kB"
        ],
        "stdout_lines": [
            [
                "Arista vEOS",
                "Hardware version:    ",
                "Serial number:       ",
                "System MAC address:  0800.27ec.005e",
                "",
                "Software image version: 4.20.1F",
                "Architecture:           i386",
                "Internal build version: 4.20.1F-6820520.4201F",
                "Internal build ID:      790a11e8-5aaf-4be7-a11a-e61795d05b91",
                "",
                "Uptime:                 1 day, 3 hours and 23 minutes",
                "Total memory:           2017324 kB",
                "Free memory:            1111848 kB"
            ]
        ]
    }
}

PLAY RECAP *************************************************************************
eos                        : ok=2    changed=0    unreachable=0    failed=0

Как видно из скриншота, обе наши задачи отработали успешно. Поскольку в первой задаче используется уровень детализации сообщений по умолчанию, она просто говорит, что хост eos выполнил задачу с результатом ok, подчеркивая успешность выполнения зеленым цветом. Вторая задача, с модулем debug, возвращает вывод выполненной команды, отображая одну и ту же информацию в двух форматах:

  • stdout
  • stdout_lines

В секции stdout показано то же самое, что вы увидели бы в интерфейсе командной строки на устройстве, но в виде одной длинной строки. А секция stdout_lines разбивает этот вывод на строки, чтобы его было удобно читать. Каждый элемент в этом списке представляет собой отдельную строку в выводе команды.

Сравним вывод команды на устройстве и в Ansible:

Вывод команды в Arista EOS stdout_lines в Ansible
eos>show vers
Arista vEOS
Hardware version:
Serial number:
System MAC address: 0800.27ec.005e

Software image version: 4.20.1F
Architecture: i386
Internal build version: 4.20.1F-6820520.4201F
Internal build ID: 790a11e8-5aaf-4be7-a11a-e61795d05b91

Uptime: 1 day, 3 hours and 56 minutes
Total memory: 2017324 kB
Free memory: 1116624 kB

«stdout_lines»: [
[
«Arista vEOS»,
«Hardware version: »,
«Serial number: »,
«System MAC address: 0800.27ec.005e»,
"",
«Software image version: 4.20.1F»,
«Architecture: i386»,
«Internal build version:
4.20.1F-6820520.4201F»,
«Internal build ID:
790a11e8-5aaf-4be7-a11a-e61795d05b91»,
"",
«Uptime: 1 day, 3 hours and 23 minutes»,
«Total memory: 2017324 kB»,
«Free memory: 1111848 kB»
]

Если вы знакомы с JSON и YAML, то наверное уже обратили вниманием на одну странность: stdout_lines начинается с двух открывающих скобок:

"stdout_lines": [
            [

Две открывающие скобки указывают на то, что stdout_lines на самом деле возвращает перечень списков строк. Если слегка изменить нашу debug-задачу, то эту фишку можно использовать для выборочного просмотра результатов выполнения команды. Поскольку в нашем перечне есть только один список строк, этот список называется нулевым (вообще-то он первый, но отсчет идет с нуля). Теперь посмотрим, как извлечь из него отдельную строку, допустим, System MAC Address. В выводе команды эта строка идет четвертой по счету, но поскольку считаем с нуля, нам, в итоге, нужна строка 3 из списка 0, иначе говоря: output.stdout_lines[0][3].

    - name: print out a single line of the output variable
      debug:
        var: output.stdout_lines[0][3]
В ответ debug-задача возвращает именно её:

TASK [print out a single line of the output variable] ******************************
ok: [eos] => {
    "output.stdout_lines[0][3]": "System MAC address:  0800.27ec.005e"
}

Какой смысл в нумерации списков и зачем она вообще нужна? Дело в том, что в рамках одной задачи можно запускать несколько команд, например, вот так (здесь у нас три команды):

---
- hosts: eos
  connection: network_cli
  tasks:
    - name: execute Arista eos command
      eos_command:
        commands:
          - show version
          - show ip int br
          - show int status
      register: output

    - name: print out command
      debug:
        var: output.stdout_lines 

Вот как выглядит вывод:

    "output.stdout_lines": [
        [
            "Arista vEOS",
            "Hardware version:    ",
            "Serial number:       ",
            "System MAC address:  0800.27ec.005e",
            "",
            "Software image version: 4.20.1F",
            "Architecture:           i386",
            "Internal build version: 4.20.1F-6820520.4201F",
            "Internal build ID:      790a11e8-5aaf-4be7-a11a-e61795d05b91",
            "",
            "Uptime:                 1 day, 4 hours and 20 minutes",
            "Total memory:           2017324 kB",
            "Free memory:            1111104 kB"
        ],
        [
            "Interface              IP Address        Status    Protocol      MTU",
            "Ethernet1              172.16.1.1/24      up         up          1500",
            "Management1            192.168.2.10/24    up         up          1500"
        ],
        [
            "Port  Name    Status       Vlan    Duplex  Speed  Type     Flags",
            "Et1           connected  routed    full    unconf EbraTestPhyPort   ",
            "Et2           connected    1       full    unconf EbraTestPhyPort   ",
            "Et3           connected    1       full    unconf EbraTestPhyPort   ",
            "Ma1           connected  routed   a-full a-1G   10/100/1000"
        ]
    ]

Здесь список номер ноль – это вывод команды show version, список номер один – вывод show ip int br, список номер два – вывод show int status. То есть номер списка определяется порядком выполнения команд.

Команды Arista EOS Соответствующие списки вывода
show version output.stdout_lines[0]
show ip int br output.stdout_lines[1]
show int status output.stdout_lines[2]

Масштабирование модуля command: переменные хоста

А что будет, если запустить плейбук на нескольких устройствах одновременно?

Автоматизация сети с помощью Ansible: модуль command - 2

Чтобы сохранить однозначность, переменная output сохраняется как переменная хоста для каждого хоста в inventory. Если у нас есть три коммутатора, и мы прогоним на них наш плейбук, то получим переменную output для каждого уникального хоста. Допустим, нам нужен IP-адрес из команды show ip int br для порта Ethernet1 на коммутаторе switch03. Поскольку show ip int br – это вторая по счету команда, которая запускается в рамках задачи, а данные по интерфейсу Ethernet1 содержатся во второй строке ее вывода, то нам надо будет написать stdout_lines[1][1]. Чтобы обращаться к переменным конкретного хоста, мы используем ключевое слово hostvars и выполняем поиск нужного нам хоста по имени.

Вот как это делается:

    - name: debug hostvar
      debug:
        var: hostvars["switch03"].output.stdout_lines[1][1]    

В результате output содержит именно то, что нам нужно:

TASK [debug hostvar] ***************************************************************
ok: [switch03] => {
    "hostvars["switch03"].output.stdout_lines[1][1]": "Ethernet1              172.16.1.3/24      up         up              1500"
}

По умолчанию задача использует переменные текущего хоста, но hostvars позволяет напрямую обратиться и к переменным другого хоста.

Условия в задачах с модулями command: параметр wait_for

Параметр wait_for позволяет реализовать проверку условий сразу после выполнения команды. Например, сделать так, что задача будет считаться выполненной успешно, только если вывод команды проверки статуса содержит определенный текст. По умолчанию параметр wait_for не используется, поэтому задача запускается только один раз, как в примерах выше. Но если задать его в явном виде, задача будет повторно запускаться до тех пор, пока не выполнится условие либо не кончится лимит попыток (по умолчанию их 10). Если включить журналирование команд, то можно увидеть, что в приведенном ниже плейбуке (который специально написан так, чтобы условие никогда не выполнилось) все происходит именно так.

---
- hosts: eos
  connection: network_cli
  tasks:
    - name: execute Arista eos command
      eos_command:
        commands:
          - show int status
        wait_for:
          - result[0] contains DURHAM

Этот плейбук будет 10 раз запускать команду show int status, поскольку в ее выводе никогда не будет строки DURHAM.

В этом можно убедиться с помощью команды show logging:

Mar 24 20:33:52 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=17 start_time=1521923632.5 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 
Mar 24 20:33:53 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=18 start_time=1521923633.71 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 
Mar 24 20:33:54 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=19 start_time=1521923634.81 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 
Mar 24 20:33:55 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=20 start_time=1521923635.92 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 
Mar 24 20:33:56 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=21 start_time=1521923636.99 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 
Mar 24 20:33:58 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=22 start_time=1521923638.07 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 
Mar 24 20:33:59 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=23 start_time=1521923639.22 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 
Mar 24 20:34:00 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=24 start_time=1521923640.32 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 
Mar 24 20:34:01 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=25 start_time=1521923641.4 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 
Mar 24 20:34:02 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=26 start_time=1521923642.47 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 

Теперь рассмотрим пример реального плейбука, в котором все настроено для установления OSPF-соседства (adjacency) с другим устройством, кроме команды ip ospf area. Мы применим эту команду и затем воспользуемся параметром wait_for, чтобы проверить наличие в выводе слова FULL: если оно там есть, то соседство успешно установлено. Если за 10 попыток FULL так и не появится, то задача завершится с ошибкой.

---
- hosts: eos
  connection: network_cli
  tasks:
    - name: turn on OSPF for interface Ethernet1
      eos_config:
        lines:
          - ip ospf area 0.0.0.0
        parents: interface Ethernet1

    - name: execute Arista eos command
      eos_command:
        commands:
          - show ip ospf neigh
        wait_for:
          - result[0] contains FULL

Выполним этот плейбук с помощью команды ansible-playbook:

➜  ansible-playbook ospf.yml

PLAY [eos] *********************************************************************************************

TASK [turn on OSPF for interface Ethernet1] *******************************************************
changed: [eos]

TASK [execute Arista eos command] ****************************************************************
ok: [eos]

PLAY RECAP ******************************************************************************************
eos                    : ok=2    changed=1    unreachable=0    failed=0      

Смотрим командную строку и видим, что плейбук выполнен успешно:

eos#show ip ospf neigh
Neighbor ID     VRF      Pri State             Dead Time   Address         Interface
2.2.2.2         default  1   FULL/DR           00:00:33    172.16.1.2      Ethernet1

Помимо contains можно использовать следующие операторы сравнения:

  • eq: – равно
  • neq: – не равно
  • gt: – больше
  • ge: – больше или равно
  • lt: – меньше
  • le: – меньше или равно

Кроме того, вместе с wait_for можно использовать три дополнительных параметра, (подробно описывается в документации на модули):

Параметр Описание
interval Время между повторами команды.
retries Макс. количество повторов, прежде чем задача завершится с ошибкой, либо будет выполнено условие.
match Совпадение всех условия или хотя бы одного.

Остановимся чуть подробнее на параметре match:

    - name: execute Arista eos command
      eos_command:
        commands:
          - show ip ospf neigh
        match: any
        wait_for:
          - result[0] contains FULL
          - result[0] contains 172.16.1.2

Когда задано match: any, задача считается успешной, если результат содержит FULL или 172.16.1.2. Если же задано match: all, то результат должен содержать и FULL, и 172.16.1.2. По умолчанию используется match: all, поскольку если вы прописываете несколько условий, то, скорее всего, хотите, чтобы они выполнялись все, а не хотя бы одно.

Когда может пригодиться match: any? Допустим, надо проверить, что дата-центр имеет двустороннюю связь с интернетом. А дата-центр подключен к пяти разным интернет-провайдерам, для каждого из которых есть свое BGP-соединение. Плейбук может проверить все ‘эти пять соединений, и если работает хотя бы одно из них, а не все пять, сообщить, что все в порядке. Просто запомните, что any – это логическое ИЛИ, а all – логическое И.

Параметр Описание
match: any Логическое «ИЛИ»
Требуется выполнение хотя бы одного условия
match: all Логическое «И»
Требуется выполнение всех условий

Негативные условия: строим обратную логику

Иногда важно не то, что есть в выводе, а то, чего там нет. Здесь конечно всегда заманчиво использовать оператор сравнения neq, но для некоторых сценариев с негативными условиями есть варианты получше. Например, если надо инвертировать оператор contains (типа, «вывод команды не должен содержать то-то и то-то»), можно использовать ключевое слово register, чтобы сохранить вывод, и затем обработать его в следующей задаче с помощью выражения when. Или, например, когда надо остановить плейбук при невыполнении условий, просто используйте модули fail или assert, чтобы специально выйти с ошибкой. Что касается оператора сравнения neq, то он полезен лишь тогда, когда из вывода можно вытащить точное значение (например, из пары ключ-значение или из JSON), а не просто строку или список строк. Иначе будет выполняться посимвольное сравнение строк.

Что дальше

Ознакомитесь с документацией по работе с выводом команд в сетевых модулях. Там приводятся полезные примеры использования ge, le и других условий при работе с выводом в формате JSON на конкретных сетевых платформах.

Автор: redhatrussia

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js