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

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



    Цель этого поста – показать, что любую повторяющуюся задачу по управлению сетью можно автоматизировать, и что 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 vEOS\nHardware version:    \nSerial number:       \nSystem MAC address:  0800.27ec.005e\n\nSoftware image version: 4.20.1F\nArchitecture:           i386\nInternal build version: 4.20.1F-6820520.4201F\nInternal build ID:      790a11e8-5aaf-4be7-a11a-e61795d05b91\n\nUptime:                 1 day, 3 hours and 23 minutes\nTotal memory:           2017324 kB\nFree 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: переменные хоста


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



    Чтобы сохранить однозначность, переменная 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 на конкретных сетевых платформах.
    Red Hat
    99,00
    Программные решения с открытым исходным кодом
    Поделиться публикацией

    Комментарии 7

      +1

      У меня вопрос (я не уверен, что его надо адресовать Redhat Russia, правда) — почему ansible не даёт возможности пост-обработки register?


      У нас есть два метода выставить переменную (три, если считать include_vars): set_fact, register. При этом set_fact очень суровый (приоритетный) — он перезаписывает к чертям почти всё, и это не всегда желаемо, а ещё он загаживает лог выполнения, т.к. set_fact — отдельная таска.


      У нас есть register, но мы не можем повлиять на структуру того, что register. Там обязательно будет простыня всего output, rc, и stderr и т.д.


      А вот что бы хотелось — это возможность сделать register для переменной с результатом вычисления относительно результата.


      Я сейчас буду изобретать:


      - command: foobar
        register:
          input_name: in_foo
          name: result_foo
          expression: '{{ in_foo.stdout[3]|int }}'
      - debug: var=result_foo

      Я бы сказал, что это мелочь, но я напоминаю, что в ansible невозможно сделать так:


      var:
         foo : ''{{foo.bar}}"

      потому что это рекурсия в jinja2, и это сильно осложняет адекватное использование чужого register...

        0

        Да, я тоже иногда жалел, что нет поддержки такого функционала. Можно сделать Pull-Request :)


        Я иногда пользуюсь переменными, как макросами. Т.к. Ansible запускает шаблонизацию переменных в момент использования, то можно делать так:


        - hosts: dest
          vars:
            my_macro: "{{ result_foo.stdout[3] | int }}"
          tasks:
            - command: foobar
              register: result_foo
            - debug:
                msg: "{{ my_macro }}"
          0
          Да, так можно, но это leaky abstraction. Например, если мы сделаем delegate для роли, у которой такое написано, то можем получить, что result_foo не определён, потому что хост поменялся (хотя мы передали my_macro) как переменную в роль.

          Вторая leaky abstraction — это использование variable_end_string/variable_start_string у `template`. Некоторые особо го-шные приложения (вроде капаситора) используют {{}} в своих шаблонах, так что приходится переключаться. В этой ситуации, любая переменная, ссылающаяся на другую переменную, превращается в plain-text `{{ result_foo }}` прямо в получившемся файле.
          0
          Register выдает варьирующийся в зависимости от модуля объем данных, который доступен для дальнейшей работы (https://docs.ansible.com/ansible/latest/user_guide/playbooks_conditionals.html#register-variables).
          Сохранение в переменную выполняет именно set fact модуль — поэтому свое место в логе.
          Можно эффективно обойтись уже имеющимися средствами, в том числе не всем известным новым циклам (https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html#) и «magic variables» (https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#accessing-information-about-other-hosts-with-magic-variables), и всегда можно обратиться в сообщество Ansible.
            0

            Первый вариант — то, что сейчас, и с чем приходится жить.
            Второй вариант — set_fact очень verbose, если у меня тривиальная обработка register, то либо она где-то под капотом, (через предопределённые переменные), либо множество set_fact. Главная печаль состоит в том, что если у меня что-то пришло через register, оно отличается от ситуации, что я его сделал через -e в командной строке.

              0

              а еще, можно писать кастомные фильтры, под каждый чих)

          0

          del… (не в ветку)

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое