суббота, 13 марта 2021 г.

VyOS | Странная лаба | API



А - автоматизация.

Сегодня короткая заметка...

Когда я знакомился с VyOS мне нужно было держать два файервола с одинаковыми настройками. Напомню, я использую стабильный релиз (не rolling) в котором на данный момент нет API. Поэтому я по старинке написал небольшую утилиту использующую expect. Сегодня глянем что бы я мог сделать имея API.

Если нет времени читать - код тут

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

Сценарий



Представим, что мы раздаем клиентские сети (те что снизу на картинке выше) каким-то... клиентам. У нас все стандартизировано. Клиент пришел, попросил сеть - нажали на кнопочку, сеть выдали. Все. 
  • Выдаем /24 из 10.0.0.0/16
  • В третьем октете имеем CLIENT_ID = 1...253 (не очень много у нас будет клиентов...)
  • Для нечетных клиентов R1 будет VRRP мастером, для четных - R2
  • Номер VRF/VFI и всего прочего в конфигурации соответсвует номеру клиента.
  • Клиенту можно дать доступ в Интернет, а можно и не дать

Включаем API

Для начала включим сам API. 

set service https api debug
set service https api keys id MY_KEY key 'SECRET_ONE'

И пройдемся CURLом по железкам. 

[root@localhost ~]# for _RID in 11 12 21 22; do curl -k -X POST -F data='{"op": "showConfig", "path": ["system","host-name"]}' -F key="SECRET_ONE" https://192.168.0.$_RID/retrieve | python2 -m json.tool; done


{

    "data": {

        "host-name": "F1"

    },

    "error": null,

    "success": true

}

 

{

    "data": {

        "host-name": "F2"

    },

    "error": null,

    "success": true

}


{

    "data": {

        "host-name": "R1"

    },

    "error": null,

    "success": true

}

 

{

    "data": {

        "host-name": "R2"

    },

    "error": null,

    "success": true

}


Собственно все. Документации по фиче не так уж много и вот тут описана большая часть необходимого.
  • По сути мы имеем stateless HTTP API, который поддерживает только метод POST.
  • Имеем следующие endpoint'ы
    • retrieve - получаем конфигурации для пути
    • image - для работы с образами
    • show - надо разобраться чем отличается от retrieve
    • generate - довольно специфичная штука для генерирования например ключей
    • configure - для собственно настройки оборудования с помощью (set, delete и comment)
    • config-file - для сохранения конфигурации (copy run start)

Пример работы

Как уже сказал, мы будем создавать клиентские сети. Для этого нам нужно применить соответсвующие настройки на 4 устройствах - R1/R2, F1/F2. Для того чтобы сделать скрипт более или менее универсальным мы 
- будем генерировать нужные данные с помощью jinja
- хранить соответствие данные-темпрейт в отдельном файле

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

А теперь по порядку

Все начинается с inventory файла. В нашем случае мы храним тут только IP адреса.

[root@localhost vyos_api_example]# cat inventory.yaml 

R1:

    address: 192.168.0.21

R2:

    address: 192.168.0.22

F1:

    address: 192.168.0.11

F2:

    address: 192.168.0.12



Ключ API храним в отдельном файле, который должен быть исключен с помощью .gitignore

[root@localhost vyos_api_example]# cat constants.py 

# This file should be in .gitignore

API_KEY = "SECRET_ONE"


Далее сами темплейты. Сам скрипт получился в стиле Jinja programming, так что строго не судите... Jinja кода получилось больше чем Python. ) Зато якобы можно переиспользовать.
 
Итак, мы имеем два темплейта под каждый тип устройства - роутеры и файерволы.

Вот что мы имеем для роутеров. 

Для R1 локальный IP адрес будет заканчиваться на 1. Для нечетных клиентов устанавливаем VRRP приоритет в 200. Для четных - 100.

{% if 'R1' in ROUTER_ID -%}

    {% set LOCAL_RID = 1 -%}

    {% set REMOTE_RID = 2 -%}

    {% if CLIENT_ID | int % 2 == 0 -%}

        {% set PRIORITY = 100 -%}

    {% else -%}

        {% set PRIORITY = 200 -%}

    {% endif -%}


Все зеркально для R2.

{% elif 'R2' in ROUTER_ID -%}

    {% set LOCAL_RID = 2 -%}

    {% set REMOTE_RID = 1 -%}

    {% if CLIENT_ID | int % 2 == 0 -%}

        {% set PRIORITY = 200 -%}

    {% else -%}

        {% set PRIORITY = 100 -%}

    {% endif -%}

{% endif -%}


Далее у нас идет блок конфигурации интерфейсов. Создаем VRF, создаем sub-interface в сторону файервола и засовывам его в VRF, настраиваем клиентский интерфейс в том же VRF, добавляем дефолтный маршрут в файервол в новом VRF. 

[{"op": "set", "path": ["vrf", "name", "VL{{CID}}", "table", "1{{CID}}"]},

{"op": "set", "path": ["interfaces", "ethernet", "eth0", "vif", "{{CID}}", "address", "172.16.{{CID}}.0/31"]},

{"op": "set", "path": ["interfaces", "ethernet", "eth0", "vif", "{{CID}}", "vrf", "VL{{CID}}"]},

{"op": "set", "path": ["interfaces", "ethernet", "{{CLIENT_INT}}", "address", "10.0.{{CID}}.{{LOCAL_RID}}/24"]},

{"op": "set", "path": ["interfaces", "ethernet", "{{CLIENT_INT}}", "description", "VLAN{{CID}}"]},

{"op": "set", "path": ["interfaces", "ethernet", "{{CLIENT_INT}}", "vrf", "VL{{CID}}"]},

{"op": "set", "path": ["protocols", "vrf", "VL{{CID}}", "static", "route", "0.0.0.0/0", "next-hop", "172.16.{{CID}}.1"]},


Для VRRP мастера добавляем transition и health-check скрипты. 

{% if PRIORITY == 200 -%}

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "health-check", "failure-count", "3"]},

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "health-check", "interval", "2"]},

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "health-check", "script", "/config/we-are-isolated.sh"]},

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "transition-script", "backup", "/config/vrrp-goes-backup.sh"]},

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "transition-script", "master", "/config/vrrp-goes-master.sh"]},

{% endif -%}


Ну и последний блок - настройка VRRP.

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "hello-source-address", "10.0.{{CID}}.{{LOCAL_RID}}"]},

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "interface", "{{CLIENT_INT}}"]},

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "peer-address", "10.0.{{CID}}.{{REMOTE_RID}}"]},

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "priority", "{{PRIORITY}}"]},

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "virtual-address", "10.0.{{CID}}.254/24"]},

{"op": "set", "path": ["high-availability", "vrrp", "group", "VLAN{{CID}}", "vrid", "{{CID}}"]}]


Передавая в темлейт словарик ниже мы можем генерировать конфигурации для R1 и R2 с учетом их ролей в VRRP.

    data: 

        ROUTER_ID: R1

        CLIENT_ID: 13

        CLIENT_INT: eth5


Для файерволов имеем похожий теплейт.

Создаем интерфейс и маршрут в новую клиентскую сеть.

[{"op": "set", "path": ["interfaces", "ethernet", "eth1", "vif", "{{CID}}", "address", "172.16.{{CID}}.1/31"]},

{"op": "set", "path": ["protocols", "static", "route", "10.0.{{CID}}.0/24", "next-hop", "172.16.{{CID}}.0"]},


Для доступа в Интернет добавляем соотвествующие правила и добавляем их в нужные зоны.

{%- if INTERNET == True -%}

{"op": "set", "path": ["firewall", "name", "WORLD-TO-VL{{CID}}","default-action", "drop"]},

{"op": "set", "path": ["firewall", "name", "WORLD-TO-VL{{CID}}","rule", "1", "action", "accept"]},

{"op": "set", "path": ["firewall", "name", "WORLD-TO-VL{{CID}}","rule", "1", "state", "established", "enable"]},

{"op": "set", "path": ["firewall", "name", "WORLD-TO-VL{{CID}}","rule", "1", "state", "related", "enable"]},

{"op": "set", "path": ["firewall", "name", "WORLD-TO-VL{{CID}}","rule", "2", "action", "drop"]},

{"op": "set", "path": ["firewall", "name", "WORLD-TO-VL{{CID}}","rule", "2", "state", "invalid", "enable"]},

{"op": "set", "path": ["zone-policy", "zone", "VL{{CID}}", "from", "WORLD", "firewall", "name", "WORLD-TO-VL{{CID}}"]},

{"op": "set", "path": ["zone-policy", "zone", "WORLD", "from", "VL{{CID}}", "firewall", "name", "TO-WORLD"]},

{%- endif %}


Создаем новую клиентскую зону с интерфейсов в ней в любом случае.

{"op": "set", "path": ["zone-policy", "zone", "VL{{CID}}", "default-action", "drop"]},

{"op": "set", "path": ["zone-policy", "zone", "VL{{CID}}", "description", "VL{{CID}}"]},

{"op": "set", "path": ["zone-policy", "zone", "VL{{CID}}", "interface", "eth1.{{CID}}"]}]


Для генерации конфигурации файерволов словарик еще меньше.

    data:

        CLIENT_ID: 13

        INTERNET: True


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

[root@localhost vyos_api_example]# cat deployments/deploy_new_client.yaml 

---

R1: 

    template: new_client_rtr.j2

    data: 

        ROUTER_ID: R1

        CLIENT_ID: 13

        CLIENT_INT: eth5

R2:

    template: new_client_rtr.j2

    data:

        ROUTER_ID: R2

        CLIENT_ID: 13

        CLIENT_INT: eth5 

F1: 

    template: new_client_fw.j2

    data:

        CLIENT_ID: 13

        INTERNET: True

F2:

    template: new_client_fw.j2

    data:

        CLIENT_ID: 13

        INTERNET: True


Python часть очень проста и по сути даже не нуждается в рассмотрении. Используем jinja для геренации конфига, requests для отправки конфига и click для аргументов. Код разбит на две части - всякие функции и сам скрипт. Предлагаю взглянуть на них самостоятельно. 

Единственный момент, VyOS использует только POST метод и Content-Type multipart/form-data. Грубо говоря, JSON данные и API ключ мы должны передавать как-будто бы заполняя форму. В связи с этим приходится преобразовывать данные. 

Нам нужно передать в files аргумент requests словарик в определенном формате {'data':(None,DATA),{'key':(None,KEY)} Это не очень удобно.

Поэтому имеем отдельную функцию, которая делает словарик в нужном формате. 

def prep_config(generated_config, api_key):

    '''

    Creating a dictionary which can be accepted by VyOS

    {'data':(None,DATA),{'key':(None,KEY)}

    '''

    to_push = {}

    to_push['data'] = (None, generated_config.replace('\n',''))

    to_push['key'] = (None, api_key)

    return(to_push)


def post_config(target, to_push):

    '''

    Posting data to the target

    '''

    url = 'https://' + target + '/configure'

    r = requests.post(url, files=to_push, verify=False)

    return(r)

Запуск

Собственно, пора запустить скрипт. Для этого нужно передать в него инвенторный yaml, файлик с deployment (какие темплейты куда заливать) и имя API ключа. 

Как видно ниже, конфигурация добавилась на все 4 устройства. 

[root@localhost vyos_api_example]# python3 vyos_deploy.py -i inventory.yaml -d deploy_new_client.yaml -a API_KEY

API key gathered

Inventory file "inventory.yaml" parsed.

Deployment file "deploy_new_client.yaml" parsed.

Config for "R1" generated.

Config prepared.

Pushing config to "R1"

Returned result is "{"success": true, "data": null, "error": null}"

Saving configuration...

Returned result is "{"success": true, "data": "Saving configuration to '/config/config.boot'...\nDone\n", "error": null}"


Config for "R2" generated.

Config prepared.

Pushing config to "R2"

Returned result is "{"success": true, "data": null, "error": null}"

Saving configuration...

Returned result is "{"success": true, "data": "Saving configuration to '/config/config.boot'...\nDone\n", "error": null}"


Config for "F1" generated.

Config prepared.

Pushing config to "F1"

Returned result is "{"success": true, "data": null, "error": null}"

Saving configuration...

Returned result is "{"success": true, "data": "Saving configuration to '/config/config.boot'...\nDone\n", "error": null}"


Config for "F2" generated.

Config prepared.

Pushing config to "F2"

Returned result is "{"success": true, "data": null, "error": null}"

Saving configuration...

Returned result is "{"success": true, "data": "Saving configuration to '/config/config.boot'...\nDone\n", "error": null}"

[root@localhost vyos_api_example]# 


Добавляем клиента, пингуем "что-то в интернете"... и вуаля.


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

Комментариев нет:

Отправить комментарий