devops
December 2, 2021

Строим карту подсетей на Python

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

Однако и на моей улице случился праздник - возникла необходимость разобраться с виртуальной сетью, состоящей из огромного количества подсетей различного размера. На вход мне подали несколько файлов в формате JSON, на выходе нужна была внятная картина происходящего. Из инструментов у меня были только Python, Stack Overflow и базовые знания по сетям.

Парсинг JSON

Взглянув на исходные данные, я понял, что ничего особенно страшного в них нет - только подсети и их id:

[
  {
    "subnet": "10.0.0.0/16",
    "id": "GBYQGY" 
  },
  {
    "subnet": "2.0.0.0/24",
    "id": "MHI6OJ"
  },
  {
    "subnet": "10.0.42.0/24",
    "id": "3NLBMW"
  },
  ...
  {
    "subnet": "11.5.183.0/30",
    "id": "TWWMFH"
  }
]

Для парсинга JSON буду использовать библиотеку json:

import json

def parseJSON(json_file):
   f = open(json_file, "r")
   data = json.loads(f.read())
   f.close()
   return data

Теперь можно получить список сетей в виде списка словарей и взглянуть на него по-человечески:

import re

# Получим список сетей из JSON-файла
subnets = parseJSON('subnets.json')

# Отсортируем адреса подсетей по возрастанию
subnets = sorted(subnets, key = lambda i: [int(m) for m in re.findall("\d+", i['subnet'])])

# Выведем список на экран
for subnet in subnets:
   print("Subnet:", subnet['subnet'].ljust(18), "ID:", subnet['id'])

Обратите внимание, что для сортировки подсетей используется библиотека re. (Спасибо Jean-François Fabre)

Выхлоп скрипта:

Subnet: 2.0.0.0/16         ID: K298OT
Subnet: 2.0.5.0/24         ID: MOXKAC
Subnet: 10.0.0.0/16        ID: ZIDR8I
Subnet: 10.0.0.0/18        ID: B8FQRF
Subnet: 10.0.0.0/24        ID: PP6BUY
Subnet: 10.0.1.0/24        ID: GLYTC9
Subnet: 172.0.0.0/18       ID: CFY9HZ
Subnet: 172.0.3.0/24       ID: E8KPII
Subnet: 172.77.63.0/24     ID: ZJZFFY
Subnet: 172.77.65.0/24     ID: GOGRCB

С первой задачей справились.

Разбираемся с сетями

Что мы знаем про подсети? У них есть префикс и маска. Маска определяет размер подсети (сколько в ней уместится IP-адресов). Благодаря ей мы можем разделить одну большую сеть на кучу маленьких. Более подробно можно почитать на Википедии.

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

Для работы с IP-адресами буду использовать библиотеку ipaddress:

import ipaddress

subnets = parseJSON('subnets.json')

for i in range(len(subnets)):
   sn1 = ipaddress.ip_network(subnets[i]['subnet'])
   for subnet in subnets[i+1:]:
      sn2 = ipaddress.ip_network(subnet['subnet'])
      if sn1.supernet_of(sn2):
         print("Subnet", str(sn1).ljust(12), "is supernet of", sn2)

Смотрим на выхлоп:

Subnet 2.0.0.0/16   is supernet of 2.0.5.0/24
Subnet 10.0.0.0/16  is supernet of 10.0.0.0/18
Subnet 10.0.0.0/16  is supernet of 10.0.0.0/24
Subnet 10.0.0.0/16  is supernet of 10.0.1.0/24
Subnet 10.0.0.0/18  is supernet of 10.0.0.0/24
Subnet 10.0.0.0/18  is supernet of 10.0.1.0/24
Subnet 172.0.0.0/18 is supernet of 172.0.3.0/24

Мы видим, что подсети вполне себе пересекаются.

Кстати, ещё один способ проверить пересечение сетей - использовать метод overlaps:

for i in range(len(subnets)):
   sn1 = ipaddress.ip_network(subnets[i]['subnet'])
   for subnet in subnets[i+1:]:
      sn2 = ipaddress.ip_network(subnet['subnet'])
      if sn1.overlaps(sn2):
         print("Subnets", sn1, "and", sn2, "are overlapsed")    

Выхлоп:

Subnets 2.0.0.0/16   and 2.0.5.0/24   are overlapsed
Subnets 10.0.0.0/16  and 10.0.0.0/18  are overlapsed
Subnets 10.0.0.0/16  and 10.0.0.0/24  are overlapsed
Subnets 10.0.0.0/16  and 10.0.1.0/24  are overlapsed
Subnets 10.0.0.0/18  and 10.0.0.0/24  are overlapsed
Subnets 10.0.0.0/18  and 10.0.1.0/24  are overlapsed
Subnets 172.0.0.0/18 and 172.0.3.0/24 are overlapsed

Строим карту

У нас есть список подсетей и умение определять их пересечение. Остаётся на основе этого построить карту.

Карта будет отображаться в виде дерева и для этого буду использовать библиотеку anytree:

from anytree import Node, RenderTree

subnets = parseJSON('subnets.json')

nodes = []
# Сразу добавляем единый корень будущему дереву, поскольку наши данные его не создадут
node_root = Node("subnet", id="id")
nodes.append(node_root)

# Добавляем подсети в список нод, сразу указывая родителя по умолчанию
for subnet in subnets:
   node = Node(subnet['subnet'], id=subnet['id'], parent=nodes[0])
   nodes.append(node)

# Находим родителей на основе факта пересечения сетей
for node in nodes:
   if node.is_leaf:
      sn1 = ipaddress.ip_network(node.name)
      for i in range(1, len(nodes)):
         sn2 = ipaddress.ip_network(nodes[i].name)
         if sn1 != sn2 and sn1.supernet_of(sn2):
            nodes[i].parent = node

# Строим дерево
for pre, fill, node in RenderTree(node_root):
   print("%s%s" % (pre, node.name))

Перед запуском необходимо установить anytree следующей командой:

pip3 install anytree

Выхлоп:

subnet
├── 2.0.0.0/16
│   └── 2.0.5.0/24
├── 10.0.0.0/16
│   └── 10.0.0.0/18
│       ├── 10.0.0.0/24
│       └── 10.0.1.0/24
├── 172.0.0.0/18
│   └── 172.0.3.0/24
├── 172.77.63.0/24
└── 172.77.65.0/24

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

from anytree.exporter import DotExporter

DotExporter(node_root).to_picture("network_map.png")

Результат:

Визуализация с помощью Graphviz

Выводы

Карта подсетей в первом приближении построена. Опыт работы с Python получен. Дальше можно оптимизировать и улучшать функционал скрипта.