Задумывались ли вы о том, как изменить конфигурацию сразу на нескольких сетевых устройствах? Что, если нужно сделать это на всей сети с сотнями и тысячами единиц оборудования? А что, если приходится делать это каждый месяц на железе от пяти разных производителей? Очевидное решение для подобных задач — автоматизация. Но реализовать её можно не одним способом, а в процессе наткнуться не на одни грабли.

Меня зовут Вадим Воловик, и я руковожу проектами разработки в Yandex Infrastructure. Наша команда NOCDEV отвечает за автоматизацию сетей всего Яндекса. Давно хотелось рассказать о задачах такого масштаба, но по ходу написания материала стало понятно, что тема тянет на целый цикл. Так что мы с коллегами расскажем о самых интересных примерах автоматизации в отдельных постах.

В этой статье проведём небольшую экскурсию по нашему сетевому «хозяйству» в десятки тысяч устройств и остановимся подробнее на том, как при таком объёме мы автоматически обновляем конфигурации. Будет интересно не только сетевым инженерам, но и бэкенд‑ и фронтенд‑разработчикам, которые занимаются автоматизацией работы с самым разным оборудованием.

О нашей сети

В настоящее время наша сеть представляет собой совокупность дата‑центровых, магистральных и офисных сетей (включая склады и лавки). Так как сервисы Яндекса георезервируются, то и сеть у нас геораспределённая.

Основными местами расположения наших сетевых устройств являются дата‑центры. Каждый дата‑центр — это десятки тысяч серверов и тысячи L3-коммутаторов. Дата‑центры взаимодействуют через резервируемую магистральную сеть хай‑фай‑класса.

В дата-центрах есть специальная сеть управления (Management Network), включающая консольные серверы и IPMI-коммутаторы.

Отдельно достойна упоминания офисная сеть с её тысячами WiFi‑точек доступа, L2-коммутаторами, системами видеоконференцсвязи и SIP‑телефонией.

Суммарно команда эксплуатации сетей, или Network Operations Center (NOC) Яндекса обслуживает более 20 000 устройств. Используется практически весь стек сетевых технологий:

  • от L2 VLAN, до L2 EVPN VXLAN;

  • IPv6 внутри дата‑центра, MPLS SRv4 + SDN между дата‑центрами;

  • OpenVPN для доступа к офисной сети, IPSec‑туннели для связей между офисами.

Сетевой центр в одном из ДЦ
Сетевой центр в одном из ДЦ

Команды NOC каждый день сталкиваются с задачами масштабирования и поддержания сети в рабочем состоянии, периодически устраняя аварии на сети. Поэтому бок о бок с ними трудится команда NOCDEV — профессиональных программистов, разрабатывающих собственный стек автоматизации сетевой инфраструктуры. Совместными усилиями направлений NOC и NOCDEV решаются задачи ввода оборудования в эксплуатацию, обновления программного обеспечения, деплоя конфигурации, real‑time‑мониторинга и другие. Всего у нас более 20 сервисов, а технологический стек выглядит следующим образом:

Языки программирования:
- Golang
- Python
- PHP
- Ruby

OS:
- Linux
- FreeBSD

Databases:
- PostgreSQL
- MySQL
- MongoDB
- YTsaurus
- YDB

API:
- RESTful
- GraphQL
- gRPC

Containerization:
- K8S
- LXC
- Docker
- Jail

Frontend:
- React
- Typescript
- SCSS

Так что если вы умеете писать код и интересуетесь инфраструктурными DevOps/NetOps‑задачами, то в этой статье для вас найдётся много любопытного.

А если хочется больше почитать о том, с чем ещё работает NOC/NOCDEV, то также рекомендую статьи от @eucariot из цикла АДСМ:

Поговорим об архитектуре

На рисунке ниже вы видите нашу высокоуровневую архитектуру автоматизации. Она не сильно отличается от других проектов сетевой автоматизации (например, можно посмотреть на ONAP).

Система автоматизации сетей состоит из следующих основных компонентов:

  • Users — вся автоматизация делается для наших пользователей (сетевых и не только инженеров и разработчиков).

  • User Interfaces — этот уровень состоит из Web‑интерфейсов, CLI, TF‑провайдеров, API и не нуждается в особом представлении.

  • Inventory — система инвентаризации аппаратного и программного обеспечения.

  • Automation — именно здесь работают модули для автоматизированного распространения настроек на сетевое оборудование.

  • Monitoring — набор модулей, позволяющих отслеживать состояние сети в различные моменты времени.

  • Executors — этот уровень представлен разнообразными библиотеками и фреймворками общения с сетевым оборудованием.

  • Network — и, конечно же, всё это опирается на сети и немного на облака.

NOCDEV Яндекса разрабатывает и поддерживает все перечисленные компоненты автоматизации.

Users

Untitled

Если вы думаете, что нашими пользователями являются только сетевые инженеры, то ошибаетесь. Сеть — это один из важнейших инфраструктурных компонентов любой IT‑компании. Соответственно, напрямую или косвенно пользователями являются все сотрудники и все пользователи сервисов компании.

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

User Interfaces

Так как основной возраст пользователей до 35 лет, то важно создавать современные Web‑интерфейсы для решений. Мы активно вкладываемся в UI/UX наших сервисов, так что в команде всегда найдётся место для фронтенд‑разработчиков и особенно для дизайнеров.

Также мы понимаем, что зачастую наши пользователи — бэкенд‑разработчики других команд и сервисов, так что мы предоставляем API (REST, gRPC, GraphQL) и Terraform‑провайдеры в качестве пользовательских интерфейсов.

Inventory

Автоматизация начинается с базы — с Inventory, которая хранит в себе такие данные о ресурсах сети, как:

  • локацию и месторасположение оборудования;

  • производителя (вендора) и модель оборудования;

  • разметку устройств по их свойствам и ролям;

  • информацию, как элементы сети связаны друг с другом;

  • атрибуты сетевых элементов: FQDN, Serial Number и прочее;

  • текущую версию ПО;

  • информацию об адресном пространстве, L2-пространстве и других ресурсах сети;

  • и ещё сотни атрибутов.

Система Inventory не является одним монолитом. Она состоит из нескольких сервисов, объединённых API Gateway. К ним мы относим:

  • Racktables (опенсорс) для хранения общих данных об оборудовании и адресном пространстве.

  • Источник истины о программном обеспечении сетевого оборудования.

  • Источник истины о сетевой связности (устройство‑устройство, устройство‑сервер).

  • Источник истины о внешней BGP‑связности.

  • Источник истины о SIP‑телефонии.

  • Inventory API.

К Inventory‑системе предъявляются достаточно жёсткие требования по отказоустойчивости, доступности и скорости работы API. Именно поэтому мы используем в этом направлении как современные, так и проверенные временем решения. Например, мы широко применяем технологию GraphQL, а в качестве API Gateway используем GraphQL Router. Для тестирования изменений у нас есть специально разработанный Inventory Staging, который поднимает Docker‑контейнер, где разработчик может безопасно для прода тестировать изменения.

Automation & Executors

В Inventory хранится информация именно о целевом состоянии сети. Когда она достаточно полна, то с использованием систем автоматизации можно привести состояние сети к целевому. Наш стек автоматизации состоит из нескольких сервисов:

  • Оркестратор пользовательских сценариев.

  • Сервис расчёта разницы между целевым состоянием сети и текущим.

  • Фреймворк генерации конфигурации оборудования (annet).

  • Сервис хранения текущей конфигурации устройства.

  • Сервис ввода устройства в эксплуатацию.

Благодаря этому стеку мы можем позволить себе еженедельно производить десятки тысяч операций на сетевом оборудовании. Служба эксплуатации сети, независимо от разработчиков, может реализовать разнообразные сценарии обслуживания сети. Наш стек чем‑то напоминает Ansible, Temporal, Apache AirFlow, но с Яндексовым шармом и является собственной разработкой.

Почему мы идём тут своим путем? Наш коллега Дмитрий Липин высказывался на эту тему в статье на примере RTC (Runtime Cloud). При наличии такого «зоопарка» оборудования и разнообразных сценариев мы не можем себе позволить блокироваться о сторонние решения. Наш цикл автоматизации зависит только от новых моделей и софта производителей сетевого оборудования.

Также выбор собственной реализации системы для выкатки конфигурации вызван тем, что производители оборудования не нацелены на унифицированный подход к конфигурации сетевых устройств. Даже внутри одной линейки оборудования H‑вендора можно встретить пять реализаций языка взаимодействия с оборудованием. При этом выкатка конфигурации по своей сути напоминает процесс деплоя кода: драфт конфигурации, ревью, автотесты, выкатка с возможностью rollback. В индустрии принято самостоятельно реализовывать инструменты общения с оборудованием.

Также мы активно работаем над сервисом‑абстракцией над сетевым оборудованием. Цель проста — свести язык общения со всем разнообразием нашего сетевого оборудования к одному (об этом мы тоже обязательно расскажем когда‑нибудь).

Monitoring

После того как конфигурация применена, хочется понять всё ли ок с оборудованием, а если что‑то не получилось — узнать, что именно. Для этого мы адаптировали сервисы мониторинга, позволяющие в реальном времени смотреть, что происходит с сетью.

Стек мониторинга сети сталкивается с задачами быстрого опроса десятков тысяч устройств (буквально за минуты). Поэтому активно применяются технологии высоконагруженных сервисов. Используются разные протоколы взаимодействия с оборудованием: CLI, SNMP, Netconf, gRPC.

Сервисы мониторинга представлены сборщиками данных, фреймворком регистрации аварий на сети.

Мониторинг — глаза эксплуатации сети, сервисы автоматизации — руки, а системы Inventory — сердце. Важно, чтобы всё работало синхронно, и NOCDEV способствует гармоничному взаимодействию всех частей.

Легенда

Портреты Милоша сгенерированы в Шедевруме, так что любые совпадения случайны
Портреты Милоша сгенерированы в Шедевруме, так что любые совпадения случайны

Быстро сказка сказывается, да не скоро дело делается.

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

И в первой вводной задаче приступим к рассмотрению задачи массовой конфигурации оборудования — одной из ключевых задач автоматизации сети.

О задаче массового обновления конфигураций

Условия задачи. Сетевому инженеру Милошу необходимо актуализировать подписи интерфейса (description) в формате {REMOTE_HOSTNAME}_{REMOTE_PORTNAME} на всех клиентских и сетевых портах.

Вопрос. За сколько дней Милош справится с актуализацией 20 000 устройств на сети, состоящей из коммутаторов и устройств Cisco, Huawei? При том, что одна операция актуализации порта занимает в среднем 5 минут на одно устройство.

Решение 1. Ручное

Как кажется, самое простое решение — вручную изменять конфигурацию на устройствах.

Плюсы — низкий порог входа для сетевого инженера.

Минусы — очень долго. Для 20 000 устройств потребуется примерно 500 человеко‑дней. Не помешает сотня‑другая Милошей.
Есть подводные камни: неизбежны ошибки.

Вывод: смотрим другие варианты.

Решение 2. Автоматизация

git clone https://github.com/vadvolo/milos-automation/tree/main

Достаточно очевидным решением этой задачи становится автоматизация процесса, которая состоит из следующих шагов:

  1. Подключение к устройству (Open Session).

  2. Сбор информации о интерфейсах устройства (Send Command).

  3. Парсинг данных (Parse Data).

  4. Генерация конфигурации (Configuration Generation).

  5. Деплой конфигурации (Configuration Deploy).

Шаг 1. Подключение к устройству

Для того чтобы подключиться к устройству, необходимо знать IP‑адрес управления, логин, пароль (или ключик) и протокол подключения. Также стоит выяснить, с устройством какого производителя (Vendor) и какой моделью (Model) мы имеем дело. Vendor и Model полезно знать, чтобы заранее понимать, на каком «языке» нужно общаться с оборудованием.

Обычно в арсенале сетевого инженера имеется система инвентаризации, хранящая информацию о устройстве. Предположим, что в компании Милоша существует такая система, например, NetBox, Racktables, или NOC Project. Знания, которые хранятся в Inventory, позволяют Милошу автоматизировать чтение и запись данных на устройство.

В решении данной задачи мы пока что пропустим момент с системой инвентаризации и предположим, что она отдаёт данные в следующем формате:

[
    {
        "hostname": "switch01",
        "vendor": "Cisco",
        "breed": "ios",
        "address": "10.0.10.2",
        "login": "milos",
        "password": "supermilos",
        "interfaces": []
    }
]

При решении данной задачи в роли Inventory‑системы будут выступать файлы. В примере мы будем использовать:

  • importInventory.json для импорта данных о сетевых устройствах

  • exportInventory.cvs для экспорта данных о сетевых устройствах

Для последующих статей нам будет важно опираться на определённый формат системы инвентаризации, поэтому в качестве формата возьмём формат Netbox со следующими полями:

Полный список полей
Name,
Status,
Tenant,
Site,
Location,
Rack,
Role,
Manufacturer,
Type,
IP Address,
ID,
Tenant Group,
Platform,
Serial number,
Asset tag,
Region,
Site Group,
Parent Device,
Position (Device Bay),
Position,
Rack face,
Latitude,
Longitude,
Airflow,
IPv4 Address,
IPv6 Address,
OOB IP,
Cluster,
Virtual Chassis,
VC Position,
VC Priority,
Description,
Config Template,
Comments,
Contacts,
Tags,
Created,
Last updated,
Console ports,
Console server ports,
Power ports,
Power outlets,
Interfaces,
Front ports,
Rear ports,
Device bays,
Module bays,
Inventory items

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

Для подключения к устройству и ведения с ним диалога Милошу необходима библиотека, которая реализует чтение информации с устройства и запись на него. Одной из известных библиотек является Napalm, написанная на Python. Милош понимает, что он столкнётся с задачами масштабирования, так что смотрит в сторону библиотеки написанной на Golang.

Мы в Яндексе разработали и выложили в опенсорс библиотеку gnetcli. Она написана на Go, с учётом нашего опыта работы со схожими библиотеками на Python. В статьях будем давать примеры использования именно gnetcli.
Более подробно о том, почему мы разрабатываем собственную библиотеку общения с оборудованием, и какие ещё есть сценарии использования — мы обязательно расскажем в следующий раз.

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

func (d *Device) Ping() error {
	var cmd *exec.Cmd

	// Checking the type of OS because the ping command varies in structure according to the OS type
	if runtime.GOOS == "windows" {
		cmd = exec.Command("ping", "-n", "1", d.Address)
	} else {
		cmd = exec.Command("ping", "-c", "1", d.Address)
	}

	out, err := cmd.CombinedOutput()

	if err != nil {
		return fmt.Errorf("there was an error pinging the host: %e", err)
	}

	outStr := string(out)
	if strings.Contains(outStr, "Request timeout") || strings.Contains(outStr, "Destination Host Unreachable") || strings.Contains(outStr, "100% packet loss") {
		return fmt.Errorf("the host is not reachable")
	} else {
		return nil
	}
}

Далее импортируем основные данные об устройствах из importInventory.json:

[
    {
        "hostname": "switch01",
        "vendor": "Cisco",
        "breed": "ios",
        "address": "10.0.10.2",
        "login": "milos",
        "password": "supermilos",
        "interfaces": []
    },
    {
        "hostname": "switch04",
        "vendor": "Huawei",
        "breed": "vrp85",
        "address": "10.0.10.122",
        "login": "milos",
        "password": "supermilos",
        "interfaces": []
    }
]

Для подключения к устройству нам потребуются address, login, password. Ниже приведён пример, возвращающий ssh streamer для работы с устройством.

func (d *Device) GetConnector() *ssh.Streamer {
	logger := zap.Must(zap.NewDevelopmentConfig().Build())
	creds := dcreds.NewSimpleCredentials(
		dcreds.WithUsername(d.Login),
		dcreds.WithPassword(dcreds.Secret(d.Password)),
		dcreds.WithLogger(logger),
	)
	return ssh.NewStreamer(d.Address, creds, ssh.WithLogger(logger))
}

Он будет использоваться в основной функции этого скрипта — отправка команд на устройство и предоставление результата их выполнения.

func (d *Device) SendCommands(commands ...string) ([]cmd.CmdRes, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()
	dev := genericcli.GenericDevice{}
	switch d.Vendor {
	case "Cisco":
		dev = cisco.NewDevice(d.GetConnector())
	case "Huawei":
		dev = huawei.NewDevice(d.GetConnector())
	default:
		return nil, errors.New("unknown vendor")
	}
	err := dev.Connect(ctx)
	if err != nil {
		return nil, err
	}
	defer dev.Close()
	reses, _ := dev.ExecuteBulk(cmd.NewCmdList(commands))
	for _, res := range reses {
		if res.Status() == 0 {
			fmt.Printf("Result: %s\n", res.Output())
		} else {
			fmt.Printf("Error: %d\nStatus: %d\n", res.Status(), res.Error())
		}
	}
	return reses, nil
}

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

Шаг 2. Сбор информации об интерфейсах устройства

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

DEVICE_1 → PORT_1 → ← PORT_2 ← DEVICE_2

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

Для неё нам необходимо найти для switch01 такие соседства:

  • fa0/1: switch02

  • fa0/2: switch03

  • fa0/3: srv1

  • fa0/4: srv2

Используя функцию SendCommand, соберём информацию об интерфейсах с устройств.

Результат работы для Cisco
data, err := d.Device.SendCommand("show ip interface brief")
...
Capability codes:
    (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
    (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other

Device ID           Local Intf     Hold-time  Capability      Port ID
switch01            Fa0/1          120        B               Fa0/1
switch02            Fa0/2          120        B               Fa0/1
srv1                Fa0/3          3601                       2cfd.a1af.54f9
srv2                Fa0/4          3601                       00e0.4c68.0182

Total entries displayed: 4

Результат работы для Huawei
data, err := d.Device.SendCommand("display interface brief")
...
PHY: Physical
*down: administratively down
^down: standby
(l): loopback
(s): spoofing
(b): BFD down
(e): ETHOAM down
(d): Dampening Suppressed
(p): port alarm down
(dl): DLDP down
(c): CFM down
(sd): STP instance discarding
(ed): error down
InUti/OutUti: input utility rate/output utility rate
Interface                  PHY      Protocol  InUti OutUti   inErrors  outErrors
100GE1/0/1                 up       up        1.31%  1.09%          0          0
100GE1/0/2                 up       up        2.59%  1.62%          0          0
100GE1/0/3                 down     down         0%     0%          0          0
100GE1/0/4                 down     down         0%     0%          0          0
100GE1/0/5                 down     down         0%     0%          0          0
100GE1/0/6                 down     down         0%     0%          0          0
100GE1/0/7                 down     down         0%     0%          0          0
100GE1/0/8                 down     down         0%     0%          0          0
...

Также Милошу потребуется информация о LLDP‑соседях. Соберем её, той же функцией.

LLDP-соседи Cisco
res, err := d.SendCommands("show lldp neighbors")
...
Capability codes:
    (R) Router, (B) Bridge, (T) Telephone, (C) DOCSIS Cable Device
    (W) WLAN Access Point, (P) Repeater, (S) Station, (O) Other

Device ID           Local Intf     Hold-time  Capability      Port ID
switch02            Fa0/1          120        B               Fa0/1
switch03            Fa0/2          120        B               Fa0/1
srv1                Fa0/3          120        B               eth0
srv2                Fa0/4          120        B               eth0

LLDP-соседи Huawei
data, err := d.Device.SendCommand("display lldp neighbor brief")
...
<switch04>display lldp neighbor brief 
Local Interface         Exptime(s) Neighbor Interface      Neighbor Device     
----------------------------------------------------------------------------
40GE1/0/1                     103  100GE9/0/2              spine3
40GE1/0/2                      94  100GE6/0/5              spine3
GE1/0/1                       109  0025-90c8-43dc          server4624
GE1/0/4                       106  0025-906c-114a          server1748
GE1/0/5                       106  0025-90c8-7960          server4663
MEth0/0/0                     118  Fa0/43                  mgmt-switch-22

Данные с устройства получены, и Милош готов к следующей части — парсинг полученных данных.

Шаг 3. Парсинг данных

Прежде чем парсить список соседей, вначале надо иметь список доступных на устройстве интерфейсов.

Обратите внимание, что FastEthernet0/0 не равно Fa0/0, хотя это один и тот же порт. Тут мы наступаем на проблему ShortName и LongName интерфейса, потому что команда show ip interface brief отдаёт длинные имена, а show lldp neighbors — короткие. Поэтому необходимо хранить оба значения имени.

Пример парсинга интерфейсов:

func (d *CiscoDevice) CutIfaceName(name string) string {
	if strings.Contains(name, "FastEthernet") {
		r := regexp.MustCompile(`FastEthernet`)
		return r.ReplaceAllString(name, "Fa")
	}
	return ""
}

func (d *CiscoDevice) GetInterfaces() error {
	res, err := d.SendCommands("show ip interface brief")
	if err != nil {
		return err
	}
	data := res[0]
	reader := bytes.NewReader(data.Output())
	scanner := bufio.NewScanner(reader)
	scanner.Split(bufio.ScanLines)
	var txtlines []string
	for scanner.Scan() {
		txtlines = append(txtlines, scanner.Text())
	}
	start_line := 0
	for i, line := range txtlines {
		if strings.Contains(line, "Interface") {
			start_line = i + 1
			break
		}
	}
	if start_line == 0 {
		return nil
	}
	for i := start_line; i < len(txtlines); i++ {
		space := regexp.MustCompile(`\s+`)
		line := space.ReplaceAllString(txtlines[i], " ")
		if len(line) == 0 {
			break
		}
		split_line := strings.Split(line, " ")
		d.Interfaces = append(d.Interfaces, &Interface{
			Name: split_line[0],
			ShortName: d.CutIfaceName(split_line[0]),
		})
	}
	return nil
}

Научившись собирать интерфейсы, можно приступать к парсингу соседей. Пример функции парсинга LLDP Neighbors:

func (d *CiscoDevice) GetLLDPNeigbours() error {
	res, err := d.SendCommands("show lldp neighbors")
	if err != nil {
		return nil
	}
	data := res[0]
	reader := bytes.NewReader(data.Output())
	scanner := bufio.NewScanner(reader)
	scanner.Split(bufio.ScanLines)
	var txtlines []string
	for scanner.Scan() {
		txtlines = append(txtlines, scanner.Text())
	}
	start_line := 0
	for i, line := range txtlines {
		if strings.Contains(line, "Device ID") {
			start_line = i + 1
			break
		}
	}
	if start_line == 0 {
		return nil
	}
	for i := start_line; i < len(txtlines); i++ {
		space := regexp.MustCompile(`\s+`)
		line := space.ReplaceAllString(txtlines[i], " ")
		if len(line) == 0 {
			break
		}
		split_line := strings.Split(line, " ")

		iface := d.GetInterfaceByName(split_line[1])
		if iface != nil {
			iface.Neighbor = split_line[0]
		}
	}
	return nil
}

За этим следует задача синхронизации реального состояния оборудования с Inventory. Одна из важных задач, которая включает в себя асинхронную многопоточную реализацию похода на устройства, стабильный и регулярный синк этих данных и многое другое из мира Software Engineering. В текущей статье мы ограничимся самой простой реализацией — скриптом. А подробнее об этом поговорим в статье про систему инвентаризации.

Помимо оборудования Cisco, у Милоша на сети присутствуют устройства Huawei. Тут его ожидает новая проблема — разные производители имеют разные форматы команд. Как видим во втором шаге, формат данных об LLDP‑соседях у Cisco и Huawei разный. Отличаются форма и содержание вывода команд, сами команды тоже разные. Поэтому одним парсером тут, к сожалению, не обойтись. Поэтому появляются парсеры и для Huawei.

func (d *HuaweiDevice) GetLLDPNeigbours() error {
	data, err := d.Device.SendCommand("display lldp neighbor brief")
	if err != nil {
		return err
	}
	scanner := bufio.NewScanner(bytes.NewReader(data.Output()))
	scanner.Split(bufio.ScanLines)
	var txtlines []string
	for scanner.Scan() {
		txtlines = append(txtlines, scanner.Text())
	}
	start_line := 0
	for i, line := range txtlines {
		if strings.Contains(line, "Local Interface") {
			start_line = i + 2
			break
		}
	}
	if start_line == 0 {
		return nil
	}
	for i := start_line; i < len(txtlines); i++ {
		space := regexp.MustCompile(`\s+`)
		line := space.ReplaceAllString(txtlines[i], " ")
		if len(line) == 0 {
			break
		}
		split_line := strings.Split(line, " ")

		iface := d.GetInterfaceByName(split_line[0])
		if iface != nil {
			iface.Neighbor = split_line[3]
			iface.NeighborPort = split_line[2]
		}
	}
	return nil
}

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

Мы собрали необходимую информацию с устройства, теперь её можно экспортировать в файл exportInventory.csv. Для этого используем формат Netbox и преобразуем данные в формат CSV.

func WriteInventoryToCSV(devices []*InventoryDevice) error {
	var data [][]string
	csvFile, err := os.Create("exportInventory.csv")
	if err != nil {
		log.Fatalf("failed creating file: %s", err)
	}
	csvwriter := csv.NewWriter(csvFile)
	csvwriter.Comma = ','
	defer csvFile.Close()
	for _, device := range devices {
		s := reflect.ValueOf(device).Elem()
		row := []string{}
		for i := 0; i < s.NumField(); i++ {
			if s.Field(i).Kind() == reflect.Slice {
				val := s.Field(i)
				ret := new(strings.Builder)
				delim := ";"
				for i := 0; i < val.Len(); i++ {
					if val.Index(i).Kind() == reflect.String {
						fmt.Println(val.Index(i).String())
						ret.WriteString(val.Index(i).String())
						ret.WriteString(delim)
					}
				}
				row = append(row, ret.String())
			} else {
				row = append(row, s.Field(i).String())
			}
		}
		data = append(data, row)
	}
	csvwriter.WriteAll(data)
	return nil
}
Так будет выглядеть exportInventory.json
Name,Status,Tenant,Site,Location,Rack,Role,Manufacturer,Type,IP Address,ID,Tenant Group,Platform,Serial number,Asset tag,Region,Site Group,Parent Device,Position (Device Bay),Position,Rack face,Latitude,Longitude,Airflow,IPv4 Address,IPv6 Address,OOB IP,Cluster,Virtual Chassis,VC Position,VC Priority,Description,Config Template,Comments,Contacts,Tags,Created,Last updated,Console ports,Console server ports,Power ports,Power outlets,Interfaces,Front ports,Rear ports,Device bays,Module bays,Inventory items
switch01,ACTIVE,,,,,,Cisco,,,,,,,,,,,,,,,,10.0.10.2,,,,,,,,,,,,,,,,,FastEthernet0/1;FastEthernet0/2;FastEthernet0/3;FastEthernet0/4;...;FastEthernet0/48;Loopback0,,,,,

База готова — пора приступать к генерации конфигурации.

Шаг 4. Подготовка конфигурации

Тут есть два варианта:

  1. Простой вариант. Реализовать генерацию конфигурации в скрипте на gnetcli.

  2. Сложный вариант. Использовать фреймворк для генераторов.

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

Милош готов к заключительному шагу этой задачи. Для того чтобы настроить description на порту интерфейса Cisco, необходимо ввести следующие команды:

enable
conf t
interface FastEthernet0/1
description switch02_Fa0/1

Давайте напишем функцию, которая будет возвращать необходимую конфигурацию.

func (d *CiscoDevice) GenInterfaceDescription() []string {
	ret := []string{}
	ret = append(ret, "en", "conf t")
	for _, iface := range d.Interfaces {
		if len(iface.Neighbor) > 0 {
			description := iface.Neighbor + "_" + iface.NeighborPort
			ret = append(ret, "interface " + iface.Name)
			ret = append(ret, "description " + description)
			ret = append(ret, "!")
		}
	}
	for _, c := range ret {
		fmt.Println(c)
	}
	return ret
}
Сгенерированная конфигурация
enable
conf t
interface FastEthernet0/2
description switch02_Fa0/1
!
interface FastEthernet0/2
description switch03_Fa0/1
!
interface FastEthernet0/3
description srv1_eth0
!
interface FastEthernet0/4
description stv1_eth0
!

На этом Милош приступает к завершающему этапу.

Шаг 5. Применение конфигурации

Если переиспользовать функцию SendCommands и подавать на вход команды из генератора GenInterfaceDescription, Милош может реализовать функцию применения конфигурации на устройствах.

func (d *CiscoDevice) SetInterfaceDescription() error {
	cmds := d.GenInterfaceDescription()
	_, err := d.SendCommands(cmds...)
	return err
}

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

А пока…

Успех! Значения description успешно выкачены на устройства. Задача решена.

Подытожим и наметим планы

Давайте перечислим, что сделал Милош для решения задачи:

  • организовал сбор и парсинг данных сетевых устройств,

  • наладил хранение данных в Inventory‑системе,

  • написал генератор конфигурации,

  • и реализовал выкатку конфигурации на сеть.

Это первый шаг на пути к автоматизации настройки сети! Мы и дальше будем следить за успехами Милоша. В этой статье мы вместе с ним постепенно погрузились в мир автоматизации сети и на достаточно простом примере подняли такие вопросы, как:

  • чтение информации с устройства;

  • запись информации на устройства;

  • генерацию конфигурации;

  • применение конфигурации;

  • хранение инвентарных данных о сети;

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

Спасибо за внимание.

Полезные ссылки
Все примеры из этой статьи рабочие и доступны в GitHub.

Комментарии (13)


  1. shibanovan
    04.04.2024 08:38
    +2

    Хм :) А аффилирован ли как то Милош с сербским офисом?


    1. vadvolo Автор
      04.04.2024 08:38
      +2

      Tako je:)


  1. event1
    04.04.2024 08:38

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


    1. vadvolo Автор
      04.04.2024 08:38
      +2

      Все верно. Обычно, если устройство поддерживает NETCONF, то он покрывает часть функционала устройства, а CLI полностью. Поэтому основной упор делаем на CLI


    1. moonug
      04.04.2024 08:38

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

      Проще говоря - с netconf/restconf ты всегда рискуешь наступить на проблемы для этих вендоров, с cli как правило нет.

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


      1. event1
        04.04.2024 08:38

        Даже в базовых моделях совместимость плохая? Базовая настройка интерфейсов должна быть одинаковая, нет?

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

        парсить произвольный текст так себе удовольствие. Со структуированными данными всё-таки удобнее работать.


        1. moonug
          04.04.2024 08:38

          Даже в базовых моделях совместимость плохая? Базовая настройка интерфейсов должна быть одинаковая, нет?

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

          парсить произвольный текст так себе удовольствие. Со структуированными данными всё-таки удобнее работать.

          Да. Жизнь - боль :). Ну это просто специфика предметной области, не более того. Заставляет чуть больше думать головой.


  1. ammo
    04.04.2024 08:38

    Милош понимает, что он столкнётся с задачами масштабирования, так что смотрит в сторону библиотеки написанной на Golang

    I/O bound задачи

    Какая-то очень сомнительная аргументация. Особенно с учетом того, что различных инструментов для автоматизации сети/инфраструктуры на Python написано на порядок больше.
    А, хотя это же Яндекс, совсем забыл...


    1. vadvolo Автор
      04.04.2024 08:38
      +1

      Про автоматизацию на Python можно будет почитать в следующих выпусках.

      В целом, наш стек это не только Go или Python, но и PHP, Ruby, Bash, M4 и другое.


  1. PS_erg
    04.04.2024 08:38

    Друг спрашивает, вы, случайно, не нанимаете?


    1. vadvolo Автор
      04.04.2024 08:38
      +1

      Нанимаем. Друг может связаться со мной или нашей HR Катей через Telegram (@vadvolo, @djeibocat)


  1. pavellp
    04.04.2024 08:38

    Cобирать подобную информацию лучше по snmp, а не через cli. Скорее всего подобную работу надо будет выполнять регулярно, а работа через cli сильно может забивать системы логирования и авторизации команд.


    1. gescheit
      04.04.2024 08:38

      Бесспорно собирать данные через SNMP удобнее в том плане, что они будут в виде структуры, что избавит от парсинг, но это и добавляет сложности в проект. Тут в примере только одна секция конфигурации, но в реальной жизни настраивать нужно значительно больше и с большой долей вероятности даже у одного вендора не всё, что нужно будет доступно в SNMP, а еще нужно помножить эту проблему на количество вендоров. То есть в реальной жизни не получится сделать SNMP для получения данных, а CLI для конфигурации.

      Кроме времени на разработку и поддержку дополнительной сущности в проекте потребуются ресурсы на поддержание доступа по SNMP. Поменяли адрес сервера - выкати настройки ACL или фаейрволла, пришло время поменять community string - не забудьте в проекте тоже поменять.

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