Глава 5. Инфраструктура автоматизации Python - дополнительные вопросы Ansible
Содержание
В данной главе мы продолжим выстраивать наши уже полученные знания и углубимся в пучину более расширенных тем Ansible. Про Ansible написано много книг. имеется много чего что мы не можем охватить в двух главах. Основная цель состоит в в введении в основные свойства и функции Ansible, которые как я полагаю, необходимы сетевому инженеру и сокращают кривую обучения настолько, насколько это возможно.
Важно указать, что если вы недостаточно чётко уяснили какие- то моменты из предыдущей главы, сейчас самое время вернуться обратно и пересмотреть их, так как они требуются для данной главы.
В данной главе мы рассмотрим более глубоко следующие разделы:
-
Условные зависимости Ansible
-
Циклы Ansible
-
Шаблоны
-
Переменные групп и хостов
-
Хранилище Ansible
-
Роли Ansible
-
Написание вашего собственного модуля
У нас есть всё что нужно для изучения, так что давайте начнём!
Условные зависимости Ansible аналогичны зависимостям программирования, поскольку они применяются для управления всем потоком ваших плейбуков (планов). Во многих случаях сам результат воспроизведения (play) может зависеть от значения некоторого факта, переменной или результата предыдущей задачи. Например, если у вас имеется некий плейбук для обновления образов маршрутизатора, вы хотите быть уверенным, что такой образ маршрутизатора имеется, прежде чем вы пуститесь во все тяжкие с перезагрузкой данного маршрутизатора.
В данном разделе мы рассмотрим оператор when, который поддерживается для всех модулей, а также уникальные состояния условных зависимостей, поддерживаемых модулях сетевых команд Ansible, таких как:
-
Эквивалентен (
eq
) -
Не эквивалентен (
neq
) -
Больше чем (
gt
) -
Больше чем или равен (
ge
) -
Меньше чем (
lt
) -
Меньше чем или равен (
le
) -
Содержится в
Оператор 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 предоставляет ряд циклов в вашем плейбуке, таких как стандартные циклы, циклы по файлам, подэлементам, 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
Давайте изменим соответствующим образом свой плейбук. В chapter5_8.yml
мы внесём следующие изменения:
-
Изменим свой файл источника на
nxos.j2
. -
Изменим имеющийся файл получателя на некую переменную.
-
Предоставьте все значения переменных, которые мы будем подставлять в данный шаблон, указанный в имеющемся разделе задач.
--- - 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
Мы также можем выполнять цикл по некоторому списку, также как и по словарю, в точности как мы это делали в
предыдущем разделе; внесём следующие изменения в 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 также поддерживает и некую условную проверку. Давайте добавим такое поле для регулировки своего
свойства имеющегося сетевого потока для определённого устройства. Мы добавим в
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 часто предоставляет чувствительную информацию, например, имя пользователя и его пароль. Будет неплохой мыслью предпринять некие меры безопасности в отношении таких переменных, чтобы мы могли защитить эту информацию. Кладовая (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
Наилучшим способом для обработки сложных задач является их разбиение на части меньшего размера. Данный
подход, конечно, является общим и для 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 разделяют все логические функции с физическими хостами чтобы лучше соответствовать вашей сетевой среде. Например, вы можете разрабатывать такие роли как стволы, листья, ядро, а также 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.