Приветствую, друг!
Я очень хотел быть последователен в своих планах касаемо статей по изучению автоматизации, но пока получается только с циклом ENCOR (по STP и Aggregation уже почти дописано!).

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

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

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

Статью с переводом (Network Automation 101) считаю основной т.к после ее прочтения складывается достаточно ясный набросок, что сейчас есть в мире автоматизации. Она от 2020 года и уже, однозначно, не исчерпывающая.

Если вы обладаете опытом и знаете, что стоило бы добавить, вы можете помочь автору сделать ее лучше! Статья опубликована в блоге и выложена на github.

Из идеи о публикации заметок по курсу Наташи Самойленко, Python для сетевых инженеров я успел довести до ума только самую первую. Планирую продолжить т.к все еще делаю задания (уже добил 18 раздел!):

Это все, что есть. В загашнике еще достаточно черновиков и идей. В этот раз мы базово разберемся с API, как с этим работать и как применять, на примере API Fortigate.

Задача: Есть некоторые количество FW Fortigate, необходимо автоматизировать бэкап конфигурации средствами API. Файл конфигурации будем класть в форматеip_Y_M_D.conf в отдельную папку, с наименованием backup_Y_M_D. (Y,M,D - год,месяц,день соответственно)

Нам понадобится:

  • ubuntu 20.04/macos/Windows с WSL
  • tmux/nvim/любой другой редактор/IDE
  • python 3.10
  • пара коробок с FortiOS 7.0.1-9

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

Оглавление

Про API, в контексте полезности для работы сетевика, хорошо написал Марат aka eucariot, поэтому не вижу смысла повторяться, бегом читать одну из частей АДСМ, которая целиком про REST API, на примере API Netbox.

Чтобы не сложилось ощущения: “и зачем читать статью дальше, ведь я не в курсе что такое API” рассмотрим ключевые аспекты, необходимые в задаче.

В конечном итоге, у нас будет рабочий скрипт на питоне, который по API дергает n-количество fortigate вне зависимости от модели, и делает следующее:

  • вызов скрипта через терминал с параметрами
  • функция в скрипте принимает на вход файл .yaml с хостами и токенами.
    • проверка на наличие необходимых ключей в файле
  • скрипт по API забирает с каждого устройства full-config
    • перехватывает наиболее возможные HTTP исключения
  • есть возможность указать директорию для бэкапа в параметре
    • создает директорию в формате backup_Y_M_D, с правами 700
    • создает внутри файл в формате ip_Y_M_D.conf
    • записывает в него конфиг
    • показывает косметические сообщения, какое устройство в данный момент обрабатывается и выполнен ли бэкап

Возможно, такая задача уже была решена (наверняка была), но это отличная задача, чтобы попробовать что-то автоматизировать и немножко прокачать скилл. Не config команда, но уже и не простой show. Поехали.

Как я перестал бояться API

Для меня API всегда был чем-то недостижимым. Серьезно. Я неплохо наловчился общаться с CLI напрямую, а не так давно заново открыл для себя этот способ взаимодействия, благодаря курсам Наташи. Имеется ввиду взаимодействие с CLI в контексте автоматизации через netmiko/ansible/nornir.

Тогда я уже знал что такое API и для чего оно нужно. Что к ним существует документация, где все описано и т.п. Но каждый раз я откладывал знакомство, оправдывая это недостаточными знаниями в программировании. Я ошибался.

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

Иными словами, будем разбираться с темой по бразильской системе. С чего начать? Этот вопрос всплывает каждый раз, когда садишься писать код не имея достаточной практики.

Раз у нас задача по Fortigate и мы знаем, что есть Fortinet API - нам нужно узнать, как работать с его API. И уже по ходу разберемся подробнее с терминологией.

Базовые концепции API

Открываем документ под названием Technical Tip: About REST API.
Вопросы начинаются сразу с названия “About REST API” что такое REST API?

REST (REpresentational State Transfer) - набор архитектурных принципов построения сервис-ориентированных систем.

API - набор инструментов, позволяющий общаться с устройством посредством запросов (request). Я покажу это на примере известного инструмента - Postman, а затем плавно перейдем в python, где нам понадобится библиотека requests.

Система, удовлетворяющая принципам REST, называется RESTful API. Другими словами, это интерфейс взаимодействия с системой, основанный на принципах REST.

Кроме REST есть еще SOAP/RPC/GraphQL, которые имеют свои плюсы и минусы. Но вне зависимости от вида, API работают по принципу request/response. Мы запрашиваем (совершая вызов API) информацию у сервера, а сервер нам отвечает данными.

Request - содержит внутри себя данные, касающиеся запроса API, такие как: base URL, endpoint, method, parameters, headers и т.п. Response - содержит в себе запрошенные данные, статус ответа и заголовки.

Чтобы обращаться к устройству посредством API, нам нужно иметь представление о методах HTTP т.к HTTP выступает в виде транспорта для REST API.

API Fortigate поддерживает 4 метода:

HTTP метод Описание
GET Запросить ресурс/набор ресурсов
POST Создать ресурс или выполнить действия
PUT Обновить ресурс
DELETE Удалить ресурс/набор ресурсов

Для скрипта нам понадобятся только GET.

Так же, чтобы понимать успешность запроса, нужно иметь представление про status code.

Их достаточно много и каждый сообщает о чем-то конкретном, вот несколько из них:

Status code Описание
200 OK Ваш запрос выполнен успешно!
201 Created Ваш запрос принят и ресурс создан.
400 Bad Request Ваш запрос либо неверен, либо отсутствует какая-либо информация.
401 Unauthorized Ваш запрос требует некоторых дополнительных разрешений.
404 Not Found Запрошенный ресурс не существует.
405 Method Not Allowed Конечная точка не позволяет использовать этот конкретный метод HTTP.
500 Internal Server Error Вероятно, что-то сломалось на стороне сервера.

Их делят на следующие группы:

  • Informational responses (100 – 199)
  • Successful responses (200 – 299)
  • Redirection messages (300 – 399)
  • Client error responses (400 – 499)
  • Server error responses (500 – 599)

В офф. документации можно почитать подробнее.

Сейчас нам важно понимать, что status code - 200 OK говорит о том, что запрос был успешен.

Автоботы, Аутентифицируемся!

Чтобы работать с API, нужно подтвердить свою возможность с ним работать. Т.е предоставить учетные данные в каком-то виде.

Есть, в том числе, публичные API, например NASA API, где, среди прочего, есть API APOD (Astronomy Picture of the Day), которое возвращает Астрономическое изображение дня :) Для работы с ним достаточно получить API-токен в форме регистрации на сайте.

Вернемся к нашим баранам, к документации. Fortinet предлагают два способа аутентифицироваться.

  • Session-based authentication
  • Token-based authentication

Session-based authentication нам не подходит т.к он основан на аутентификации по логину. Если по простому, вы можете сделать запрос только если залогинились на fortigate и успешно получили куки сеанса и csrf токен пользователя.

В аутентификации на основе сессий, мы вынуждены сначала инициировать подключение посредством POST-запросаlogincheck и затем хранить валидные куки и csrf-токен текущего сеанса. Выполнение этих требования позволит нам обращаться к ресурсам FW. Как только сеанс завершается, доступ по API на устройство закрывается.

Это не очень удобно для написания автоматизации (до кучи, метод уже считается legacy), поэтому мы будем использовать метод на основе API-токена.

Пример простого GET запроса на основе сессии:

http://<Fortigate-IP>/api/v2/cmdb/firewall/address

Token-based authentication - этот способ избавляет нас от необходимости ввода логина/пароля и отправлять их через HTTP, хранить куки, парсить уникальный csrf. Вместо этого, создается администратор API и постоянный ключ - API токен.

Создаем API-токен
Копируем API-токен

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

Пример простого GET запроса на основе API токена:

http://<Fortigate-IP>/api/v2/cmdb/firewall/address?access_token=<access-token>

Токен должен быть в каждом запросе т.к у нас теперь нет конкретной сессии с вводом учетных данных.

Итак, резюмируем, чтобы начать работать с API Fortinet, нам нужно:

  • создаем администратора API в GUI Fortigate
  • копируем в блокнот значение API-токена

API Fortinet

Прежде чем начать, немного инфы про API Fortigate.

  1. Fortinet прячет API от общего доступа за developer аккаунтом. Попасть в него можно после регистрации, в процессе которой вы укажете два аккаунта “спонсора” (в виде почтовых адресов @fortinet.com), кто сможет подтвердить, что вам это действительно надо. У меня таких контактов нет :)
  2. Местами API очень нелогичное. Например можно встретить дополнительные . в ресурсе, что выглядит не совсем логично. Пример: cmdb/log.syslogd/setting - посмотреть настройки syslog и более логичный cmdb/firewall/policy - посмотреть список политик.
  3. Основная проблема, о которой надо помнить всегда, нет процедуры pre-config, т.е все изменения сразу попадают в конфиг и применяются. Отправили вы POST с созданием интерфейса и он сразу же будет создан и доступен.

Первый пункт решается просто, находим +- актуальный документ API Reference в сети и изучаем (если таковые есть). Самое свежее, что удалось найти: FortiOS 6.0.6 API Ref.

В крайнем случае, мы можем пострадать реверс-инженирингом используя в браузере Inspect, который находится в инструментах разработчика. Для примера, попробуем найти API запрос на бэкап конфига.
GUI backup

Делаем self-бэкап и смотрим в Inspect->Network. Среди приличного количества запросов, находим backup?...
GUI backup отправка запроса

Request URL в секции General и есть наш API запрос, точнее это то, что произошло, когда мы нажали кнопку OK.

http://<Fortigate-IP>/api/v2/monitor/system/config/backup?destination=file&scope=global

Посмотрев на адрес, мы можем понять что есть что. Например/api/v2/monitor/ - это наш base URL. Технически, это ничем не отличается от обычного URL типа google.com только в этом случае тут есть ключевое слово api и чтобы увидеть то, что скрыто за этим адресом, нужны учетные данные. Например отправив GET на https://api.github.com мы увидим список ресурсов, доступных для работы по API.

Base URL нам не даст каких либо полезных данных, поэтому мы обратимся к Endpoints, вторая половина адреса, которая, при наличии хорошей документации, подробно описана.

Например, в нашем запросе эндпоинтом будет считаться system/config/backup, который, к тому же, можно найти в документации:
API ref документация

У эндпоинтов есть свои обязательные и опциональные параметры, опять же, в нашем примере параметры это ?destination=file&scope=global которые так же описаны в документации:
API ref подробнее

  • Обязательный scope, описан в запросе как key=value scope=global
  • Опциональный destination, так же в формате key=value - destination=file

Символ ? в адресе URL обозначает начало строки параметров (query string), которые используются для передачи дополнительной информации на сервер. После которого передаются параметры в виде пары key=value и разделенных символом &.

Попробуем выполнить наш запрос на бэкап в Postman.

Запрос в Postman

Качаем софтину с офф. сайта и устанавливаем. Аккаунт создавать не обязательно.
Установка Postman

Создадим коллекцию Fortigate_API:
Создаем коллекцию

Затем создадим новый HTTP-request:
Создаем запрос

В открывшееся поле запроса вставляем наш запрос на бэкап конфига и API-токен, который мы скопировали ранее и выбираем метод GET.

Чтобы не добавлять токен в запрос, его можно добавить в Headers в поле значение (value) Bearer <API токен>, и Authorization в поле ключ (key). Правим Headers

Тыцаем Send и если Status: 200 OK, то мы получаем нашу конфигурацию:
Запрос 200 OK

Важно: В конфиге не будет куска, созданного пользователем с профилем c более высокими правами (например super-admin) т.к API пользователь (по умолчанию) имеет только READ права.

Варианта решения два:

  1. Создать отдельный профиль с полными правами на нужную ветку и выдать этот профиль API пользователю
  2. Выдать API пользователю профиль super-admin, используя консоль. т.к в GUI это сделать не получится.
config system api-user
 edit <ваш api-user>
 set accprofile super-admin
 end

Запрос сделали, вывод получили, доступ по API проверили, теперь перейдем к реализации на Python.

Пишем скрипт на Python

Повторюсь, самое сложное в этой задаче, если вы никогда такого не делали - начать.

В этом нам поможет Postman. Возьмем уже созданный запрос и откроем Code Snippet, где в списке можно найти Python - Requests. ![[Pasted image 20230321143424.png]]

В коде используется библиотека - requests. Установить ее можно с помощью следующей команды:

python -m -pip install requests

Если вы скопируете и вставите этот код в файл с расширением .py и затем выполните его, то получите точно такой же ответ в виде конфига, который видели в Postman.

Стоит отметить, что в скрипте будет больше от Python, чем от API. За работу с API будет отвечать только следующая часть кода:

import requests

url = "http://<Fortigate-IP>/api/v2/monitor/system/config/backup?destination=file&scope=global"

payload={}
headers = {
  'Authorization': 'Bearer <ваш API token'
}

response = requests.request("GET", url, headers=headers, data=payload)

print(response.text)

.text- возвращает содержимое ответа в формате Unicode. .content- возвращает содержимое ответа в байтах.

Первое, с чем нужно разобраться: Как унифицировать скрипт для использования на нескольких устройствах?

  1. Уникальные API-токены
  2. Уникальные IP адреса

Код будем писать в рамках функции fortios_backup_config. В функции будет два основных аргумента: device и token, который она будет получать на вход из файла в формате .yaml в следующем виде:

# fortigate_# 1
- host: X.X.X.X
  token: '<Ваш API-токен #1>'
# fortigate_# 2
- host: Y.Y.Y.Y
  token: '<Ваш API-токен #2>'
# Fortigate # n

Я не сообразил ничего лучше yaml файла, если есть способ лучше, можно смело предлагать в комментариях

В скрипте будем считывать этой файл с помощью модуля yaml, который будет переводить содержимое в понятный для python вид - список словарей:

[{'host': 'X.X.X.X', 'token': '<Ваш API-токен #1>'}, {'host': 'Y.Y.Y.Y', 'token': '<Ваш API-токен #2>'}]

И два опциональных ключевых аргумента:enable_warning и enable_ssl. enable_ssl нужен для удобной манипуляции (вкл/выкл) с проверкой SSL т.к я недостаточно хорош в SSL и не крутил его на своих фортиках. Это небезопасно, поэтому добавлена возможность включить его обратно с помощью передачи аргумента в функцию.
enable_warning больше косметический, по умолчанию данный параметр отключает вывод сообщения, которое выводится для каждого хоста, если отключена проверка SSL:

InsecureRequestWarning: Unverified HTTPS request is being made to host

Было бы хорошо вызывать функцию из консоли, с возможностью передачи в нее аргументов. Для этого будем использовать модуль argparse.

Сразу определим для него отдельную функцию parse_cmd_args:

def parse_cmd_args():
    # Создаем парсер аргументов командной строки
    parser = argparse.ArgumentParser()

    # Принимаем аргументы от пользователя из командной строки
    parser.add_argument(
        "--config-file", "-C",
        type=str, required=True,
        help="Путь к файлу конфигурации"
    )
    parser.add_argument(
        "--backup-dir", "-D",
        type=str,
        default="/home/aik/manage-tools/output/backups/fortigate/",
        help="Директория для сохранения резервных копий",
    )
    parser.add_argument(
        "--enable-warning", "-W",
        action="store_false",
        help="Включить предупреждения безопасности SSL",
    )
    parser.add_argument(
        "--enable-ssl", "-S",
        action="store_true",
        help="Включить проверку SSL"
    )
    return parser.parse_args()

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

Будем придерживаться правила “функция должна выполнять конкретную задачу”. Если разобраться, то дальше у нас будет две основных задачи, прежде чем запрашивать конфиг:

  • прочитать файл .yaml
  • разобрать файл, принять аргументы от parse_cmd_args и отправить все в основную функцию fortios_backup_config

Соберем из этого соответствующие функции:

  1. Функция для чтения файла конфигурации устройств и загрузки его в переменную.
    def read_devices(config_file):
     with open(config_file) as f:
     devices = yaml.safe_load(f)
     return devices
    

В with open() мы передаем обязательный аргумент --config-file описанный в функции parse_cmd_args выше и имеющий свойство required=True, что делает его обязательным. В нем мы передаем файл .yaml в формате, описанном выше. Передать его можно с коротким ключом -C.

  1. Функция для обработки каждого устройства из списка devices.
    def process_device(device, backup_dir, enable_warning, enable_ssl):
     if "host" not in device or "token" not in device:
         logging.error(f"Отсутствуют обязательные параметры 'host' или 'token' для устройства {device}")
         return
     print(f"{'#'*20} Подключаюсь к {device['host']} {'#'*20}")
     fortios_backup_config(
         device["host"],
         device["token"],
         backup_dir=backup_dir,
         enable_warning=enable_warning,
         enable_ssl=enable_ssl,
     )
    

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

Вdef(main) - это наша главная функция скрипта, задействуем все наши функции:

def main():
    args = parse_cmd_args()
    devices = read_devices(args.config_file)
    for device in devices:
        process_device(device, args.backup_dir, args.enable_warning, args.enable_ssl)

Внутри мы создаем переменную args где будут храниться наши аргументы и кладем в нее вызов функции parse_cmd_args().

И воспользуемся циклом for, чтобы перебрать полученные адреса хостов, для выполнения кода на каждом устройстве.

В конструкции if __name__ == "__main__" просто укажем main(). Во-первых, чтобы не городить забор, во-вторых, если функция будет передаваться в виде модуля, не потерялась переменная args т.к то, что внутри if __name__ == "__main__" будет проигнорировано. Именно для этого она и используется.

Теперь переходим внутрь основной функции fortios_backup_config и пойдем в ней по порядку.

Сперва унифицируем запрос на прием токена, передав аргумент функции сразу в headers, используя f-строку:

headers = {"Authorization": f"Bearer {token}"}

Теперь соберем запрос под конкретное устройство, используя тот же способ, вместо конкретного IP-адреса подставим переменную из аргумента функции:

url = f"https://{device}/api/v2/monitor/system/config/backup?destination=file&scope=global"

f-строки крайне удобный инструмент, позволяющий “вклинить” переменную в строку, с помощью конструкции {var}.

Основной код завернут в конструкцию try-except, чтобы перехватить очевидные исключения и не прерывать выполнение кода для других хостов в списке:

try:
    # Отправка запроса и обработка исключений
	response = requests.get(
		url, headers=headers, data=payload, verify=enable_ssl, timeout=10
	)
	response.raise_for_status()
except requests.exceptions.Timeout:
	logger.exception(f"Таймаут подключения, проверь доступность {device}.")
	return
except requests.exceptions.HTTPError as err:
	logger.exception(f"HTTP ошибка: {url}, гугли по следующему коду ответа: {err.response.status_code}")
	return
except requests.exceptions.RequestException as err:
	logger.exception(f"Что-то пошло не так, необходимо дополнительно разобраться, вот код ошибки: {err}")
	return

В методе request используем свойство verify, но в значение подставляем ключевой аргумент функции, чтобы влиять на проверку SSL verify=enable_ssl. По умолчанию False.

Если у вас, как и у меня, self-сертификаты на Frotigate, то будет следующая ошибка, которая прервет выполнение кода:

requests.exceptions.SSLError: HTTPSConnectionPool(host='<host_ip>', port=443): Max retries exceeded with url: /api/v2/monitor/system/config/backup?destination=file&scope=global (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate

Теперь разберемся, куда и как будем сохранять конфиг. Но для начала, его надо получить.

Если все в порядке и код ответа 200, то сохраняем результат работы requests в переменнуюfull_config. В ней будет сохранен вывод из переменной response со свойством text, чтобы вывод был сразу в unicode-формате:

# Получение полной конфигурации и создание директории и файла бэкапа
full_config = response.text
current_time = datetime.datetime.today().strftime("%Y_%b_%d")
backup_path = os.path.join(backup_dir, f"backup_{current_time}")
os.makedirs(backup_path, exist_ok=True, mode=0o700)
backup_file_path = os.path.join(backup_path, f"{device}_{current_time}.conf")

С помощью модуля datetime в переменную current_time соберем текущее время, которое используем в названии папки и файла.

Далее, с помощью модуля os соберем путь, куда будут положены файлы с бэкапом и то же самое сделаем для файла.

Это еще один обязательный параметр, который можно задать с коротким ключом -D. Так мы передаем директорию, куда будет сохранена директория с бэкапами, для backup_dir.

С помощью os.makedirs создадим директорию и используем параметр exist_ok = True, который проверяет, существует ли такая директория, прежде чем ее создавать.

Свойствоmode=0o700 в os.makedirs- создает директорию backup_dir с правами 700.

Дальше просто записываем в собранный ранееbackup_file_path наш конфиг, который сохранен в переменной full_config.

with open(backup_file_path, "w") as f:
	f.write(full_config)
	print(f"{'#'*20} Backup выполнен {'#'*20}")

Все модули, с которыми мы имели дело в коде, импортируются через import. Ниже показаны все модули, использованные в коде:

import requests # для взаимодействия с API
import yaml # для работы с YAML файлами
import datetime # манипуляции с дата/время
import os # взаимодействие с файловой системой
import urllib3 # обработка HTTP запросов
import logging # работа с ошибками/исключениями
import argparse # передача аргументов в функцию из CMD

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

С разбором кода закончили, теперь пара слов, как это работает.

Копируем скрипт и файл .yaml в удобное место, правим .yaml в соответствии с вашими устройствами:

  • host (в формате IP-адреса)
  • token (копируем значение вашего API-токена)

Скрипт имеет справку, которую можно вызывать ключом -h:
Справка по скрипту

Параметр --config-file и --backup-dir обязателен, остальное опционально.

Пример вызова функции из терминала:

python fortios_backup_config.py -C ../devices_api.yaml -D ~/manage-tools/output/backups/

Успешный вывод будет выглядеть следующим образом:
Работа скрипта

После завершшения работы скрипта, у нас есть директория по указанному пути с правами 700 и файлами бэкапа внутри:
Итоговая директория
Итоговые файлы бэкапа

На этом все. Спасибо, что дочитали до конца. Если остались вопросы, пишите в комментарии под постом в Telegram. Попробую помочь, чем смогу. Это поможет улучшить будущие статьи по теме автоматизации.

Что почитать для закрепления:


Хочешь обсудить тему?

С вопросами, комментариями и/или замечаниями, приходи в чат или подписывайся на канал.