Глава 5. Инфраструктура автоматизации Python - дополнительные вопросы Ansible

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

Важно указать, что если вы недостаточно чётко уяснили какие- то моменты из предыдущей главы, сейчас самое время вернуться обратно и пересмотреть их, так как они требуются для данной главы.

В данной главе мы рассмотрим более глубоко следующие разделы:

  • Условные зависимости Ansible

  • Циклы Ansible

  • Шаблоны

  • Переменные групп и хостов

  • Хранилище Ansible

  • Роли Ansible

  • Написание вашего собственного модуля

У нас есть всё что нужно для изучения, так что давайте начнём!

Условные зависимости Ansible

Условные зависимости Ansible аналогичны зависимостям программирования, поскольку они применяются для управления всем потоком ваших плейбуков (планов). Во многих случаях сам результат воспроизведения (play) может зависеть от значения некоторого факта, переменной или результата предыдущей задачи. Например, если у вас имеется некий плейбук для обновления образов маршрутизатора, вы хотите быть уверенным, что такой образ маршрутизатора имеется, прежде чем вы пуститесь во все тяжкие с перезагрузкой данного маршрутизатора.

В данном разделе мы рассмотрим оператор when, который поддерживается для всех модулей, а также уникальные состояния условных зависимостей, поддерживаемых модулях сетевых команд Ansible, таких как:

  • Эквивалентен (eq)

  • Не эквивалентен (neq)

  • Больше чем (gt)

  • Больше чем или равен (ge)

  • Меньше чем (lt)

  • Меньше чем или равен (le)

  • Содержится в

Оператор when

Оператор when полезен когда вам нужно проверить весь вывод из полученного результата и действовать соответствующим образом. Давайте рассмотрим пример его применения в chapter5_1.yml:


---
- name: IOS Command Output
  hosts: "iosv-devices"
  gather_facts: false
  connection: local
  vars:
    cli:
      host: "{{ ansible_host }}"
      username: "{{ username }}"
      password: "{{ password }}"
      transport: cli
    tasks:
      - name: show hostname
        ios_command:
          commands:
            - show run | i hostname
              provider: "{{ cli }}"
          register: output
      - name: show output
        when: '"iosv-2" in "{{ output.stdout }}"'
        debug:
          msg: '{{ output }}'
 	   

Мы уже видели все элементы из данного плейбука в Главе 4, Инфраструктура автоматизации Python - основы Ansible, единственная разница состоит во второй задаче, так как мы применяем оператор when для проверки того содержит ли вывод iosv-2. Если да, то мы продолжаем данную задачу, которая применяет модуль отладки для отображения всего вывода. Когда данный плейбук исполняется, мы наблюдаем такой вывод:


<пропуск>
TASK [show output]
*************************************************************
skipping: [ios-r1]
ok: [ios-r2] =< {
    "msg": {
        "changed": false,
        "stdout": [
            "hostname iosv-2"
        ],
        "stdout_lines": [
            [
                "hostname iosv-2"
            ]
        ],
        "warnings": []
    }
}
<пропуск>
 	   

Вы можете видеть, что ваше устройство iosv-r1 опущено в выводе, так как данный оператор не проходит. Дальше мы можем расширить этот пример в chapter5_2.yml чтобы применять изменения настроек только когда они соответствуют установленному условию:


<пропуск>
tasks:
  - name: show hostname
    ios_command:
      commands:
        - show run | i hostname
      provider: "{{ cli }}"
    register: output
  - name: config example
    when: '"iosv-2" in "{{ output.stdout }}"'
    ios_config:
      lines:
        - logging buffered 30000
      provider: "{{ cli }}"
 	   

Здесь мы можем увидеть вывод результата:


TASK [config example]
**********************************************************
skipping: [ios-r1]
changed: [ios-r2]
PLAY RECAP

***********************************************************
ios-r1 : ok=1 changed=0 unreachable=0 failed=0
ios-r2 : ok=2 changed=1 unreachable=0 failed=0
 	   

Отметим, что данный вывод показывает, что только iosv-r2 изменился, а iosv-r1 пропускается. Данный оператор when также очень полезен в тех случаях, когда поддерживается модуль установки и вы желаете обработать некоторые facts, которые мы получаем изначально. Например, следующий оператор обеспечит вам что воздействие будет произведено только для хостов Ubuntu с основной версией 16 при размещении такого условного выражения в оператор:


when: ansible_os_family == "Debian" and ansible_lsb.major_release|int >= 16
 	   
[Замечание]Замечание

Для получения дополнительной информации по условным зависимостям обратитесь к документации Ansible http://docs.ansible.com/ansible/playbooks_conditionals.html.

Условные зависимости сетевого модуля

Давайте рассмотрим пример с сетевым устройством. Мы можем получить преимущества от того факта, что и IOSv и Arista EOS предоставляют вывод в формате JSON в командах просмотра. Например, мы можем проверить состояние имеющегося интерфейса:


arista1#sh interfaces ethernet 1/3 | json
{
 "interfaces": {
 "Ethernet1/3": {
 "interfaceStatistics": {
<пропуск>
 "outPktsRate": 0.0
 },
 "name": "Ethernet1/3",
 "interfaceStatus": "disabled",
 "autoNegotiate": "off",
<пропуск>
}
arista1#
 	   

Если у нас имеется некая операция, которую мы намереваемся подготовить и это зависит от запрещения Ethernet1/3 чтобы не иметь воздействия на пользователя, такого как активное подключение пользователей к Ethernet1/3, мы можем воспользоваться следующей настройкой задач в chapter5_3.yml, которая предоставляется в модуле eos_command, чтобы убедиться что это имеет место, прежде чем мы продолжим:


<пропуск>
 tasks:
   - name: "sh int ethernet 1/3 | json"
     eos_command:
       commands:
         - "show interface ethernet 1/3 | json"
       provider: "{{ cli }}"
       waitfor:
         - "result[0].interfaces.Ethernet1/3.interfaceStatus eq disabled"
       register: output
         - name: show output
           debug:
             msg: "Interface Disabled, Safe to Proceed"
 	   

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


TASK [sh int ethernet 1/3 | json]
**********************************************
ok: [arista1]

TASK [show output]
*************************************************************
ok: [arista1] => {
 "msg": "Interface Disabled, Safe to Proceed"
}
 	   

В противном случае таким образом выдаётся ошибка:


TASK [sh int ethernet 1/3 | json]
**********************************************
fatal: [arista1]: FAILED! => {"changed": false, "commands": ["show interface ethernet 1/3 | json | json"], "failed": true, "msg":
"matched error in response: show interface ethernet 1/3 | json | jsonrn% Invalid input (privileged mode required)rn********1>"}
 to retry, use: --limit 
@/home/echou/Master_Python_Networking/Chapter5/chapter5_3.retry

PLAY RECAP
******************************************************************
arista1 : ok=0 changed=0 unreachable=0 failed=1
 	   

Если вашей ситуации соответствуют такие условия как содержится в, больше чем или меньше чем, проверяйте с их участием.

Циклы Ansible

Ansible предоставляет ряд циклов в вашем плейбуке, таких как стандартные циклы, циклы по файлам, подэлементам, do-until и многие другие. В данном разделе мы рассмотрим две формы из наиболее часто используемых, а тменно, стандартные циклы и циклы по значениям хэш- тегов.

Стандартные циклы

Стандартные циклы в плейбуке часто используются для простого исполнения аналогичных задач множество раз. Синтаксис стандартного цикла очень прост; а именно, переменная {{ item }} является резервирующей пространство цикла по списку with_items. Например, давайте рассмотрим следующее:


tasks:
  - name: echo loop items
    command: echo {{ item }}
    with_items: ['r1', 'r2', 'r3', 'r4', 'r5']
 	   

Она пройдёт цикл из пяти элементов с одной и той же командой echo:


TASK [echo loop items] *********************************************************
changed: [192.168.199.185] => (item=r1)
changed: [192.168.199.185] => (item=r2)
changed: [192.168.199.185] => (item=r3)
changed: [192.168.199.185] => (item=r4)
changed: [192.168.199.185] => (item=r5)
 	   

При объединении с модулем сетевых команд следующая задача добавит множество vlan в данное устройство:


tasks:
  - name: add vlans
    eos_config:
      lines:
        - vlan {{ item }}
    provider: "{{ cli }}"
  with_items:
      - 100
      - 200
      - 300
 	   

Такой список with_items также может быть считан из некоторой переменной, что предоставляет дополнительную гибкость для имеющейся структуры вашего плейбука:


vars:
  vlan_numbers: [100, 200, 300]
<пропуск>
tasks:
  - name: add vlans
    eos_config:
      lines:
          - vlan {{ item }}
      provider: "{{ cli }}"
    with_items: "{{ vlan_numbers }}
 	   

Стандартный цикл является отличным средством экономии времени, когда дело доходит до выполнения дублируемых задач в плейбуке. В следующем разделе мы рассмотрим цикл по словарям.

Циклы по словарям

Цикл по простому списку это прекрасно. Однако часто у нас имеются некие сущности с более чем одним связанным с ними атрибутом. Если вы подумаете о примере с vlan в последнем разделе, каждый vlan мог бы иметь некоторые дополнительные атрибуты уникальные именно для него, такие как описание vlan, его IP адрес и, возможно, и другие. Часто мы можем использовать словари для представления таких сущностей чтобы собирать в них множество атрибутов.

Давайте расширим наш пример vlan в последнем разделе для примера словаря в chapter5_6.yml. Мы определим значения своего словаря для трёх vlans, причём каждый имеет встроенный словарь для описания и адреса IP:


<пропуск>
vars:
   cli:
     host: "{{ ansible_host }}"
       username: "{{ username }}"
       password: "{{ password }}"
       transport: cli
     vlans: {
        "100": {"description": "floor_1", "ip": "192.168.10.1"},
        "200": {"description": "floor_2", "ip": "192.168.20.1"}
        "300": {"description": "floor_3", "ip": "192.168.30.1"}
   }
 	   

Мы можем настроить самую первую задачу, add vlans, воспользовавшись имеющимся ключом для каждого элемента как номера vlan:


tasks:
  - name: add vlans
    nxos_config:
      lines:
        - vlan {{ item.key }}
      provider: "{{ cli }}"
    with_dict: "{{ vlans }}"
 	   

Мы можем продолжить настройку данного интерфейса vlan. Отметим, что мы используем имеющиеся родительские параметры для уникальной идентификации того раздела, который команд должен быть проверен данной командой. Это обусловлено тем фактом, что само описание и его IP адрес оба настроены в подразделе interface vlan <number>:


- name: configure vlans
  nxos_config:
    lines:
      - description {{ item.value.name }}
      - ip address {{ item.value.ip }}/24
    provider: "{{ cli }}"
    parents: interface vlan {{ item.key }}
  with_dict: "{{ vlans }}"
 	   

После исполнения вы обнаружите проход по своему словарю:


TASK [configure vlans] *********************************************************
changed: [nxos-r1] => (item={'key': u'300', 'value': {u'ip': u'192.168.30.1', u'name': u'floor_3'}})
changed: [nxos-r1] => (item={'key': u'200', 'value': {u'ip': u'192.168.20.1', u'name': u'floor_2'}})
changed: [nxos-r1] => (item={'key': u'100', 'value': {u'ip': u'192.168.10.1', u'name': u'floor_1'}})
 	   

Давайте проверим, применяется ли к данному устройству предлагаемая настройка:


nx-osv-1# sh run | i vlan
<пропуск>
vlan 1,10,100,200,300
nx-osv-1#

nx-osv-1# sh run | section "interface Vlan100"
interface Vlan100
  description floor_1
  ip address 192.168.10.1/24
nx-osv-1#
 	   
[Замечание]Замечание

Оносительно дополнительных типов циклов не стесняйтесь обратиться к документации: http://docs.ansible.com/ansible/playbooks_loops.html .

В первые несколько раз при своём применении зацикливание по словарям требует некоторого опыта. Но также как и стандартные циклы, циклы по словарям станут бесценным инструментом на вашем инструментальном поясе.

Шаблоны

Сколько я себя помню, я всегда применял своего роба сетевой шаблон. По моему опыту многие сетевые устройства имеют идентичные разделы сетевых настроек, в особенности если эти устройства обслуживают одну и ту же роль в вашей сетевой среде. В большинстве случаев, когда нам необходимо предоставить некое новое сетевое устройство, мы применяем одни и теже настройки в виде некоторого шаблона, заменяем все необходимые поля и копируем данный файл по всем новым устройствам.При помощи Ansible вы можете автоматизировать всю такую работу при помощи своего модуля шаблона (http://docs.ansible.com/ansible/template_module.html).

Базовый файл шаблона, который мы применяем будет использовать язык шаблонов Jinja2 (http://jinja.pocoo.org/docs/). Мы кратко обсудили язык шаблонов Jinja2 в предыдущей главе и мы взглянем на него немного больше здесь. В точности как и Ansible, Jinja2 имеет свой собственный синтаксис и метод выполнения циклов и условных зависимостей; к счастью, для наших целей нам потребуется знать всего лишь самые основы его. Вы постепенно изучите этот синтаксис в процессе построения своих плейбуков.

Основной синтаксис для использования шаблона очень прост; вам всего лишь необходимо определить сам файл источника и расположение получателя, куда вы желаете его скопировать. Сейчас мы создадим некий пустой файл:


$ touch file1
 	   

Затем мы применим следующий плейбук для копирования file1 в file2, причём отметим, что данный плейбук исполняется только на самой управляющей машине ; теперь определим конкретный путь и для файла источника, и для файла получателя в виде аргументов:


---
- name: Template Basic
  hosts: locahost

  tasks:
    - name: copy one file to another
      template:
        src=./file1
        dest=./file2
 	   

Нам не нужно определять файл хоста, так как локальный хост доступен по умолчанию; вы однако получите предупреждение:


$ ansible-playbook chapter5_7.yml
[WARNING]: provided hosts list is empty, only localhost is available
<пропуск>
TASK [copy one file to another] ************************************************
changed: [localhost]
<пропуск>
 	   

Имя файла источника может иметь любое расширение, однако так как они будут обрабатываться механизмом шаблонов Jinja2, давайте создадим некий файл с названием nxos.j2 в качестве своего источника шаблона. Этот шаблон будет следовать соглашениям Jinja2 применения двойных фигурных скобок для определения своих переменных:


hostname {{ item.value.hostname }}
feature telnet
feature ospf
feature bgp
feature interface-vlan

username {{ item.value.username }} password {{ item.value.password }} role network-operator
 	   

Шаблоны Jinja2

Давайте изменим соответствующим образом свой плейбук. В chapter5_8.yml мы внесём следующие изменения:

  1. Изменим свой файл источника на nxos.j2.

  2. Изменим имеющийся файл получателя на некую переменную.

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

    
    ---
    - name: Template Looping
      hosts: localhost
    
      vars:
        nexus_devices: {
          "nx-osv-1": {"hostname": "nx-osv-1", "username": "cisco", "password": "cisco"}
        }
    
      tasks:
        - name: create router configuration files
          template:
            src=./nxos.j2
            dest=./{{ item.key }}.conf
          with_dict: "{{ nexus_devices }}"
     	   

После исполнения данного плейбука вы обнаружите созданный файл получатель nx-osv-1.conf с заполненными значениями и готовый к использованию:


$ cat nx-osv-1.conf
hostname nx-osv-1

feature telnet
feature ospf
feature bgp
feature interface-vlan

username cisco password cisco role network-operator
 	   

Циклы Jinja2

Мы также можем выполнять цикл по некоторому списку, также как и по словарю, в точности как мы это делали в предыдущем разделе; внесём следующие изменения в nxos.j2:


{% for vlan_num in item.value.vlans %}
vlan {{ vlan_num }}
{% endfor %}

{% for vlan_interface in item.value.vlan_interfaces %}
interface {{ vlan_interface.int_num }}
  ip address {{ vlan_interface.ip }}/24
{% endfor %}
 	   

Укажем в данном плейбуке определённый дополнительный список и переменные словаря:


vars:
  nexus_devices: {
    "nx-osv-1": {
    "hostname": "nx-osv-1",
    "username": "cisco",
    "password": "cisco",
    "vlans": [100, 200, 300],
    "vlan_interfaces": [
       {"int_num": "100", "ip": "192.168.10.1"},
       {"int_num": "200", "ip": "192.168.20.1"},
       {"int_num": "300", "ip": "192.168.30.1"}
     ]
    }
  }
 	   

Исполните данный плейбук и вы обнаружите в своей конфигурации маршрутизатора заполненными и настройки для vlan, и настройки для vlan_interfaces.

Условные зависимости Jinja2

Jinja2 также поддерживает и некую условную проверку. Давайте добавим такое поле для регулировки своего свойства имеющегося сетевого потока для определённого устройства. Мы добавим в nxos.j2 следующее:


{% if item.value.netflow_enable %}
feature netflow
{% endif %}
 	   

Вот его плейбук:


vars:
nexus_devices: {
<пропуск>
       "netflow_enable": True
<пропуск>
}
 	   

Самый последний шаг, который мы предпримем, состоит в том чтобы сделать nxos.j2 более масштабируемым, разместив сам раздел vlan внутри некоторой условной проверки true - false. В условиях на практике в большинстве случаев у нас будет иметься множество устройств обладающих информацией об их vlan, однако только одно устройство будет выступать в роли определённого для хостов клиентов шлюза:


{% if item.value.l3_vlan_interfaces %}
{% for vlan_interface in item.value.vlan_interfaces %}
interface {{ vlan_interface.int_num }}
ip address {{ vlan_interface.ip }}/24
{% endfor %}
{% endif %}
 	   

В свой плейбук мы также добавим некоторое второе устройство с названием nx-osv-2:


vars:
  nexus_devices: {
  <пропуск>
    "nx-osv-2": {
    "hostname": "nx-osv-2",
    "username": "cisco",
    "password": "cisco",
    "vlans": [100, 200, 300],
    "l3_vlan_interfaces": False,
    "netflow_enable": False
    }
    <пропуск>
  }
 	   

Клёво, да? Это безусловно сохранит нам массу времени в чём- то что требует повторяющего копирования и вставки перед этим.

Группы и переменные хостов

Отметим, что в предыдущем примере мы повторились в переменных имени пользователя и пароля для двух устройств в объемлющей переменной nexus_devices:


vars:
  nexus_devices: {
    "nx-osv-1": {
      "hostname": "nx-osv-1",
      "username": "cisco",
      "password": "cisco",
      "vlans": [100, 200, 300],
    <пропуск>
    "nx-osv-2": {
      "hostname": "nx-osv-2",
      "username": "cisco",
      "password": "cisco",
      "vlans": [100, 200, 300],
    <пропуск>
 	   

Это не является идеальным. Если нам понадобится изменить значение такого имени пользователя и его пароль, нам необходимо будет не забыть сделать это в двух местах. Это усиливает бремя нашего управления а также шансы допустить ошибку если мы забудем выполнить изменения во всех местах. В качестве практичного приёма Ansible предлагает нам применять каталоги group_vars и host_vars для подразделения наших переменных.

[Замечание]Замечание

Для ознакомления с прочими практическими приёмами советуем ознакомиться с http://docs.ansible.com/ansible/playbooks_best_practices.html.

Переменные групп

По умолчанию Ansible будет отыскивать групповые переменные в том же самом каталоге, где находится данный плейбук с названием group_vars на предмет переменных, которые могут применяться в данной группе. По умолчанию, он будет выглядеть как определённый файл с названием, соответствующим данному имени группы. Например, если у нас в нашем файле учёта ресурсов имеется некая группа с названием [nexus-devices], в group_vars мы можем иметь некий файл с названием nexus-devices для размещения всех применяемых к данной группе переменных. Мы также можем иметь некий файл с названием all, который содержит переменные, применяемые ко всем нашим группам.

Мы применим данную функциональность для своих переменных имени пользователя и пароля:


$ mkdir group_vars
 	   

Затем мы можем создать некий файл YAML с названием all, который будет содержать соответствующие имя пользователя и пароль:


$ cat group_vars/all
---
username: cisco
password: cisco
 	   

Теперь мы можем воспользоваться этими переменными в своём плейбуке:


vars:
  nexus_devices: {
    "nx-osv-1": {
      "hostname": "nx-osv-1",
      "username": "{{ username }}",
      "password": "{{ password }}",
      "vlans": [100, 200, 300],
    <пропуск>
    "nx-osv-2": {
      "hostname": "nx-osv-2",
      "username": "{{ username }}",
      "password": "{{ password }}",
      "vlans": [100, 200, 300],
    <пропуск>
 	   

Переменные хостов

В формате, аналогичном для переменных групп мы можем дальше отделить все переменные хоста:


$ mkdir host_vars
 	   

В нашем случае мы выполнили данную команду в своём локальном хосте, следовательно соответствующий файл в host_vars должени иметь надлежащее название, такое как host_vars/localhost. Отметим, что мы оставили объявленными свои переменные в group_vars.


vars:
  nexus_devices: {
    "nx-osv-1": {
      "hostname": "nx-osv-1",
      "username": "{{ username }}",
      "password": "{{ password }}",
      "vlans": [100, 200, 300],
    <пропуск>
	  "netflow_enable": True
    "nx-osv-2": {
      "hostname": "nx-osv-2",
      "username": "{{ username }}",
      "password": "{{ password }}",
      "vlans": [100, 200, 300],
    <пропуск>
 	   

После того как мы выделили все переменные, наш плейбук теперь становится обладающим очень малым весом и содержит только саму логику наших операций:


$cat chapter5_9.yml
---
- name: Ansible Group and Host Varibles
  hosts: localhost
  tasks:
    - name: create router configuration files
      template:
        src=./nxos.j2
        dest=./{{ item.key }}.conf
      with_dict: "{{ nexus_devices }}"
 	   

Наши каталоги group_vars и host_vars не только снижают перегруженность операций. Они также могут помочь с безопасностью данных файлов, что мы увидим далее.

Кладовая Ansible

Как мы можем видеть из своего предыдущего раздела, во многих случаях определённая переменная Ansible часто предоставляет чувствительную информацию, например, имя пользователя и его пароль. Будет неплохой мыслью предпринять некие меры безопасности в отношении таких переменных, чтобы мы могли защитить эту информацию. Кладовая (vault) Ansible предоставляет шифрование для файлов вместо того чтобы применять обычный текст.

Функции Vault Ansible запускаются командой ansible-vault. Вы можете создать вручную некий зашифрованный файл применив опцию создания. Вы получите приглашение на ввод пароля. Если вы попробуете просмотреть данный файл, вы обнаружите что данный файл не является обычным текстом:


$ ansible-vault create secret.yml
Vault password:
$ cat secret.yml
$ANSIBLE_VAULT;1.1;AES256
336564626462373962326635326361323639323635353630646665656430353261383737623<пропуск>6535373338373838636365303564646230323334323861393033356632623962
 	   

Далее вы можете изменять этот файл с помощью опции edit или просматривать этот файл применяя опцию view:


$ ansible-vault edit secret.yml
Vault password:
$ ansible-vault view secret.yml
Vault password:
 	   

Давайте зашифруем свои файлы переменных group_vars/all и host_vars/localhost:


$ ansible-vault encrypt group_vars/all host_vars/localhost
Vault password:
Encryption successful
 	   

Теперь, когда мы исполним свой плейбук мы получим сообщение об ошибке расшифровки:


ERROR! Decryption failed on
/home/echou/Master_Python_Networking/Chapter5/Vaults/group_vars/all
 	   

Нам необходимо применять опцию --ask-vault-pass при исполнении данного плейбука:


$ ansible-playbook chapter5_10.yml --ask-vault-pass
Vault password:
 	   

Расшифровка производится в оперативной памяти для всех зашифрованных файлов в кладовой, к которым выполняется доступ.

[Замечание]Замечание

В настоящее время имеющаяся кладовая требует чтобы все файлы в ней были зашифрованы одним и тем же паролем.

Мы также можем сохранить этот пароль в некотором файле и убедиться что данный конкретный файл имеет ограничения в доступе:


$ chmod 400 ~/.vault_password.txt
$ ls -lia ~/.vault_password.txt
809496 -r-------- 1 echou echou 9 Feb 18 12:17 /home/echou/.vault_password.txt
 	   

Затем мы можем выполнить свой плейбук с параметром --vault-password-file:


 ansible-playbook chapter5_10.yml --vault-password-file ~/.vault_password.txt
 	   

Включения и роли Ansible

Наилучшим способом для обработки сложных задач является их разбиение на части меньшего размера. Данный подход, конечно, является общим и для Python, и для сетевой инженерии. В Python мы разбиваем сложный код на функции, классы, модули и пакеты. В Сетевой среде мы также разбиваем большие сети на разделы такие как стойки, ряды, кластеры и центры обработки данных. В Ansible для организации крупных плейбуков (планов) применяются roles (роли) и includes (вложения). Разбиение большого плейбука Ansible упрощает всю структуру, так как каждый файл сосредотачивается на меньшем числе задач. Это также облегчает повторное применение таких разделов боле простым в вашем плейбуке.

Оператор включения

По мере роста вашего плейбака в размере, со временем становится очевидным что многие имеющиеся задачи и плейбуки (планы) могут использоваться совместно в различных плейбуках. Оператор includes Ansible аналогичен многим файлам настройки Linux, которые просто сообщают машине необходимость данный файл таким образом, как будто включаемый файл напрямую записан в нём. Мы можем выполнять оператор включения и в плейбуках (планах), и в задачах. Мы рассмотрим некий простой пример по расширению наших задач.

Давайте предположим, что мы хотим отображать вывод для двух различных плейбуков. Мы можем сделать некий отдельный файл YAML с названием show_output.yml как некоторую дополнительную задачу:


---
- name: show output
  debug:
    var: output
 	   

Затем мы можем повторно применять эту задачу во многих плейбуках, например в chapter5_11_1.yml, что выглядит во многом аналогично нашему самому последнему плейбуку, за исключением зарегистрированного вывода и оператора включения в самом конце:


---
- name: Ansible Group and Host Varibles
  hosts: localhost

  tasks:
    - name: create router configuration files
      template:
        src=./nxos.j2
        dest=./{{ item.key }}.conf
      with_dict: "{{ nexus_devices }}"
      register: output

    - include: show_output.yml
 	   

Другой плейбук, chapter5_11_2.yml, может повторно применять включение show_output.yml аналогичным образом:


---
- name: show users
  hosts: localhost

  tasks:
    - name: show local users
      command: who
      register: output

    - include: show_output.yml
 	   

Отметим, что оба плейбука используют одно и то же имя переменной, так как сам show_output.yml жёстко кодирует данное имя переменной для простоты при демонстрации. Вы также можете передавать во включаемый файл переменные.

Роли Ansible

Роли Ansible разделяют все логические функции с физическими хостами чтобы лучше соответствовать вашей сетевой среде. Например, вы можете разрабатывать такие роли как стволы, листья, ядро, а также Cisco, Juniper и Arista. Одни и те же физические хосты могут относиться ко множеству ролей; например, некое устройство может относиться как к Juniper, так и к ядру. Это делает логику более осмысленной, поскольку мы можем выполнять такие операции как обновление для всех устройств Juniper по всей сетевой среде безотносительно к их расположению в конкретном уровне общей сетевой среды.

Роли Ansible могут автоматически загружать определённые переменные, задачи и обработчики на основе некоторой известной инфраструктуры файлов. Ключевым является то, что это именно та известная структура файлов, которую мы автоматически вкладываем; на самом деле вы можете представлять себе роли как предварительно созданные и вкладываемые операторы Ansible.

Имеющаяся документация ролей плейбука Ansible (http://docs.ansible.com/ansible/playbooks_roles.html#roles) описывает некий список каталогов ролей которые вы можете настраивать. У вас нет необходимости применять их все сразу; в действительности мы будем изменять только определённые задачи и папки переменных. Однако неплохо знать что они имеются.

Ниже перечислено что мы будем использовать в качестве примера для наших ролей:


  chapter5_12.yml
  chapter5_13.yml
  hosts
  roles
   cisco_nexus
       defaults
       files
       handlers
       meta
       tasks
           main.yml
       templates
       vars
           main.yml
   spines
       defaults
       files
       handlers
       tasks
           main.yml
       templates
       vars
           main.yml
 	   

Вы можете видеть, что на самом верхнем уровне у нас имеется файл хостов, а также наши плейбуки. У нас также есть некая папка с названием roles; внутри неё у нас есть две определённые роли, cisco_nexus и spines. Большая часть подчинённых папок под этими ролями были пустыми, за исключение папок задач и переменных. Внутри каждой из них имеется некий файл с названием main.yml. Такое поведение является определяемым по умолчанию, причём файл main.yml является вашей точкой входа, которая автоматически включается в данный плейбук когда вы определяете данную роль в конкретном плейбуке. Если вам нужно разбиение на дополнительные файлы, вы можете применять оператор вложения в самом файле main.yml.

Вот наш сценарий:

  • У нас есть два устройства Cisco Nexus, nxos-r1 и nxos-r2. Мы настроем сервер журнала а также регистрацию состояния соединения для каждого из них, применяя к ним роль cisco_nexus.

  • Кроме того, nxos-r1 также является неким устройством ствола, в котором мы хотим настроить более детальное ведение протокола, так как он занимает более критически важное положение в нашей сетевой среде.

Для нашей роли cisco_nexus у нас есть следующие переменные в roles/cisco_nexus/vars/main.yml:


---
cli:
  host: "{{ ansible_host }}"
  username: cisco
  password: cisco
  transport: cli
 	   

Также у нас имеются в роли roles/cisco_nexus/tasks/main.yml следующие настроенные задачи:


---
- name: configure logging parameters
  nxos_config:
    lines:
      - logging server 191.168.1.100
      - logging event link-status default
    provider: "{{ cli }}"
 	   

Наш плейбук чрезвычайно прост, так как он требуется всего лишь для определения тех хостов, которые мы бы хотели настраивать при помощи роли cisco_nexus:


---
- name: playbook for cisco_nexus role
  hosts: "cisco_nexus"
  gather_facts: false
  connection: local
  roles:
    - cisco_nexus
 	   

Когда вы исполните этот плейбук, данный план вложит все определённые в роли cisco_nexus задчи и переменные и настроит надлежащим образом все необходимые устройства.

Для нашей роли spine у нас имеется некая дополнительная задача более многословного ведения журнала в roles/spines/tasks/mail.yml:


---
- name: change logging level
  nxos_config:
  lines:
    - logging level local7 7
  provider: "{{ cli }}"
 	   

В своём плейбуке мы можем определить, что он содержит обе роли, как cisco_nexus, так и spiens:


---
- name: playbook for spine role
  hosts: "spines"
  gather_facts: false
  connection: local

  roles:
    - cisco_nexus
    - spines
 	   

Отметим, что когда мы сделаем это, наша роль cisco_nexus будет исполнена вслед за ролью spiens:


TASK [cisco_nexus : configure logging parameters] ******************************
changed: [nxos-r1]

TASK [spines : change logging level] *******************************************
ok: [nxos-r1]
 	   

Роли Ansible являются гибкими и масштабируемыми. В точности так же как и функции и классы Python. Как только ваш код достигает определённого уровня, почти всегда будет хорошей мыслью разбить его на куски меньшего размера для удобства сопровождения.

[Замечание]Замечание

Вы можете найти дополнительные примеры ролей в репозитории примеров Git по адресу (https://github.com/ansible/ansible-examples).

Написание вашего собственного пользовательского модуля

На данный момент вам может начать казаться, что управление сетевыми средствами во многом зависит от обнаружения правильного модуля для своего устройства. В этой логике несомненно присутствует доля истины. Модули предоставляют некий способ абстрактного взаимодействия между управляемым хостом и самой управляющей машиной, в то время как он делает для вас возможным сосредоточиться на самой логике вашей работы. Вплоть до этого момента мы видели, что все основные производители предоставляют некий широкий диапазон поддержки модулей для Cisco, Juniper и Arista.

Рассмотрим в качестве примера модули Cisco Nexus, помимо конкретных задач, таких как управление соседним BGP (nxos_bgp) и сервером aaa (nxos_aaa_server), большинство производителей также предоставляют способы исполнения произвольного отображения (nxos_config) и настройки (nxos_config) команд. Обычно это охватывает все наши варианты использования.

Но что если используемое нами в настоящий момент устройство не имеет прямо сейчас никаких модулей, которые мы бы могли найти? В данном разделе мы рассмотрим некоторые способы которыми вы можете устранить такую ситуацию написав свой собственный пользовательский модуль.

Самый первый пользовательский модуль

Напимание некоторого пользовательского модуля не обязательно усложнять; на самом деле он даже не обязан быть на Python. Но, поскольку мы уже знакомы с Python, мы будем использовать Python для наших пользовательских модулей. Мы предполагаем, что данный модуль это то, что мы будем применять самостоятельно и в своей команде не обращаясь дополнительно к Ansible, таким образом игнорируя некое документирование и форматирование в этот раз.

По умолчанию, если вы создаёте некую папку библиотеки в том же самом каталоге, что и ваш плейбук, Ansible включит этот каталог в путь поиска модуля. Все такие модули должны возвращать некий вывод JSON обратно в свой плейбук.

Возвращаясь к Главе 3, Работа с сетями через API и для достижения целей, мы применяем следующий сценарий Python NXAPI для взаимодействия с устройством NX-OS:


import requests
import json

url='http://172.16.1.142/ins'
switchuser='cisco'
switchpassword='cisco'

myheaders={'content-type':'application/json-rpc'}
payload=[
  {
    "jsonrpc": "2.0",
    "method": "cli",
    "params": {
      "cmd": "show version",
      "version": 1.2
    },
    "id": 1
  }
]
response = requests.post(url,data=json.dumps(payload),
headers=myheaders,auth=(switchuser,switchpassword)).json()

print(response['result']['body']['sys_ver_str'])
 	   

Когда мы его выполним, мы просто получим номер версии системы. Если мы просто изменим самую последнюю строку с тем чтобы она выполняла вывод в формате JSON, мы увидим следующее:


version = response['result']['body']['sys_ver_str']
print json.dumps({"version": version})
 	   

Затем мы можем воспользоваться в нашем плейбуке встраиваемым модулем действия (https://docs.ansible.com/ansible/dev_guide/developing_plugins.html), chapter5_14.yml, для вызова данного пользовательского модуля:


---
- name: Your First Custom Module
  hosts: localhost
  gather_facts: false
  connection: local

  tasks:
    - name: Show Version
      action: custom_module_1
      register: output

    - debug:
        var: output
 	   

Отметим, что в точности как и при соединении через ssh мы исполняем этот модуль локально при том что этот модуль делает исходящие вызовы API. Когда вы выполните данный плейбук, вы получите следующий вывод:


$ ansible-playbook chapter5_14.yml
[WARNING]: provided hosts list is empty, only localhost is available
PLAY [Your First Custom Module] ************************************************

TASK [Show Version] ************************************************************
ok: [localhost]

TASK [debug] *******************************************************************
ok: [localhost] => {
 "output": {
 "changed": false,
 "version": "7.3(0)D1(1)"
 }
} 

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

Как вы можете видеть, вы можете написать любой модуль, который поддерживается API и Ansible успешно примет любой возвращаемый вывод JSON.

Второй пользовательский модуль

При построении своего последнего модуля давайте воспользуемся имеющимся в Ansible модулем общего назначения Boilerplate (заготовок кода), как это описывается в документации разработки модуля (http://docs.ansible.com/ansible/dev_guide/developing_modules_general.html). Мы изменим самый последний пользовательский модуль в custom_module_2.py чтобы воспользоваться проглатыванием входа из своего плейбука.

Вначале мы импортируем сам код Boilerplate из ansible.module_utils.basic:


from ansible.module_utils.basic import AnsibleModule

if __name__ == '__main__':
    main()
 	   

Начиная с этого момента мы можем затем определять свою основную функцию в которой мы поместим наш код. AnsibleModule предоставляет множество общего кода для обработки возвращаемых результатов и синтаксического разбора аргументов. В нашем следующем примере мы проведём синтаксический разбор аргументов для host, username и password и сделаем их необходимыми полями:


def main():
    module = AnsibleModule(
      argument_spec = dict(
      host = dict(required=True),
      username = dict(required=True),
      password = dict(required=True)
      )
    )
 	   

Данные значения затем могут быть изъяты и применены в нашем коде:


device = module.params.get('host')
username = module.params.get('username')
password = module.params.get('password')

url='http://' + host + '/ins'
switchuser=username
switchpassword=password
 	   

Наконец, мы последуем коду выхода и вернём своё значение:


module.exit_json(changed=False, msg=str(data))
 	   

Наш новый плейбук будет выглядеть в точности как самый последний приведённый нами ранее с одним исключением теперь, когда мы можем передавать значения для различных устройств в своём плейбуке:


tasks:
  - name: Show Version
    action: custom_module_1 host="172.16.1.142" username="cisco" password="cisco"
    register: output
 	   

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

Конечно, это работающий, но не завершённый модуль; одной из вещей, которую мы не выполняем, это отсутствие какого бы то ни было контроля ошибок или документирования. Однако, он демонстрирует как просто построить некий пользовательский модуль из уже сделанного нами сценария.

Выводы

В данной главе мы рассмотрели множество вопросов. Основываясь на своих полученных ранее базовых знаниях Ansible, мы перешли к более сложным темам, таким как условные зависимости, циклы и шаблоны. Мы рассмотрели как сделать наш плейбук (план) более масштабируемым при помощи переменных хоста, переменных групп, операторов вложения и ролей. Мы также рассмотрели как обезопасить наш плейбук при помощи кладовой (vault) Ansible. Наконец, мы применили Python чтобы сделать собственные пользовательские модули.

Ansible является очень гибкой структурой Python, которая может применяться для сетевой автоматизации. Он предоставляет другой уровень абстракции, отличающийся от таких сценариев как Pexpect и основанные на API. В зависимости от ваших потребностей и сетевого окружения он может быть идеальной инфраструктурой, которую вы можете применять для сохранения своих времени и энергии.

В следующей главе мы рассмотрим реализацию сетевой безопасности с помощью Python.