Это часть серии статей, посвященных принципам сетевой автоматизации.

DRY в компьютерных науках

Фраза Do Not Repeat Yourself или DRY- довольно простое понятие. Если вы пишете фрагмент кода несколько раз, сделайте его модульным таким образом, чтобы не копировать код несколько раз. Определение DRY гласит:

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

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

В контексте контекста, Барт Симпсон, который снова и снова пишет одну и ту же фразу на доске, определенно не DRY.

Пример

В этом первом надуманном примере вы увидите дублирование кода при проверке данных VLAN.

>>> def add_vlan(vlan_id):
...     if not isinstance(vlan_id, int):
...         raise TypeError("VLAN ID is not an integer")
...     elif not (vlan_id >=1 and vlan_id <= 4096):
...         raise ValueError("Invalid VLAN ID, which must be between 1-4096")
...     return "vlan {vlan_id}".format(vlan_id=vlan_id)
...
>>> def configure_port_vlan(interface, vlan_id):
...     if not isinstance(vlan_id, int):
...         raise TypeError("VLAN ID is not an integer")
...     elif not (vlan_id >=1 and vlan_id <= 4096):
...         raise ValueError("Invalid VLAN ID, which must be between 1-4096")
...     return "interface {interface}\n switchport access vlan {vlan_id}".format(interface=interface, vlan_id=vlan_id)
...
>>>

 

DRY данные

Вы заметите, что в официальном определении есть ссылка на «знание», которое, как указано в Pragmatic Programmer

«Знания о системе намного шире, чем просто ее код. Это относится к схемам баз данных, планам тестирования, системе сборки и даже документации ».

Проще говоря, DRY применим не только к коду. Один из примеров, понятный сетевым инженерам, — это управление данными для маски подсети. Я часто сталкиваюсь с решениями данных и связанных файлов Jinja, которые похожи на:

subnets:
  - network: 10.1.1.0
    cidr: 24
    subnet_mask: 255.255.255.0
    wildcard_mask: 0.0.0.255
  - network: 10.1.2.0
    cidr: 24
    subnet_mask: 255.255.255.0
    wildcard_mask: 0.0.0.255
{% if ansible_network_os = "ios" %}
  ip address {{ subnet['network'] | ipaddr('add', 1 ) }} {{ subnet['subnet_mask'] }}{# example result: 10.1.1.1 255.255.255.0 #}
{% elif ansible_network_os = "nxos" %}
  ip address {{ subnet['network'] | ipaddr('add', 1 ) }}/{{ subnet['cidr'] }}{# example result: 10.1.1.1/24 #}
{% endif %}

Это решение, безусловно, имеет свои преимущества, однако, учитывая огромное количество управляемых подсетей, вполне вероятно, что в какой-то момент появится пользователь, который создаст cidrиз 25и subnet_maskкак 255.255.255.0. Однако подсеть и подстановочную маску можно определить из файла cidr, и многократное ведение данных является излишним и подверженным ошибкам. Единовременная стоимость создания перевода гарантирует, что вы не будете повторяться и не столкнетесь с проблемами, описанными здесь.

Вместо этого вы можете управлять своими данными более DRY, например:

subnets:
  - network: 10.1.1.0/24
  - network: 10.1.2.0/24
{% if ansible_network_os = "ios" %}
  ip address {{ subnet['network'] | ipaddr('add', 1 ) }} {{ subnet['network'] | ipaddr('netmask') }}{# example result: 10.1.1.1 255.255.255.0 #}
{% elif ansible_network_os = "nxos" %}
  ip address {{ subnet['network'] | ipaddr('add', 1 ) }}/{{ subnet['network'] | ipaddr('prefix') }}{# example result: 10.1.1.1/24 #}
{% endif %}

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

Намокать в DRY мире

Если DRY не повторяет какой-либо элемент знаний, обратное будет «Записывать каждый раз» (WET). Зачем вам повторяться? Что ж, как всегда, это зависит от обстоятельств, и всегда необходимо учитывать конструктивные особенности.

Например, обычная модель в конечном итоге, — это клиенты, которые создают один шаблон Jinja для IOS, NXOS и EOS. Поскольку эти три ОС имеют много общего, с точки зрения DRY вы можете быть склонны писать свои шаблоны в виде объединенных шаблонов.

.
├── bgp.j2
├── ntp.j2
└── vlan.j2

0 directories, 3 files

С примером файла Jinja, например

{% for vlan in vlans %}
vlan {{ vlan['id'] }}
 name {{ vlan['name'] }}
{% endfor %}
!

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

{% if ansible_network_os == 'ios' %}
router bgp {{ bgp['asn'] }}
 bgp router-id {{ bgp['id'] }}
 bgp log-neighbor-changes
{% for neighbor in bgp['neighbors'] %}
 neighbor {{ neighbor['ip'] }} remote-as {{ neighbor['asn'] }}
 neighbor {{ neighbor['ip'] }} description {{ neighbor['description'] }}
{% endfor %}
 address-family ipv4
{% for net in bgp['networks'] %}
  network {{ net | ipaddr('network') }} mask {{ net | ipaddr('netmask') }}
{% endfor %}
{% for neighbor in bgp['neighbors'] %}
  neighbor {{ neighbor['ip'] }} activate
{% endfor %}
 exit-address-family
{# 
--------------
SWITCH TO NXOS
--------------
#}
{% elif ansible_network_os == 'nxos' %}
router bgp {{ bgp['asn'] }}
  router-id {{ bgp['id'] }}
  address-family ipv4 unicast
{% for net in bgp['networks'] %}
    network {{ net }}
{% endfor %}
{% for neighbor in bgp['neighbors'] %}
  neighbor {{ neighbor['ip'] }} remote-as {{ neighbor['asn'] }}
    description {{ neighbor['description'] }}
    address-family ipv4 unicast
{% endfor %}
{% endif %}

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

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

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

.
├── eos
│   ├── bgp.j2
│   ├── ntp.j2
│   └── vlan.j2
├── ios
│   ├── bgp.j2
│   ├── ntp.j2
│   └── vlan.j2
└── nxos
    ├── bgp.j2
    ├── ntp.j2
    └── vlan.j2

3 directories, 9 files

Подобные ситуации будут возникать постоянно, и вам просто нужно будет использовать опыт / интуицию, чтобы понять влияние и принять оттуда лучшее решение.

Вывод

Управление кодом или данными в большинстве случаев должно выполняться СУХИМ способом, поскольку этот метод сокращает количество ошибок и способствует оптимизации вычислений. При этом есть некоторые конструктивные особенности, при которых может иметь смысл использовать подход WET.