conf-tree


Nameconf-tree JSON
Version 0.1.1 PyPI version JSON
download
home_pageNone
SummaryParsing, filtering and diff calculation for network device configurations
upload_time2024-12-02 10:24:57
maintainerNone
docs_urlNone
authorAlexander Ignatov
requires_python>=3.10
licenseMIT
keywords cisco arista huawei network automation configuration
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Библиотека Conf-Tree

- [Библиотека Conf-Tree](#библиотека-conf-tree)
  - [Краткое описание](#краткое-описание)
  - [Быстрый пример (00.quick-start.py)](#быстрый-пример-00quick-startpy)
  - [Преобразование в дерево (01.parsing.py)](#преобразование-в-дерево-01parsingpy)
  - [Поиск/фильтрация (02.searching.py)](#поискфильтрация-02searchingpy)
  - [Сериализация/десериализация (03.serialization.py)](#сериализациядесериализация-03serializationpy)
  - [Изменение порядка (04.reorder.py)](#изменение-порядка-04reorderpy)
  - [Разница конфигураций](#разница-конфигураций)
    - [Наивная разница (05.naive.diff.py)](#наивная-разница-05naivediffpy)
    - [Пост-обработка разницы конфигураций (06.postproc.diff.py)](#пост-обработка-разницы-конфигураций-06postprocdiffpy)
    - [Секции без вычисления разницы (07.no.diff.section.py)](#секции-без-вычисления-разницы-07nodiffsectionpy)
    - [Секции, где порядок имеет значение (08.ordered.diff.py)](#секции-где-порядок-имеет-значение-08ordereddiffpy)
  - [История версий](#история-версий)
  - [TODO](#todo)

## Краткое описание

Библиотека для работы с конфигурацией сетевых устройств:

- преобразование конфигурации в дерево
- поиск/фильтрация конфигурации
- вычисление разницы (diff) между двумя конфигурациями

## Быстрый пример ([00.quick-start.py](./examples/00.quick-start.py))

- Читаем текущий и целевой конфигурации из файлов
- Преобразуем конфигурации в деревья, попутно размечая тегами секции bgp и static routes
- Получаем разницу конфигураций
- Фильтруем разницу (а можно сначала фильтровать текущее/целевое деревья, а потом вычислять разницу между ними)

<details>
    <summary>Листинг (click me)</summary>

```python
In [2]: from conf_tree import ConfTreeEnv, Vendor

In [3]: def get_configs() -> tuple[str, str]:
   ...:     with open(file="./examples/configs/cisco-router-1.txt", mode="r") as f:
   ...:         current_config = f.read()
   ...:     with open(file="./examples/configs/cisco-router-2.txt", mode="r") as f:
   ...:         target_config = f.read()
   ...:     return current_config, target_config
   ...: 

In [4]: def get_ct_environment() -> ConfTreeEnv:
   ...:     tagging_rules: list[dict[str, str | list[str]]] = [
   ...:         {"regex": r"^router bgp \d+$", "tags": ["bgp"]},
   ...:         {"regex": r"^ip route \S+", "tags": ["static"]},
   ...:     ]
   ...:     return ConfTreeEnv(
   ...:         vendor=Vendor.CISCO,
   ...:         tagging_rules=tagging_rules,
   ...:     )
   ...: 

In [5]: current_config, target_config = get_configs()

In [6]: env = get_ct_environment()

In [7]: current = env.parse(current_config)

In [8]: target = env.parse(target_config)

In [9]: diff = env.diff(current, target)

In [10]: print("\n!-- разница конфигураций --")
    ...: print(diff.config)
    ...: 

!-- разница конфигураций --
interface Tunnel2
 no ip ospf priority 0
 ip ospf priority 1
!
router bgp 64512
 no neighbor RR peer-group
 address-family ipv4
  network 10.255.255.1 mask 255.255.255.255
!
line vty 0 4
 no exec-timeout 15 0
 exec-timeout 10 0
!
line vty 5 15
 no exec-timeout 15 0
 exec-timeout 10 0
!
ip name-server 192.168.0.9
!
no ip name-server 192.168.0.3
!
no ip route 192.168.255.1 255.255.255.255 Tunnel2
!
no ip route vrf FVRF 192.66.55.44 255.255.255.255 143.31.31.2
!

In [11]: print("\n!-- разница без секций с тегами bgp и static --")
    ...: diff_no_routing = env.search(diff, exclude_tags=["bgp", "static"])
    ...: print(diff_no_routing.config)
    ...: 

!-- разница без секций с тегами bgp и static --
interface Tunnel2
 no ip ospf priority 0
 ip ospf priority 1
!
line vty 0 4
 no exec-timeout 15 0
 exec-timeout 10 0
!
line vty 5 15
 no exec-timeout 15 0
 exec-timeout 10 0
!
ip name-server 192.168.0.9
!
no ip name-server 192.168.0.3
!

In [12]: print("\n!-- разница в секции с тегом bgp --")
    ...: diff_bgp = env.search(diff, include_tags=["bgp"])
    ...: print(diff_bgp.config)
    ...: 

!-- разница в секции с тегом bgp --
router bgp 64512
 no neighbor RR peer-group
 address-family ipv4
  network 10.255.255.1 mask 255.255.255.255
!
```

</details>
<br>

## Преобразование в дерево ([01.parsing.py](./examples/01.parsing.py))

- Преобразование текстовой конфигурации в дерево на основе отступов в тексте
- Возможность размечать секции/строки тегами для последующей фильтрации
- pre-run и post-run обработка конфига и получившегося дерева, например нормализация входного конфига, обработка баннеров (cisco) и пр.

<details>
    <summary>Листинг (click me)</summary>

```python
In [1]: from conf_tree import ConfTreeEnv, Vendor

In [2]: def get_configs() -> str:
   ...:     with open(file="./examples/configs/cisco-example-1.txt", mode="r") as f:
   ...:         config = f.read()
   ...:     return config
   ...: 

In [3]: def get_ct_environment() -> ConfTreeEnv:
   ...:     return ConfTreeEnv(vendor=Vendor.CISCO)
   ...: 

In [4]: config_config = get_configs()

In [5]: env = get_ct_environment()

In [6]: current = env.parse(config_config)

In [7]: print("\n---дерево в виде привычной конфигурации---")
   ...: print(current.config)

---дерево в виде привычной конфигурации---
service tcp-keepalives-in
!
service timestamps debug datetime msec localtime show-timezone
!
enable secret 5 2Fe034RYzgb7xbt2pYxcpA==
!
aaa group server tacacs+ TacacsGroup
 server 192.168.0.100
 server 192.168.0.101
!
interface Tunnel1
 ip address 10.0.0.2 255.255.255.0
 no ip redirects
!
interface Tunnel2
 ip address 10.1.0.2 255.255.255.0
 no ip redirects
!
interface FastEthernet0
 switchport access vlan 100
 no ip address
!
router bgp 64512
 neighbor 192.168.255.1 remote-as 64512
 neighbor 192.168.255.1 update-source Loopback0
 address-family ipv4
  network 192.168.100.0 mask 255.255.255.0
  neighbor 192.168.255.1 activate
!

In [8]: print("\n---конфигурация с маскированными секретами---")
   ...: print(current.masked_config)

---конфигурация с маскированными секретами---
service tcp-keepalives-in
!
service timestamps debug datetime msec localtime show-timezone
!
enable secret 5 ******
!
aaa group server tacacs+ TacacsGroup
 server 192.168.0.100
 server 192.168.0.101
!
interface Tunnel1
 ip address 10.0.0.2 255.255.255.0
 no ip redirects
!
interface Tunnel2
 ip address 10.1.0.2 255.255.255.0
 no ip redirects
!
interface FastEthernet0
 switchport access vlan 100
 no ip address
!
router bgp 64512
 neighbor 192.168.255.1 remote-as 64512
 neighbor 192.168.255.1 update-source Loopback0
 address-family ipv4
  network 192.168.100.0 mask 255.255.255.0
  neighbor 192.168.255.1 activate
!

In [9]: print("\n---дерево в виде патча для устройства---")
   ...: print(current.patch)

---дерево в виде патча для устройства---
service tcp-keepalives-in
service timestamps debug datetime msec localtime show-timezone
enable secret 5 2Fe034RYzgb7xbt2pYxcpA==
aaa group server tacacs+ TacacsGroup
server 192.168.0.100
server 192.168.0.101
exit
interface Tunnel1
ip address 10.0.0.2 255.255.255.0
no ip redirects
exit
interface Tunnel2
ip address 10.1.0.2 255.255.255.0
no ip redirects
exit
interface FastEthernet0
switchport access vlan 100
no ip address
exit
router bgp 64512
neighbor 192.168.255.1 remote-as 64512
neighbor 192.168.255.1 update-source Loopback0
address-family ipv4
network 192.168.100.0 mask 255.255.255.0
neighbor 192.168.255.1 activate
exit
exit

In [10]: print("\n---патч с маскированными секретами---")
    ...: print(current.masked_patch)

---патч с маскированными секретами---
service tcp-keepalives-in
service timestamps debug datetime msec localtime show-timezone
enable secret 5 ******
aaa group server tacacs+ TacacsGroup
server 192.168.0.100
server 192.168.0.101
exit
interface Tunnel1
ip address 10.0.0.2 255.255.255.0
no ip redirects
exit
interface Tunnel2
ip address 10.1.0.2 255.255.255.0
no ip redirects
exit
interface FastEthernet0
switchport access vlan 100
no ip address
exit
router bgp 64512
neighbor 192.168.255.1 remote-as 64512
neighbor 192.168.255.1 update-source Loopback0
address-family ipv4
network 192.168.100.0 mask 255.255.255.0
neighbor 192.168.255.1 activate
exit
exit

In [11]: print("\n---дерево в виде формальной конфигурации (аналогично formal в ios-xr)---")
    ...: print(current.formal_config)

---дерево в виде формальной конфигурации (аналогично formal в ios-xr)---
service tcp-keepalives-in
service timestamps debug datetime msec localtime show-timezone
enable secret 5 2Fe034RYzgb7xbt2pYxcpA==
aaa group server tacacs+ TacacsGroup / server 192.168.0.100
aaa group server tacacs+ TacacsGroup / server 192.168.0.101
interface Tunnel1 / ip address 10.0.0.2 255.255.255.0
interface Tunnel1 / no ip redirects
interface Tunnel2 / ip address 10.1.0.2 255.255.255.0
interface Tunnel2 / no ip redirects
interface FastEthernet0 / switchport access vlan 100
interface FastEthernet0 / no ip address
router bgp 64512 / neighbor 192.168.255.1 remote-as 64512
router bgp 64512 / neighbor 192.168.255.1 update-source Loopback0
router bgp 64512 / address-family ipv4 / network 192.168.100.0 mask 255.255.255.0
router bgp 64512 / address-family ipv4 / neighbor 192.168.255.1 activate
```

</details>
<br>

## Поиск/фильтрация ([02.searching.py](./examples/02.searching.py))

- может быть на основе тегов, проставленных во время преобразования в дерево
- может быть по строке (regex)
- в результате получается копия дерева с которой можно работать так же, как и с оригиналом

<details>
    <summary>Листинг (click me)</summary>

```python
In [1]: from conf_tree import ConfTreeEnv, Vendor
   ...: 
   ...: 
   ...: def get_configs() -> str:
   ...:     with open(file="./examples/configs/cisco-example-1.txt", mode="r") as f:
   ...:         config = f.read()
   ...:     return config
   ...: 
   ...: 
   ...: def get_ct_environment() -> ConfTreeEnv:
   ...:     tagging_rules: list[dict[str, str | list[str]]] = [
   ...:         {"regex": r"^router bgp \d+$", "tags": ["bgp"]},
   ...:         {"regex": r"^interface (Tunnel1) / ip address .*", "tags": ["interface", "tunnel-1-ip"]},
   ...:         {"regex": r"^interface (Tunnel2) / ip address .*", "tags": ["interface", "tunnel-1-ip"]},
   ...:         {"regex": r"^interface (\S+)$", "tags": ["interface"]},
   ...:     ]
   ...:     return ConfTreeEnv(
   ...:         vendor=Vendor.CISCO,
   ...:         tagging_rules=tagging_rules,
   ...:     )
   ...: 

In [2]: config_config = get_configs()
   ...: env = get_ct_environment()
   ...: router = env.parse(config_config)

In [3]: print("\n---все вхождения 'address'---")
   ...: address = env.search(router, string="address")
   ...: print(address.config)
   ...: 

---все вхождения 'address'---
interface Tunnel1
 ip address 10.0.0.2 255.255.255.0
!
interface Tunnel2
 ip address 10.1.0.2 255.255.255.0
!
interface FastEthernet0
 no ip address
!
router bgp 64512
 address-family ipv4
!

In [4]: print("\n---все вхождения 'address' с возможными потомками---")
   ...: address_children = env.search(router, string="address", include_children=True)
   ...: print(address_children.config)
   ...: 

---все вхождения 'address' с возможными потомками---
interface Tunnel1
 ip address 10.0.0.2 255.255.255.0
!
interface Tunnel2
 ip address 10.1.0.2 255.255.255.0
!
interface FastEthernet0
 no ip address
!
router bgp 64512
 address-family ipv4
  network 192.168.100.0 mask 255.255.255.0
  neighbor 192.168.255.1 activate
!

In [5]: print("\n---все вхождения 'address \d{1,3}'---")
   ...: address_ip = env.search(router, string=r"address \d{1,3}")
   ...: print(address_ip.config)
   ...: 

---все вхождения 'address \d{1,3}'---
interface Tunnel1
 ip address 10.0.0.2 255.255.255.0
!
interface Tunnel2
 ip address 10.1.0.2 255.255.255.0
!

In [6]: print("\n---конфигурация по тегу 'bgp'---")
   ...: bgp = env.search(router, include_tags=["bgp"])
   ...: print(bgp.masked_config)
   ...: 

---конфигурация по тегу 'bgp'---
router bgp 64512
 neighbor 192.168.255.1 remote-as 64512
 neighbor 192.168.255.1 update-source Loopback0
 address-family ipv4
  network 192.168.100.0 mask 255.255.255.0
  neighbor 192.168.255.1 activate
!

In [7]: print("\n---все, кроме тега 'bgp'---")
   ...: no_bgp = env.search(router, exclude_tags=["bgp"])
   ...: print(no_bgp.masked_config)
   ...: 

---все, кроме тега 'bgp'---
service tcp-keepalives-in
!
service timestamps debug datetime msec localtime show-timezone
!
enable secret 5 ******
!
aaa group server tacacs+ TacacsGroup
 server 192.168.0.100
 server 192.168.0.101
!
interface Tunnel1
 ip address 10.0.0.2 255.255.255.0
 no ip redirects
!
interface Tunnel2
 ip address 10.1.0.2 255.255.255.0
 no ip redirects
!
interface FastEthernet0
 switchport access vlan 100
 no ip address
!
```

</details>
<br>

Регулярные выражения пишутся для formal вида, т.е. строки с учетом иерархии над ней. Это дает возможность расставлять теги с учетом того, в какой секции находится конфигурационная строка:

```text
interface Tunnel1 / ip address 10.0.0.2 255.255.255.0
interface Tunnel2 / ip address 10.1.0.2 255.255.255.0
```

На ip интерфейса Tunnel1 вешаем тег "tunnel-1-ip", на ip интерфейса Tunnel2 вешаем тег "tunnel-2-ip"

```python
{
    "regex": r"^interface (Tunnel1) / ip address \S+ \S+(?: )?(secondary)?$",
    "tags": ["interface", "tunnel-1-ip"],
},
{
    "regex": r"^interface (Tunnel2) / ip address \S+ \S+(?: )?(secondary)?$",
    "tags": ["interface", "tunnel-1-ip"],
},
```

Если в регулярном выражении есть неименованные группы, то их содержимое автоматически попадает в теги:

```python
{
    "regex": r"^interface (\S+)$",
    "tags": ["interface"],
},
```

Помимо тега "interface", на строку конфигурации будет так же назначен тег, равный имени самого.

Если строка конфигурации не попала не в одно из правил, тогда теги для нее берутся из вышестоящего уровня. Например если на "interface Loopback0" были назначены теги ["interface", "Loopback0"], то все строки под этой секцией так же будут иметь эти теги, если явно не перезапишутся более узкими правилами.

## Сериализация/десериализация ([03.serialization.py](./examples/03.serialization.py))

Позволяет сохранить дерево в словарь и восстановить дерево из словаря, дальше в json, например сложить и сохранить в базу/отдать через API.

<details>
    <summary>Листинг (click me)</summary>

```python
In [1]: from conf_tree import ConfTreeEnv, Vendor
   ...: 
   ...: 
   ...: def get_configs() -> str:
   ...:     with open(file="./examples/configs/cisco-example-2.txt", mode="r") as f:
   ...:         config = f.read()
   ...:     return config
   ...: 
   ...: 
   ...: def get_ct_environment() -> ConfTreeEnv:
   ...:     tagging_rules: list[dict[str, str | list[str]]] = [
   ...:         {"regex": r"^router bgp \d+$", "tags": ["bgp"]},
   ...:         {"regex": r"^interface (\S+)$", "tags": ["interface"]},
   ...:     ]
   ...:     return ConfTreeEnv(
   ...:         vendor=Vendor.CISCO,
   ...:         tagging_rules=tagging_rules,
   ...:     )
   ...: 

In [2]: config = get_configs()
   ...: env = get_ct_environment()
   ...: router_original = env.parse(config)
   ...: 

In [3]: config_dict = env.to_dict(router_original)
   ...: print("\n---сериализация---")
   ...: print(config_dict)
   ...: 

---сериализация---
{'line': '', 'tags': [], 'children': {'service tcp-keepalives-in': {'line': 'service tcp-keepalives-in', 'tags': [], 'children': {}}, 'service timestamps debug datetime msec localtime show-timezone': {'line': 'service timestamps debug datetime msec localtime show-timezone', 'tags': [], 'children': {}}, 'interface FastEthernet0': {'line': 'interface FastEthernet0', 'tags': ['interface', 'FastEthernet0'], 'children': {'switchport access vlan 100': {'line': 'switchport access vlan 100', 'tags': ['interface', 'FastEthernet0'], 'children': {}}, 'no ip address': {'line': 'no ip address', 'tags': ['interface', 'FastEthernet0'], 'children': {}}}}, 'router bgp 64512': {'line': 'router bgp 64512', 'tags': ['bgp'], 'children': {'neighbor 192.168.255.1 remote-as 64512': {'line': 'neighbor 192.168.255.1 remote-as 64512', 'tags': ['bgp'], 'children': {}}, 'neighbor 192.168.255.1 update-source Loopback0': {'line': 'neighbor 192.168.255.1 update-source Loopback0', 'tags': ['bgp'], 'children': {}}, 'address-family ipv4': {'line': 'address-family ipv4', 'tags': ['bgp'], 'children': {'network 192.168.100.0 mask 255.255.255.0': {'line': 'network 192.168.100.0 mask 255.255.255.0', 'tags': ['bgp'], 'children': {}}, 'neighbor 192.168.255.1 activate': {'line': 'neighbor 192.168.255.1 activate', 'tags': ['bgp'], 'children': {}}}}}}}}

In [4]: router_restored = env.from_dict(config_dict)
   ...: print("\n---десериализация---")
   ...: print(router_restored.patch)

---десериализация---
service tcp-keepalives-in
service timestamps debug datetime msec localtime show-timezone
interface FastEthernet0
switchport access vlan 100
no ip address
exit
router bgp 64512
neighbor 192.168.255.1 remote-as 64512
neighbor 192.168.255.1 update-source Loopback0
address-family ipv4
network 192.168.100.0 mask 255.255.255.0
neighbor 192.168.255.1 activate
exit
exit

In [5]: print("\n---равенство двух объектов---")
   ...: print(router_original == router_restored)

---равенство двух объектов---
True
```

</details>
<br>

## Изменение порядка ([04.reorder.py](./examples/04.reorder.py))

У дерева есть метод `reorder()`, который позволяет отсортировать конфигурацию в определенном порядке. Например в случаях, когда сначала нужно соблюсти порядок настройки объектов конфигурации: сначала prefix-lists, затем route-maps (которые используют созданные prefix-lists), затем назначить созданные route-maps на bgp пиров.

<details>
    <summary>Листинг (click me)</summary>

```python
In [1]: from conf_tree import ConfTreeEnv, Vendor
   ...: 
   ...: 
   ...: def get_configs() -> str:
   ...:     with open(file="./examples/configs/cisco-example-4.txt", mode="r") as f:
   ...:         config = f.read()
   ...:     return config
   ...: 
   ...: 
   ...: def get_ct_environment() -> ConfTreeEnv:
   ...:     tagging_rules: list[dict[str, str | list[str]]] = [
   ...:         {"regex": r"^router bgp .* neighbor (\S+) route-map (\S+) (?:in|out)", "tags": ["rm-attach"]},
   ...:         {"regex": r"^router bgp \d+$", "tags": ["bgp"]},
   ...:         {"regex": r"^route-map (\S+) (?:permit|deny) \d+$", "tags": ["rm"]},
   ...:         {"regex": r"^ip community-list (?:standard|expanded) (\S+)", "tags": ["cl"]},
   ...:         {"regex": r"^ip prefix-list (\S+)", "tags": ["pl"]},
   ...:     ]
   ...:     return ConfTreeEnv(
   ...:         vendor=Vendor.CISCO,
   ...:         tagging_rules=tagging_rules,
   ...:     )
   ...: 

In [2]: config = get_configs()
   ...: env = get_ct_environment()
   ...: router = env.parse(config)
   ...: 

In [3]: print("\n--community-list -> prefix-list -> route-map -> bgp -> untagged--")
   ...: router.reorder(["cl", "pl", "rm", "bgp"])
   ...: print(router.config)

--community-list -> prefix-list -> route-map -> bgp -> untagged--
ip community-list standard cl_PE1 permit 64512:10001
!
ip community-list standard cl_PE2 permit 64512:10002
!
ip community-list expanded cl_VPNv4_1 permit 64512:2[0-9][0-9][0-9]1
!
ip community-list expanded cl_VPNv4_2 permit 64512:2[0-9][0-9][0-9]2
!
ip prefix-list pl_CSC seq 5 permit 10.0.0.0/24 ge 32
!
route-map rm_CSC_PE_in deny 10
 match community cl_PE1 cl_PE2
!
route-map rm_CSC_PE_in permit 20
 match ip address prefix-list pl_CSC
 set local-preference 200
!
route-map rm_RR_in permit 10
 match community cl_VPNv4_1
 set local-preference 200
!
route-map rm_RR_in permit 20
 match community cl_VPNv4_2
 set local-preference 190
!
router bgp 64512
 neighbor CSC peer-group
 neighbor CSC remote-as 12345
 neighbor RR peer-group
 neighbor RR remote-as 64512
 address-family ipv4
  neighbor CSC send-community both
  neighbor CSC route-map rm_CSC_PE_in in
  neighbor CSC send-label
 address-family vpnv4
  neighbor RR route-map rm_RR_in in
!
no platform punt-keepalive disable-kernel-core
!
no service dhcp
!
ip dhcp bootp ignore
!
no service pad
!

In [4]: print("\n--bgp -> community-list -> prefix-list -> route-map -> untagged -> rm-attach--")
   ...: wo_rm_attach = env.search(router, exclude_tags=["rm-attach"])
   ...: rm_attach = env.search(router, include_tags=["rm-attach"])
   ...: wo_rm_attach.reorder(["bgp", "cl", "pl", "rm"])
   ...: print(wo_rm_attach.config)
   ...: print(rm_attach.config)

--bgp -> community-list -> prefix-list -> route-map -> untagged -> rm-attach--
router bgp 64512
 neighbor CSC peer-group
 neighbor CSC remote-as 12345
 neighbor RR peer-group
 neighbor RR remote-as 64512
 address-family ipv4
  neighbor CSC send-community both
  neighbor CSC send-label
 address-family vpnv4
!
ip community-list standard cl_PE1 permit 64512:10001
!
ip community-list standard cl_PE2 permit 64512:10002
!
ip community-list expanded cl_VPNv4_1 permit 64512:2[0-9][0-9][0-9]1
!
ip community-list expanded cl_VPNv4_2 permit 64512:2[0-9][0-9][0-9]2
!
ip prefix-list pl_CSC seq 5 permit 10.0.0.0/24 ge 32
!
route-map rm_CSC_PE_in deny 10
 match community cl_PE1 cl_PE2
!
route-map rm_CSC_PE_in permit 20
 match ip address prefix-list pl_CSC
 set local-preference 200
!
route-map rm_RR_in permit 10
 match community cl_VPNv4_1
 set local-preference 200
!
route-map rm_RR_in permit 20
 match community cl_VPNv4_2
 set local-preference 190
!
no platform punt-keepalive disable-kernel-core
!
no service dhcp
!
ip dhcp bootp ignore
!
no service pad
!
router bgp 64512
 address-family ipv4
  neighbor CSC route-map rm_CSC_PE_in in
 address-family vpnv4
  neighbor RR route-map rm_RR_in in
!
```

</details>
<br>

## Разница конфигураций

Разница конфигураций вычисляется путем сравнения деревьев текущей и целевой конфигурации. Наивная (сырая, raw) разница получается удалением отсутствующих в целевой конфигурации команд и добавление тех, которых нет в целевой. Удаление производится путем дописывания no/undo/... (в зависимости от производителя) перед командой. Во многих случаях такой подход дает рабочий результат, а в тех случаях, когда такой вариант не работает, сырая разница модифицируется PostPrecessing правилами. Набор правил наполняется постепенно по мере эксплуатации библиотеки и нахождения случаев, которые не работают через наивный вариант с добавлением no/undo/... Разница вычисляется без учета порядка команд, т.е.

```text
some command 1
some command 2
```

будет равна

```text
some command 2
some command 1
```

Для секций, где порядок важен (ACL например) есть параграф ниже.

### Наивная разница ([05.naive.diff.py](./examples/05.naive.diff.py))

Наивная разница получается путем простого добавления no/undo/... к командам, которых быть не должно, и добавлением тех команд которых нет в исходной конфигурации. Основные моменты:

- все глобальные "undo-команды" помещаются в конец. Сделано с той целью, что бы удаление (а не изменение) объектов было в самом конце, и что бы, по возможности, избежать проблем зависимостей использования (например когда prefix-list удаляется раньше, чем он перестает использоваться). Данное поведение можно отключить ключом `reorder_root` метода `diff()`.
- ко всем глобальным "undo-командам" добавляется тег `clear`. Сделано для того, что бы можно было отфильтровать удаление объектов, например если не нужно проводить очистку конфигураций (или наоборот, провести только очистку) от неописанных в целевой конфигурации настроек.

<details>
    <summary>Листинг (click me)</summary>

```python
In [1]: from conf_tree import ConfTreeEnv, Vendor
   ...: 
   ...: 
   ...: def get_configs() -> tuple[str, str]:
   ...:     with open(file="./examples/configs/cisco-naive-diff-target.txt", mode="r") as f:
   ...:         target = f.read()
   ...:     with open(file="./examples/configs/cisco-naive-diff-existed.txt", mode="r") as f:
   ...:         existed = f.read()
   ...: 
   ...:     return existed, target
   ...: 
   ...: 
   ...: def get_ct_environment() -> ConfTreeEnv:
   ...:     return ConfTreeEnv(vendor=Vendor.CISCO)
   ...: 

In [2]: existed_config, target_config = get_configs()
   ...: env = get_ct_environment()
   ...: existed = env.parse(existed_config)
   ...: target = env.parse(target_config)

In [3]: print("\n---Наивная разница конфигураций---")
   ...: diff = env.diff(a=existed, b=target)
   ...: print(diff.config)

---Наивная разница конфигураций---
interface FastEthernet0
 no switchport access vlan 100
 description User
 switchport access vlan 123
!
router bgp 64512
 address-family ipv4
  no network 192.168.100.0 mask 255.255.255.0
  network 192.168.200.1 mask 255.255.255.0
!
line vty 0 4
 transport input all
!
no router ospf 1
!

In [4]: print("\n---Наивная разница конфигураций: без очистки---")
   ...: diff_without_clear = env.search(diff, exclude_tags=["clear"])
   ...: print(diff_without_clear.config)
   ...: 

---Наивная разница конфигураций: без очистки---
interface FastEthernet0
 no switchport access vlan 100
 description User
 switchport access vlan 123
!
router bgp 64512
 address-family ipv4
  no network 192.168.100.0 mask 255.255.255.0
  network 192.168.200.1 mask 255.255.255.0
!
line vty 0 4
 transport input all
!

In [5]: print("\n---Наивная разница конфигураций: только очистка---")
   ...: diff_clear = env.search(diff, include_tags=["clear"])
   ...: print(diff_clear.config)
   ...: 

---Наивная разница конфигураций: только очистка---
no router ospf 1
!
```

</details>
<br>

### Пост-обработка разницы конфигураций ([06.postproc.diff.py](./examples/06.postproc.diff.py))

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

<details>
    <summary>Листинг (click me)</summary>

```python
In [1]: import re
   ...: 
   ...: from conf_tree import ConfTree, ConfTreeEnv, ConfTreePostProc, Vendor, register_rule

In [2]: @register_rule
   ...: class CiscoPostProcBGP(ConfTreePostProc):
   ...:     @classmethod
   ...:     def _delete_nodes(cls, ct: ConfTree, regex: str) -> None:
   ...:         nodes_to_delete: list[ConfTree] = []
   ...:         for node in ct.children.values():
   ...:             if len(node.children) != 0:
   ...:                 cls._delete_nodes(node, regex)
   ...:                 if len(node.children) == 0:
   ...:                     nodes_to_delete.append(node)
   ...:             else:
   ...:                 if re.match(regex, node.line):
   ...:                     nodes_to_delete.append(node)
   ...:         for node in nodes_to_delete:
   ...:             node.delete()
   ...:
   ...:     @classmethod
   ...:     def process(cls, ct: ConfTree) -> None:
   ...:         bgp_nodes = [node for node in ct.children.values() if node.line.startswith("router bgp ")]
   ...:         if len(bgp_nodes) != 1:
   ...:             return
   ...:         bgp = bgp_nodes[0]
   ...: 
   ...:         bgp_global = {node.line: node for node in bgp.children.values() if len(node.children) == 0}
   ...:         bgp_af = {node.line: node for node in bgp.children.values() if len(node.children) != 0}
   ...:         bgp.children = bgp_global | bgp_af
   ...: 
   ...:         regexes_to_delete = set()
   ...:         groups_to_delete = set()
   ...:         peers_to_delete = set()
   ...:         for node in bgp.children.values():
   ...:             if node.line.startswith("no neighbor ") and node.line.endswith(" peer-group"):
   ...:                 _, _, group, _ = node.line.split()
   ...:                 groups_to_delete.add(group)
   ...:         for node in bgp.children.values():
   ...:             if (
   ...:                 m := re.fullmatch(
   ...:                     pattern=rf"no neighbor (?P<peer>\S+) peer-group (?:{'|'.join(groups_to_delete)})",
   ...:                     string=node.line,
   ...:                 )
   ...:             ) is not None:
   ...:                 peers_to_delete.add(m.group("peer"))
   ...: 
   ...:         if len(groups_to_delete) != 0:
   ...:             regexes_to_delete.add(rf"no neighbor (?:{'|'.join(groups_to_delete)}) (?!peer-group)")
   ...:         if len(peers_to_delete) != 0:
   ...:             regexes_to_delete.add(rf"no neighbor (?:{'|'.join(peers_to_delete)})")
   ...: 
   ...:         if len(regexes_to_delete) != 0:
   ...:             cls._delete_nodes(bgp, "|".join(regexes_to_delete))

In [3]: def get_configs() -> tuple[str, str]:
   ...:     with open(file="./examples/configs/cisco-postproc-diff-target.txt", mode="r") as f:
   ...:         target = f.read()
   ...:     with open(file="./examples/configs/cisco-postproc-diff-existed.txt", mode="r") as f:
   ...:         existed = f.read()
   ...: 
   ...:     return existed, target
   ...: 

In [4]: def get_ct_environment_naive() -> ConfTreeEnv:
   ...:     return ConfTreeEnv(vendor=Vendor.CISCO, post_proc_rules=[])
   ...: 
   ...: 
   ...: def get_ct_environment_postproc() -> ConfTreeEnv:
   ...:     # декоратор register_rule добавляет правило в общий список и можно тут не
   ...:     # переопределять его через аргумент post_proc_rules, но если необходимо 
   ...:     # протестировать только какие-то определенные правила, тогда явно задаем их 
   ...:     # или указываем пустой список, что бы получить наивную разницу без обработки
   ...:     return ConfTreeEnv(vendor=Vendor.CISCO, post_proc_rules=[CiscoPostProcBGP])
   ...: 

In [5]: existed_config, target_config = get_configs()

In [6]: print("\n---Наивная разница конфигураций---")
   ...: env_naive = get_ct_environment_naive()
   ...: existed = env_naive.parse(existed_config)
   ...: target = env_naive.parse(target_config)
   ...: diff = env_naive.diff(a=existed, b=target)
   ...: print(diff.config)
   ...: 

---Наивная разница конфигураций---
router bgp 64512
 no neighbor RR peer-group
 no neighbor RR remote-as 64512
 no neighbor RR ebgp-multihop 255
 no neighbor RR update-source Loopback0
 no neighbor 192.168.255.2 peer-group RR
 no neighbor 192.168.255.3 peer-group RR
 address-family ipv4
  no neighbor RR send-community both
  no neighbor RR advertisement-interval 0
  no neighbor 192.168.255.2 activate
  no neighbor 192.168.255.3 activate
  neighbor 192.168.255.1 send-community both
!

In [7]: print("\n---Обработанная разница конфигураций---")
   ...: env_postproc = get_ct_environment_postproc()
   ...: existed = env_postproc.parse(existed_config)
   ...: target = env_postproc.parse(target_config)
   ...: diff = env_postproc.diff(a=existed, b=target)
   ...: print(diff.config)
   ...: 

---Обработанная разница конфигураций---
router bgp 64512
 no neighbor RR peer-group
 address-family ipv4
  neighbor 192.168.255.1 send-community both
!
```

</details>
<br>

### Секции без вычисления разницы ([07.no.diff.section.py](./examples/07.no.diff.section.py))

Некоторые секции конфигураций нужно настраивать полностью, даже если нужно поменять какой-то один параметр. Это могут быть RPL секции в IOS-XR или XPL в Huawei (смысл один и тот же: при входе в секцию, нужно применить полные её настройки). Регулярное выражение, описывающее такие секции может быть передано в качестве аргумента при создании окружения, в этом случае для этих секций в качестве разницы конфигураций будет применяться целевая конфигурация целиком, а не разница между текущей и целевой.

<details>
    <summary>Листинг (click me)</summary>

```python
In [1]: from conf_tree import ConfTreeEnv, Vendor

In [2]: def get_configs() -> tuple[str, str]:
   ...:     with open(file="./examples/configs/cisco-no-diff-section-target.txt", mode="r") as f:
   ...:         target = f.read()
   ...:     with open(file="./examples/configs/cisco-no-diff-section-existed.txt", mode="r") as f:
   ...:         existed = f.read()
   ...: 
   ...:     return existed, target
   ...: 

In [3]: def get_ct_environment_naive() -> ConfTreeEnv:
   ...:     return ConfTreeEnv(vendor=Vendor.CISCO)
   ...: 

In [4]: def get_ct_environment_no_diff() -> ConfTreeEnv:
   ...:     return ConfTreeEnv(
   ...:         vendor=Vendor.CISCO,
   ...:         no_diff_sections=[
   ...:             r"prefix-set \S+",
   ...:             r"route-policy \S+",
   ...:         ],
   ...:     )
   ...: 

In [5]: existed_config, target_config = get_configs()

In [6]: print("\n---Наивная разница конфигураций---")
   ...: env_naive = get_ct_environment_naive()
   ...: existed = env_naive.parse(existed_config)
   ...: target = env_naive.parse(target_config)
   ...: diff = env_naive.diff(a=existed, b=target)
   ...: print(diff.config)

---Наивная разница конфигураций---
interface BVI123
 no description User-OLD
 description User-NEW
!
prefix-set ps-google
 no 8.8.8.8/32
 8.8.8.8/32,
 8.8.4.4/32
!
route-policy rp-google
 elseif destination in ps-some-networks then
  drop
!

In [7]: print("\n---Разница конфигураций с учетом no-diff секций---")
   ...: env_no_diff = get_ct_environment_no_diff()
   ...: existed = env_no_diff.parse(existed_config)
   ...: target = env_no_diff.parse(target_config)
   ...: diff = env_no_diff.diff(a=existed, b=target)
   ...: print(diff.config)

---Разница конфигураций с учетом no-diff секций---
interface BVI123
 no description User-OLD
 description User-NEW
!
prefix-set ps-google
 8.8.8.8/32,
 8.8.4.4/32
!
route-policy rp-google
 if destination in ps-google then
  drop
 elseif destination in ps-some-networks then
  drop
 else
  pass
 endif
!
```

</details>
<br>

### Секции, где порядок имеет значение ([08.ordered.diff.py](./examples/08.ordered.diff.py))

В некоторых секциях (например acl) порядок записей имеет значение. Регулярное выражение, описывающее такие секции может быть передано при создании окружения. В этом случае библиотека будет стараться привести текущую конфигурацию к целевой с учетом порядка команд. Но несмотря на это, к таким секциям нужно относиться с особой внимательностью и, по возможности, избегать их. Упомянутые выше ACL могут иметь entry-number перед правилом, что избавляет от необходимости проверки порядка, так как не важно в каком порядке будут применены команды, за счет наличия entry-number в ACE, правила будут установлены в нужное место (отдельно не забываем про возможности делать re-sequence, эту команду можно, например добавить через post-processing при модификации acl, тогда будет всегда консистентный шаг между ACE).

<details>
    <summary>Листинг (click me)</summary>

```python
In [1]: from conf_tree import ConfTreeEnv, Vendor

In [2]: def get_configs() -> tuple[str, str]:
   ...:     with open(file="./examples/configs/cisco-ordered-diff-target.txt", mode="r") as f:
   ...:         target = f.read()
   ...:     with open(file="./examples/configs/cisco-ordered-diff-existed.txt", mode="r") as f:
   ...:         existed = f.read()
   ...: 
   ...:     return existed, target
   ...: 

In [3]: def get_ct_environment_naive() -> ConfTreeEnv:
   ...:     return ConfTreeEnv(vendor=Vendor.CISCO)
   ...: 
   ...: 
   ...: def get_ct_environment_ordered() -> ConfTreeEnv:
   ...:     return ConfTreeEnv(
   ...:         vendor=Vendor.CISCO,
   ...:         ordered_sections=[
   ...:             r"ip access-list standard \S+$",
   ...:         ],
   ...:     )
   ...: 

In [4]: existed_config, target_config = get_configs()

In [5]: print("\n---Наивная разница конфигураций---")
   ...: env_naive = get_ct_environment_naive()
   ...: existed = env_naive.parse(existed_config)
   ...: target = env_naive.parse(target_config)
   ...: diff = env_naive.diff(a=existed, b=target)
   ...: print(diff.config)

---Наивная разница конфигураций---
ip access-list standard acl_TEST_STD
 permit 8.8.4.4
!
ip access-list extended act_TEST_EXT
 15 permit ip host 8.8.4.4 any
!

In [6]: print("\n---Разница конфигураций с учетом секций со значимым порядком---")
   ...: env_ordered = get_ct_environment_ordered()
   ...: existed = env_ordered.parse(existed_config)
   ...: target = env_ordered.parse(target_config)
   ...: diff = env_ordered.diff(a=existed, b=target)
   ...: print(diff.config)

---Разница конфигураций с учетом секций со значимым порядком---
ip access-list standard acl_TEST_STD
 no deny   any
 permit 8.8.4.4
 deny   any
!
ip access-list extended act_TEST_EXT
 15 permit ip host 8.8.4.4 any
!
```

</details>
<br>

## История версий

- 0.1.0 - код залит на github, протестирован cicd
- 0.1.1 - добавлен readme, некоторые тесты

## TODO

- Добавить возможность указывать шаблон для команд, что бы при вычислении разницы можно было понять где аргументы, и если есть возможность, обойтись без undo-команды, так как команда с новыми аргументами перезапишет существующие опции.
- Добавить возможность указывать как правильно удалять команды (часто это делается без указания опций и приходится править удаление команд через post-processing)


            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "conf-tree",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.10",
    "maintainer_email": null,
    "keywords": "cisco, arista, huawei, network, automation, configuration",
    "author": "Alexander Ignatov",
    "author_email": "ignatov.alx@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/a3/fd/faf52839d1770822bddbf2c023973b17b7e0c043528ae79e9388a8c1ad9c/conf_tree-0.1.1.tar.gz",
    "platform": null,
    "description": "# \u0411\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 Conf-Tree\n\n- [\u0411\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 Conf-Tree](#\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430-conf-tree)\n  - [\u041a\u0440\u0430\u0442\u043a\u043e\u0435 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435](#\u043a\u0440\u0430\u0442\u043a\u043e\u0435-\u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435)\n  - [\u0411\u044b\u0441\u0442\u0440\u044b\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 (00.quick-start.py)](#\u0431\u044b\u0441\u0442\u0440\u044b\u0439-\u043f\u0440\u0438\u043c\u0435\u0440-00quick-startpy)\n  - [\u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0432 \u0434\u0435\u0440\u0435\u0432\u043e (01.parsing.py)](#\u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435-\u0432-\u0434\u0435\u0440\u0435\u0432\u043e-01parsingpy)\n  - [\u041f\u043e\u0438\u0441\u043a/\u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f (02.searching.py)](#\u043f\u043e\u0438\u0441\u043a\u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f-02searchingpy)\n  - [\u0421\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f/\u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f (03.serialization.py)](#\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f\u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f-03serializationpy)\n  - [\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043f\u043e\u0440\u044f\u0434\u043a\u0430 (04.reorder.py)](#\u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435-\u043f\u043e\u0440\u044f\u0434\u043a\u0430-04reorderpy)\n  - [\u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439](#\u0440\u0430\u0437\u043d\u0438\u0446\u0430-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439)\n    - [\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 (05.naive.diff.py)](#\u043d\u0430\u0438\u0432\u043d\u0430\u044f-\u0440\u0430\u0437\u043d\u0438\u0446\u0430-05naivediffpy)\n    - [\u041f\u043e\u0441\u0442-\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0440\u0430\u0437\u043d\u0438\u0446\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 (06.postproc.diff.py)](#\u043f\u043e\u0441\u0442-\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430-\u0440\u0430\u0437\u043d\u0438\u0446\u044b-\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439-06postprocdiffpy)\n    - [\u0421\u0435\u043a\u0446\u0438\u0438 \u0431\u0435\u0437 \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u044b (07.no.diff.section.py)](#\u0441\u0435\u043a\u0446\u0438\u0438-\u0431\u0435\u0437-\u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f-\u0440\u0430\u0437\u043d\u0438\u0446\u044b-07nodiffsectionpy)\n    - [\u0421\u0435\u043a\u0446\u0438\u0438, \u0433\u0434\u0435 \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u0438\u043c\u0435\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 (08.ordered.diff.py)](#\u0441\u0435\u043a\u0446\u0438\u0438-\u0433\u0434\u0435-\u043f\u043e\u0440\u044f\u0434\u043e\u043a-\u0438\u043c\u0435\u0435\u0442-\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435-08ordereddiffpy)\n  - [\u0418\u0441\u0442\u043e\u0440\u0438\u044f \u0432\u0435\u0440\u0441\u0438\u0439](#\u0438\u0441\u0442\u043e\u0440\u0438\u044f-\u0432\u0435\u0440\u0441\u0438\u0439)\n  - [TODO](#todo)\n\n## \u041a\u0440\u0430\u0442\u043a\u043e\u0435 \u043e\u043f\u0438\u0441\u0430\u043d\u0438\u0435\n\n\u0411\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0435\u0439 \u0441\u0435\u0442\u0435\u0432\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432:\n\n- \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0432 \u0434\u0435\u0440\u0435\u0432\u043e\n- \u043f\u043e\u0438\u0441\u043a/\u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438\n- \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0435 \u0440\u0430\u0437\u043d\u0438\u0446\u044b (diff) \u043c\u0435\u0436\u0434\u0443 \u0434\u0432\u0443\u043c\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f\u043c\u0438\n\n## \u0411\u044b\u0441\u0442\u0440\u044b\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 ([00.quick-start.py](./examples/00.quick-start.py))\n\n- \u0427\u0438\u0442\u0430\u0435\u043c \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0438 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0438\u0437 \u0444\u0430\u0439\u043b\u043e\u0432\n- \u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u0443\u0435\u043c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0432 \u0434\u0435\u0440\u0435\u0432\u044c\u044f, \u043f\u043e\u043f\u0443\u0442\u043d\u043e \u0440\u0430\u0437\u043c\u0435\u0447\u0430\u044f \u0442\u0435\u0433\u0430\u043c\u0438 \u0441\u0435\u043a\u0446\u0438\u0438 bgp \u0438 static routes\n- \u041f\u043e\u043b\u0443\u0447\u0430\u0435\u043c \u0440\u0430\u0437\u043d\u0438\u0446\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439\n- \u0424\u0438\u043b\u044c\u0442\u0440\u0443\u0435\u043c \u0440\u0430\u0437\u043d\u0438\u0446\u0443 (\u0430 \u043c\u043e\u0436\u043d\u043e \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u0435\u043a\u0443\u0449\u0435\u0435/\u0446\u0435\u043b\u0435\u0432\u043e\u0435 \u0434\u0435\u0440\u0435\u0432\u044c\u044f, \u0430 \u043f\u043e\u0442\u043e\u043c \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0442\u044c \u0440\u0430\u0437\u043d\u0438\u0446\u0443 \u043c\u0435\u0436\u0434\u0443 \u043d\u0438\u043c\u0438)\n\n<details>\n    <summary>\u041b\u0438\u0441\u0442\u0438\u043d\u0433 (click me)</summary>\n\n```python\nIn [2]: from conf_tree import ConfTreeEnv, Vendor\n\nIn [3]: def get_configs() -> tuple[str, str]:\n   ...:     with open(file=\"./examples/configs/cisco-router-1.txt\", mode=\"r\") as f:\n   ...:         current_config = f.read()\n   ...:     with open(file=\"./examples/configs/cisco-router-2.txt\", mode=\"r\") as f:\n   ...:         target_config = f.read()\n   ...:     return current_config, target_config\n   ...: \n\nIn [4]: def get_ct_environment() -> ConfTreeEnv:\n   ...:     tagging_rules: list[dict[str, str | list[str]]] = [\n   ...:         {\"regex\": r\"^router bgp \\d+$\", \"tags\": [\"bgp\"]},\n   ...:         {\"regex\": r\"^ip route \\S+\", \"tags\": [\"static\"]},\n   ...:     ]\n   ...:     return ConfTreeEnv(\n   ...:         vendor=Vendor.CISCO,\n   ...:         tagging_rules=tagging_rules,\n   ...:     )\n   ...: \n\nIn [5]: current_config, target_config = get_configs()\n\nIn [6]: env = get_ct_environment()\n\nIn [7]: current = env.parse(current_config)\n\nIn [8]: target = env.parse(target_config)\n\nIn [9]: diff = env.diff(current, target)\n\nIn [10]: print(\"\\n!-- \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 --\")\n    ...: print(diff.config)\n    ...: \n\n!-- \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 --\ninterface Tunnel2\n no ip ospf priority 0\n ip ospf priority 1\n!\nrouter bgp 64512\n no neighbor RR peer-group\n address-family ipv4\n  network 10.255.255.1 mask 255.255.255.255\n!\nline vty 0 4\n no exec-timeout 15 0\n exec-timeout 10 0\n!\nline vty 5 15\n no exec-timeout 15 0\n exec-timeout 10 0\n!\nip name-server 192.168.0.9\n!\nno ip name-server 192.168.0.3\n!\nno ip route 192.168.255.1 255.255.255.255 Tunnel2\n!\nno ip route vrf FVRF 192.66.55.44 255.255.255.255 143.31.31.2\n!\n\nIn [11]: print(\"\\n!-- \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u0431\u0435\u0437 \u0441\u0435\u043a\u0446\u0438\u0439 \u0441 \u0442\u0435\u0433\u0430\u043c\u0438 bgp \u0438 static --\")\n    ...: diff_no_routing = env.search(diff, exclude_tags=[\"bgp\", \"static\"])\n    ...: print(diff_no_routing.config)\n    ...: \n\n!-- \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u0431\u0435\u0437 \u0441\u0435\u043a\u0446\u0438\u0439 \u0441 \u0442\u0435\u0433\u0430\u043c\u0438 bgp \u0438 static --\ninterface Tunnel2\n no ip ospf priority 0\n ip ospf priority 1\n!\nline vty 0 4\n no exec-timeout 15 0\n exec-timeout 10 0\n!\nline vty 5 15\n no exec-timeout 15 0\n exec-timeout 10 0\n!\nip name-server 192.168.0.9\n!\nno ip name-server 192.168.0.3\n!\n\nIn [12]: print(\"\\n!-- \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u0432 \u0441\u0435\u043a\u0446\u0438\u0438 \u0441 \u0442\u0435\u0433\u043e\u043c bgp --\")\n    ...: diff_bgp = env.search(diff, include_tags=[\"bgp\"])\n    ...: print(diff_bgp.config)\n    ...: \n\n!-- \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u0432 \u0441\u0435\u043a\u0446\u0438\u0438 \u0441 \u0442\u0435\u0433\u043e\u043c bgp --\nrouter bgp 64512\n no neighbor RR peer-group\n address-family ipv4\n  network 10.255.255.1 mask 255.255.255.255\n!\n```\n\n</details>\n<br>\n\n## \u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0432 \u0434\u0435\u0440\u0435\u0432\u043e ([01.parsing.py](./examples/01.parsing.py))\n\n- \u041f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0432 \u0434\u0435\u0440\u0435\u0432\u043e \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u043e\u0442\u0441\u0442\u0443\u043f\u043e\u0432 \u0432 \u0442\u0435\u043a\u0441\u0442\u0435\n- \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0440\u0430\u0437\u043c\u0435\u0447\u0430\u0442\u044c \u0441\u0435\u043a\u0446\u0438\u0438/\u0441\u0442\u0440\u043e\u043a\u0438 \u0442\u0435\u0433\u0430\u043c\u0438 \u0434\u043b\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u0439 \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438\n- pre-run \u0438 post-run \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0430 \u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0432\u0448\u0435\u0433\u043e\u0441\u044f \u0434\u0435\u0440\u0435\u0432\u0430, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u043d\u043e\u0440\u043c\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u0432\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0430, \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0431\u0430\u043d\u043d\u0435\u0440\u043e\u0432 (cisco) \u0438 \u043f\u0440.\n\n<details>\n    <summary>\u041b\u0438\u0441\u0442\u0438\u043d\u0433 (click me)</summary>\n\n```python\nIn [1]: from conf_tree import ConfTreeEnv, Vendor\n\nIn [2]: def get_configs() -> str:\n   ...:     with open(file=\"./examples/configs/cisco-example-1.txt\", mode=\"r\") as f:\n   ...:         config = f.read()\n   ...:     return config\n   ...: \n\nIn [3]: def get_ct_environment() -> ConfTreeEnv:\n   ...:     return ConfTreeEnv(vendor=Vendor.CISCO)\n   ...: \n\nIn [4]: config_config = get_configs()\n\nIn [5]: env = get_ct_environment()\n\nIn [6]: current = env.parse(config_config)\n\nIn [7]: print(\"\\n---\u0434\u0435\u0440\u0435\u0432\u043e \u0432 \u0432\u0438\u0434\u0435 \u043f\u0440\u0438\u0432\u044b\u0447\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438---\")\n   ...: print(current.config)\n\n---\u0434\u0435\u0440\u0435\u0432\u043e \u0432 \u0432\u0438\u0434\u0435 \u043f\u0440\u0438\u0432\u044b\u0447\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438---\nservice tcp-keepalives-in\n!\nservice timestamps debug datetime msec localtime show-timezone\n!\nenable secret 5 2Fe034RYzgb7xbt2pYxcpA==\n!\naaa group server tacacs+ TacacsGroup\n server 192.168.0.100\n server 192.168.0.101\n!\ninterface Tunnel1\n ip address 10.0.0.2 255.255.255.0\n no ip redirects\n!\ninterface Tunnel2\n ip address 10.1.0.2 255.255.255.0\n no ip redirects\n!\ninterface FastEthernet0\n switchport access vlan 100\n no ip address\n!\nrouter bgp 64512\n neighbor 192.168.255.1 remote-as 64512\n neighbor 192.168.255.1 update-source Loopback0\n address-family ipv4\n  network 192.168.100.0 mask 255.255.255.0\n  neighbor 192.168.255.1 activate\n!\n\nIn [8]: print(\"\\n---\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0441 \u043c\u0430\u0441\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u0430\u043c\u0438---\")\n   ...: print(current.masked_config)\n\n---\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0441 \u043c\u0430\u0441\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u0430\u043c\u0438---\nservice tcp-keepalives-in\n!\nservice timestamps debug datetime msec localtime show-timezone\n!\nenable secret 5 ******\n!\naaa group server tacacs+ TacacsGroup\n server 192.168.0.100\n server 192.168.0.101\n!\ninterface Tunnel1\n ip address 10.0.0.2 255.255.255.0\n no ip redirects\n!\ninterface Tunnel2\n ip address 10.1.0.2 255.255.255.0\n no ip redirects\n!\ninterface FastEthernet0\n switchport access vlan 100\n no ip address\n!\nrouter bgp 64512\n neighbor 192.168.255.1 remote-as 64512\n neighbor 192.168.255.1 update-source Loopback0\n address-family ipv4\n  network 192.168.100.0 mask 255.255.255.0\n  neighbor 192.168.255.1 activate\n!\n\nIn [9]: print(\"\\n---\u0434\u0435\u0440\u0435\u0432\u043e \u0432 \u0432\u0438\u0434\u0435 \u043f\u0430\u0442\u0447\u0430 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430---\")\n   ...: print(current.patch)\n\n---\u0434\u0435\u0440\u0435\u0432\u043e \u0432 \u0432\u0438\u0434\u0435 \u043f\u0430\u0442\u0447\u0430 \u0434\u043b\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430---\nservice tcp-keepalives-in\nservice timestamps debug datetime msec localtime show-timezone\nenable secret 5 2Fe034RYzgb7xbt2pYxcpA==\naaa group server tacacs+ TacacsGroup\nserver 192.168.0.100\nserver 192.168.0.101\nexit\ninterface Tunnel1\nip address 10.0.0.2 255.255.255.0\nno ip redirects\nexit\ninterface Tunnel2\nip address 10.1.0.2 255.255.255.0\nno ip redirects\nexit\ninterface FastEthernet0\nswitchport access vlan 100\nno ip address\nexit\nrouter bgp 64512\nneighbor 192.168.255.1 remote-as 64512\nneighbor 192.168.255.1 update-source Loopback0\naddress-family ipv4\nnetwork 192.168.100.0 mask 255.255.255.0\nneighbor 192.168.255.1 activate\nexit\nexit\n\nIn [10]: print(\"\\n---\u043f\u0430\u0442\u0447 \u0441 \u043c\u0430\u0441\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u0430\u043c\u0438---\")\n    ...: print(current.masked_patch)\n\n---\u043f\u0430\u0442\u0447 \u0441 \u043c\u0430\u0441\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c\u0438 \u0441\u0435\u043a\u0440\u0435\u0442\u0430\u043c\u0438---\nservice tcp-keepalives-in\nservice timestamps debug datetime msec localtime show-timezone\nenable secret 5 ******\naaa group server tacacs+ TacacsGroup\nserver 192.168.0.100\nserver 192.168.0.101\nexit\ninterface Tunnel1\nip address 10.0.0.2 255.255.255.0\nno ip redirects\nexit\ninterface Tunnel2\nip address 10.1.0.2 255.255.255.0\nno ip redirects\nexit\ninterface FastEthernet0\nswitchport access vlan 100\nno ip address\nexit\nrouter bgp 64512\nneighbor 192.168.255.1 remote-as 64512\nneighbor 192.168.255.1 update-source Loopback0\naddress-family ipv4\nnetwork 192.168.100.0 mask 255.255.255.0\nneighbor 192.168.255.1 activate\nexit\nexit\n\nIn [11]: print(\"\\n---\u0434\u0435\u0440\u0435\u0432\u043e \u0432 \u0432\u0438\u0434\u0435 \u0444\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 (\u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e formal \u0432 ios-xr)---\")\n    ...: print(current.formal_config)\n\n---\u0434\u0435\u0440\u0435\u0432\u043e \u0432 \u0432\u0438\u0434\u0435 \u0444\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 (\u0430\u043d\u0430\u043b\u043e\u0433\u0438\u0447\u043d\u043e formal \u0432 ios-xr)---\nservice tcp-keepalives-in\nservice timestamps debug datetime msec localtime show-timezone\nenable secret 5 2Fe034RYzgb7xbt2pYxcpA==\naaa group server tacacs+ TacacsGroup / server 192.168.0.100\naaa group server tacacs+ TacacsGroup / server 192.168.0.101\ninterface Tunnel1 / ip address 10.0.0.2 255.255.255.0\ninterface Tunnel1 / no ip redirects\ninterface Tunnel2 / ip address 10.1.0.2 255.255.255.0\ninterface Tunnel2 / no ip redirects\ninterface FastEthernet0 / switchport access vlan 100\ninterface FastEthernet0 / no ip address\nrouter bgp 64512 / neighbor 192.168.255.1 remote-as 64512\nrouter bgp 64512 / neighbor 192.168.255.1 update-source Loopback0\nrouter bgp 64512 / address-family ipv4 / network 192.168.100.0 mask 255.255.255.0\nrouter bgp 64512 / address-family ipv4 / neighbor 192.168.255.1 activate\n```\n\n</details>\n<br>\n\n## \u041f\u043e\u0438\u0441\u043a/\u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u044f ([02.searching.py](./examples/02.searching.py))\n\n- \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0442\u0435\u0433\u043e\u0432, \u043f\u0440\u043e\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044b\u0445 \u0432\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0432 \u0434\u0435\u0440\u0435\u0432\u043e\n- \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u043e \u0441\u0442\u0440\u043e\u043a\u0435 (regex)\n- \u0432 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0435 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f \u043a\u043e\u043f\u0438\u044f \u0434\u0435\u0440\u0435\u0432\u0430 \u0441 \u043a\u043e\u0442\u043e\u0440\u043e\u0439 \u043c\u043e\u0436\u043d\u043e \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0442\u0430\u043a \u0436\u0435, \u043a\u0430\u043a \u0438 \u0441 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b\u043e\u043c\n\n<details>\n    <summary>\u041b\u0438\u0441\u0442\u0438\u043d\u0433 (click me)</summary>\n\n```python\nIn [1]: from conf_tree import ConfTreeEnv, Vendor\n   ...: \n   ...: \n   ...: def get_configs() -> str:\n   ...:     with open(file=\"./examples/configs/cisco-example-1.txt\", mode=\"r\") as f:\n   ...:         config = f.read()\n   ...:     return config\n   ...: \n   ...: \n   ...: def get_ct_environment() -> ConfTreeEnv:\n   ...:     tagging_rules: list[dict[str, str | list[str]]] = [\n   ...:         {\"regex\": r\"^router bgp \\d+$\", \"tags\": [\"bgp\"]},\n   ...:         {\"regex\": r\"^interface (Tunnel1) / ip address .*\", \"tags\": [\"interface\", \"tunnel-1-ip\"]},\n   ...:         {\"regex\": r\"^interface (Tunnel2) / ip address .*\", \"tags\": [\"interface\", \"tunnel-1-ip\"]},\n   ...:         {\"regex\": r\"^interface (\\S+)$\", \"tags\": [\"interface\"]},\n   ...:     ]\n   ...:     return ConfTreeEnv(\n   ...:         vendor=Vendor.CISCO,\n   ...:         tagging_rules=tagging_rules,\n   ...:     )\n   ...: \n\nIn [2]: config_config = get_configs()\n   ...: env = get_ct_environment()\n   ...: router = env.parse(config_config)\n\nIn [3]: print(\"\\n---\u0432\u0441\u0435 \u0432\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f 'address'---\")\n   ...: address = env.search(router, string=\"address\")\n   ...: print(address.config)\n   ...: \n\n---\u0432\u0441\u0435 \u0432\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f 'address'---\ninterface Tunnel1\n ip address 10.0.0.2 255.255.255.0\n!\ninterface Tunnel2\n ip address 10.1.0.2 255.255.255.0\n!\ninterface FastEthernet0\n no ip address\n!\nrouter bgp 64512\n address-family ipv4\n!\n\nIn [4]: print(\"\\n---\u0432\u0441\u0435 \u0432\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f 'address' \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u043c\u0438 \u043f\u043e\u0442\u043e\u043c\u043a\u0430\u043c\u0438---\")\n   ...: address_children = env.search(router, string=\"address\", include_children=True)\n   ...: print(address_children.config)\n   ...: \n\n---\u0432\u0441\u0435 \u0432\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f 'address' \u0441 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u044b\u043c\u0438 \u043f\u043e\u0442\u043e\u043c\u043a\u0430\u043c\u0438---\ninterface Tunnel1\n ip address 10.0.0.2 255.255.255.0\n!\ninterface Tunnel2\n ip address 10.1.0.2 255.255.255.0\n!\ninterface FastEthernet0\n no ip address\n!\nrouter bgp 64512\n address-family ipv4\n  network 192.168.100.0 mask 255.255.255.0\n  neighbor 192.168.255.1 activate\n!\n\nIn [5]: print(\"\\n---\u0432\u0441\u0435 \u0432\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f 'address \\d{1,3}'---\")\n   ...: address_ip = env.search(router, string=r\"address \\d{1,3}\")\n   ...: print(address_ip.config)\n   ...: \n\n---\u0432\u0441\u0435 \u0432\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f 'address \\d{1,3}'---\ninterface Tunnel1\n ip address 10.0.0.2 255.255.255.0\n!\ninterface Tunnel2\n ip address 10.1.0.2 255.255.255.0\n!\n\nIn [6]: print(\"\\n---\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e \u0442\u0435\u0433\u0443 'bgp'---\")\n   ...: bgp = env.search(router, include_tags=[\"bgp\"])\n   ...: print(bgp.masked_config)\n   ...: \n\n---\u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043f\u043e \u0442\u0435\u0433\u0443 'bgp'---\nrouter bgp 64512\n neighbor 192.168.255.1 remote-as 64512\n neighbor 192.168.255.1 update-source Loopback0\n address-family ipv4\n  network 192.168.100.0 mask 255.255.255.0\n  neighbor 192.168.255.1 activate\n!\n\nIn [7]: print(\"\\n---\u0432\u0441\u0435, \u043a\u0440\u043e\u043c\u0435 \u0442\u0435\u0433\u0430 'bgp'---\")\n   ...: no_bgp = env.search(router, exclude_tags=[\"bgp\"])\n   ...: print(no_bgp.masked_config)\n   ...: \n\n---\u0432\u0441\u0435, \u043a\u0440\u043e\u043c\u0435 \u0442\u0435\u0433\u0430 'bgp'---\nservice tcp-keepalives-in\n!\nservice timestamps debug datetime msec localtime show-timezone\n!\nenable secret 5 ******\n!\naaa group server tacacs+ TacacsGroup\n server 192.168.0.100\n server 192.168.0.101\n!\ninterface Tunnel1\n ip address 10.0.0.2 255.255.255.0\n no ip redirects\n!\ninterface Tunnel2\n ip address 10.1.0.2 255.255.255.0\n no ip redirects\n!\ninterface FastEthernet0\n switchport access vlan 100\n no ip address\n!\n```\n\n</details>\n<br>\n\n\u0420\u0435\u0433\u0443\u043b\u044f\u0440\u043d\u044b\u0435 \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u043f\u0438\u0448\u0443\u0442\u0441\u044f \u0434\u043b\u044f formal \u0432\u0438\u0434\u0430, \u0442.\u0435. \u0441\u0442\u0440\u043e\u043a\u0438 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c \u0438\u0435\u0440\u0430\u0440\u0445\u0438\u0438 \u043d\u0430\u0434 \u043d\u0435\u0439. \u042d\u0442\u043e \u0434\u0430\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u0435\u0433\u0438 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c \u0442\u043e\u0433\u043e, \u0432 \u043a\u0430\u043a\u043e\u0439 \u0441\u0435\u043a\u0446\u0438\u0438 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0442\u0440\u043e\u043a\u0430:\n\n```text\ninterface Tunnel1 / ip address 10.0.0.2 255.255.255.0\ninterface Tunnel2 / ip address 10.1.0.2 255.255.255.0\n```\n\n\u041d\u0430 ip \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 Tunnel1 \u0432\u0435\u0448\u0430\u0435\u043c \u0442\u0435\u0433 \"tunnel-1-ip\", \u043d\u0430 ip \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 Tunnel2 \u0432\u0435\u0448\u0430\u0435\u043c \u0442\u0435\u0433 \"tunnel-2-ip\"\n\n```python\n{\n    \"regex\": r\"^interface (Tunnel1) / ip address \\S+ \\S+(?: )?(secondary)?$\",\n    \"tags\": [\"interface\", \"tunnel-1-ip\"],\n},\n{\n    \"regex\": r\"^interface (Tunnel2) / ip address \\S+ \\S+(?: )?(secondary)?$\",\n    \"tags\": [\"interface\", \"tunnel-1-ip\"],\n},\n```\n\n\u0415\u0441\u043b\u0438 \u0432 \u0440\u0435\u0433\u0443\u043b\u044f\u0440\u043d\u043e\u043c \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u0438 \u0435\u0441\u0442\u044c \u043d\u0435\u0438\u043c\u0435\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0433\u0440\u0443\u043f\u043f\u044b, \u0442\u043e \u0438\u0445 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u043c\u043e\u0435 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u043f\u0430\u0434\u0430\u0435\u0442 \u0432 \u0442\u0435\u0433\u0438:\n\n```python\n{\n    \"regex\": r\"^interface (\\S+)$\",\n    \"tags\": [\"interface\"],\n},\n```\n\n\u041f\u043e\u043c\u0438\u043c\u043e \u0442\u0435\u0433\u0430 \"interface\", \u043d\u0430 \u0441\u0442\u0440\u043e\u043a\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u0443\u0434\u0435\u0442 \u0442\u0430\u043a \u0436\u0435 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d \u0442\u0435\u0433, \u0440\u0430\u0432\u043d\u044b\u0439 \u0438\u043c\u0435\u043d\u0438 \u0441\u0430\u043c\u043e\u0433\u043e.\n\n\u0415\u0441\u043b\u0438 \u0441\u0442\u0440\u043e\u043a\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0435 \u043f\u043e\u043f\u0430\u043b\u0430 \u043d\u0435 \u0432 \u043e\u0434\u043d\u043e \u0438\u0437 \u043f\u0440\u0430\u0432\u0438\u043b, \u0442\u043e\u0433\u0434\u0430 \u0442\u0435\u0433\u0438 \u0434\u043b\u044f \u043d\u0435\u0435 \u0431\u0435\u0440\u0443\u0442\u0441\u044f \u0438\u0437 \u0432\u044b\u0448\u0435\u0441\u0442\u043e\u044f\u0449\u0435\u0433\u043e \u0443\u0440\u043e\u0432\u043d\u044f. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0435\u0441\u043b\u0438 \u043d\u0430 \"interface Loopback0\" \u0431\u044b\u043b\u0438 \u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u044b \u0442\u0435\u0433\u0438 [\"interface\", \"Loopback0\"], \u0442\u043e \u0432\u0441\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u043f\u043e\u0434 \u044d\u0442\u043e\u0439 \u0441\u0435\u043a\u0446\u0438\u0435\u0439 \u0442\u0430\u043a \u0436\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u043c\u0435\u0442\u044c \u044d\u0442\u0438 \u0442\u0435\u0433\u0438, \u0435\u0441\u043b\u0438 \u044f\u0432\u043d\u043e \u043d\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0448\u0443\u0442\u0441\u044f \u0431\u043e\u043b\u0435\u0435 \u0443\u0437\u043a\u0438\u043c\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c\u0438.\n\n## \u0421\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f/\u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f ([03.serialization.py](./examples/03.serialization.py))\n\n\u041f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0434\u0435\u0440\u0435\u0432\u043e \u0432 \u0441\u043b\u043e\u0432\u0430\u0440\u044c \u0438 \u0432\u043e\u0441\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u0435\u0440\u0435\u0432\u043e \u0438\u0437 \u0441\u043b\u043e\u0432\u0430\u0440\u044f, \u0434\u0430\u043b\u044c\u0448\u0435 \u0432 json, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0441\u043b\u043e\u0436\u0438\u0442\u044c \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0432 \u0431\u0430\u0437\u0443/\u043e\u0442\u0434\u0430\u0442\u044c \u0447\u0435\u0440\u0435\u0437 API.\n\n<details>\n    <summary>\u041b\u0438\u0441\u0442\u0438\u043d\u0433 (click me)</summary>\n\n```python\nIn [1]: from conf_tree import ConfTreeEnv, Vendor\n   ...: \n   ...: \n   ...: def get_configs() -> str:\n   ...:     with open(file=\"./examples/configs/cisco-example-2.txt\", mode=\"r\") as f:\n   ...:         config = f.read()\n   ...:     return config\n   ...: \n   ...: \n   ...: def get_ct_environment() -> ConfTreeEnv:\n   ...:     tagging_rules: list[dict[str, str | list[str]]] = [\n   ...:         {\"regex\": r\"^router bgp \\d+$\", \"tags\": [\"bgp\"]},\n   ...:         {\"regex\": r\"^interface (\\S+)$\", \"tags\": [\"interface\"]},\n   ...:     ]\n   ...:     return ConfTreeEnv(\n   ...:         vendor=Vendor.CISCO,\n   ...:         tagging_rules=tagging_rules,\n   ...:     )\n   ...: \n\nIn [2]: config = get_configs()\n   ...: env = get_ct_environment()\n   ...: router_original = env.parse(config)\n   ...: \n\nIn [3]: config_dict = env.to_dict(router_original)\n   ...: print(\"\\n---\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f---\")\n   ...: print(config_dict)\n   ...: \n\n---\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f---\n{'line': '', 'tags': [], 'children': {'service tcp-keepalives-in': {'line': 'service tcp-keepalives-in', 'tags': [], 'children': {}}, 'service timestamps debug datetime msec localtime show-timezone': {'line': 'service timestamps debug datetime msec localtime show-timezone', 'tags': [], 'children': {}}, 'interface FastEthernet0': {'line': 'interface FastEthernet0', 'tags': ['interface', 'FastEthernet0'], 'children': {'switchport access vlan 100': {'line': 'switchport access vlan 100', 'tags': ['interface', 'FastEthernet0'], 'children': {}}, 'no ip address': {'line': 'no ip address', 'tags': ['interface', 'FastEthernet0'], 'children': {}}}}, 'router bgp 64512': {'line': 'router bgp 64512', 'tags': ['bgp'], 'children': {'neighbor 192.168.255.1 remote-as 64512': {'line': 'neighbor 192.168.255.1 remote-as 64512', 'tags': ['bgp'], 'children': {}}, 'neighbor 192.168.255.1 update-source Loopback0': {'line': 'neighbor 192.168.255.1 update-source Loopback0', 'tags': ['bgp'], 'children': {}}, 'address-family ipv4': {'line': 'address-family ipv4', 'tags': ['bgp'], 'children': {'network 192.168.100.0 mask 255.255.255.0': {'line': 'network 192.168.100.0 mask 255.255.255.0', 'tags': ['bgp'], 'children': {}}, 'neighbor 192.168.255.1 activate': {'line': 'neighbor 192.168.255.1 activate', 'tags': ['bgp'], 'children': {}}}}}}}}\n\nIn [4]: router_restored = env.from_dict(config_dict)\n   ...: print(\"\\n---\u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f---\")\n   ...: print(router_restored.patch)\n\n---\u0434\u0435\u0441\u0435\u0440\u0438\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f---\nservice tcp-keepalives-in\nservice timestamps debug datetime msec localtime show-timezone\ninterface FastEthernet0\nswitchport access vlan 100\nno ip address\nexit\nrouter bgp 64512\nneighbor 192.168.255.1 remote-as 64512\nneighbor 192.168.255.1 update-source Loopback0\naddress-family ipv4\nnetwork 192.168.100.0 mask 255.255.255.0\nneighbor 192.168.255.1 activate\nexit\nexit\n\nIn [5]: print(\"\\n---\u0440\u0430\u0432\u0435\u043d\u0441\u0442\u0432\u043e \u0434\u0432\u0443\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432---\")\n   ...: print(router_original == router_restored)\n\n---\u0440\u0430\u0432\u0435\u043d\u0441\u0442\u0432\u043e \u0434\u0432\u0443\u0445 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432---\nTrue\n```\n\n</details>\n<br>\n\n## \u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u043f\u043e\u0440\u044f\u0434\u043a\u0430 ([04.reorder.py](./examples/04.reorder.py))\n\n\u0423 \u0434\u0435\u0440\u0435\u0432\u0430 \u0435\u0441\u0442\u044c \u043c\u0435\u0442\u043e\u0434 `reorder()`, \u043a\u043e\u0442\u043e\u0440\u044b\u0439 \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043e\u0442\u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0432 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u043c \u043f\u043e\u0440\u044f\u0434\u043a\u0435. \u041d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0432 \u0441\u043b\u0443\u0447\u0430\u044f\u0445, \u043a\u043e\u0433\u0434\u0430 \u0441\u043d\u0430\u0447\u0430\u043b\u0430 \u043d\u0443\u0436\u043d\u043e \u0441\u043e\u0431\u043b\u044e\u0441\u0442\u0438 \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438: \u0441\u043d\u0430\u0447\u0430\u043b\u0430 prefix-lists, \u0437\u0430\u0442\u0435\u043c route-maps (\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0435 prefix-lists), \u0437\u0430\u0442\u0435\u043c \u043d\u0430\u0437\u043d\u0430\u0447\u0438\u0442\u044c \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u044b\u0435 route-maps \u043d\u0430 bgp \u043f\u0438\u0440\u043e\u0432.\n\n<details>\n    <summary>\u041b\u0438\u0441\u0442\u0438\u043d\u0433 (click me)</summary>\n\n```python\nIn [1]: from conf_tree import ConfTreeEnv, Vendor\n   ...: \n   ...: \n   ...: def get_configs() -> str:\n   ...:     with open(file=\"./examples/configs/cisco-example-4.txt\", mode=\"r\") as f:\n   ...:         config = f.read()\n   ...:     return config\n   ...: \n   ...: \n   ...: def get_ct_environment() -> ConfTreeEnv:\n   ...:     tagging_rules: list[dict[str, str | list[str]]] = [\n   ...:         {\"regex\": r\"^router bgp .* neighbor (\\S+) route-map (\\S+) (?:in|out)\", \"tags\": [\"rm-attach\"]},\n   ...:         {\"regex\": r\"^router bgp \\d+$\", \"tags\": [\"bgp\"]},\n   ...:         {\"regex\": r\"^route-map (\\S+) (?:permit|deny) \\d+$\", \"tags\": [\"rm\"]},\n   ...:         {\"regex\": r\"^ip community-list (?:standard|expanded) (\\S+)\", \"tags\": [\"cl\"]},\n   ...:         {\"regex\": r\"^ip prefix-list (\\S+)\", \"tags\": [\"pl\"]},\n   ...:     ]\n   ...:     return ConfTreeEnv(\n   ...:         vendor=Vendor.CISCO,\n   ...:         tagging_rules=tagging_rules,\n   ...:     )\n   ...: \n\nIn [2]: config = get_configs()\n   ...: env = get_ct_environment()\n   ...: router = env.parse(config)\n   ...: \n\nIn [3]: print(\"\\n--community-list -> prefix-list -> route-map -> bgp -> untagged--\")\n   ...: router.reorder([\"cl\", \"pl\", \"rm\", \"bgp\"])\n   ...: print(router.config)\n\n--community-list -> prefix-list -> route-map -> bgp -> untagged--\nip community-list standard cl_PE1 permit 64512:10001\n!\nip community-list standard cl_PE2 permit 64512:10002\n!\nip community-list expanded cl_VPNv4_1 permit 64512:2[0-9][0-9][0-9]1\n!\nip community-list expanded cl_VPNv4_2 permit 64512:2[0-9][0-9][0-9]2\n!\nip prefix-list pl_CSC seq 5 permit 10.0.0.0/24 ge 32\n!\nroute-map rm_CSC_PE_in deny 10\n match community cl_PE1 cl_PE2\n!\nroute-map rm_CSC_PE_in permit 20\n match ip address prefix-list pl_CSC\n set local-preference 200\n!\nroute-map rm_RR_in permit 10\n match community cl_VPNv4_1\n set local-preference 200\n!\nroute-map rm_RR_in permit 20\n match community cl_VPNv4_2\n set local-preference 190\n!\nrouter bgp 64512\n neighbor CSC peer-group\n neighbor CSC remote-as 12345\n neighbor RR peer-group\n neighbor RR remote-as 64512\n address-family ipv4\n  neighbor CSC send-community both\n  neighbor CSC route-map rm_CSC_PE_in in\n  neighbor CSC send-label\n address-family vpnv4\n  neighbor RR route-map rm_RR_in in\n!\nno platform punt-keepalive disable-kernel-core\n!\nno service dhcp\n!\nip dhcp bootp ignore\n!\nno service pad\n!\n\nIn [4]: print(\"\\n--bgp -> community-list -> prefix-list -> route-map -> untagged -> rm-attach--\")\n   ...: wo_rm_attach = env.search(router, exclude_tags=[\"rm-attach\"])\n   ...: rm_attach = env.search(router, include_tags=[\"rm-attach\"])\n   ...: wo_rm_attach.reorder([\"bgp\", \"cl\", \"pl\", \"rm\"])\n   ...: print(wo_rm_attach.config)\n   ...: print(rm_attach.config)\n\n--bgp -> community-list -> prefix-list -> route-map -> untagged -> rm-attach--\nrouter bgp 64512\n neighbor CSC peer-group\n neighbor CSC remote-as 12345\n neighbor RR peer-group\n neighbor RR remote-as 64512\n address-family ipv4\n  neighbor CSC send-community both\n  neighbor CSC send-label\n address-family vpnv4\n!\nip community-list standard cl_PE1 permit 64512:10001\n!\nip community-list standard cl_PE2 permit 64512:10002\n!\nip community-list expanded cl_VPNv4_1 permit 64512:2[0-9][0-9][0-9]1\n!\nip community-list expanded cl_VPNv4_2 permit 64512:2[0-9][0-9][0-9]2\n!\nip prefix-list pl_CSC seq 5 permit 10.0.0.0/24 ge 32\n!\nroute-map rm_CSC_PE_in deny 10\n match community cl_PE1 cl_PE2\n!\nroute-map rm_CSC_PE_in permit 20\n match ip address prefix-list pl_CSC\n set local-preference 200\n!\nroute-map rm_RR_in permit 10\n match community cl_VPNv4_1\n set local-preference 200\n!\nroute-map rm_RR_in permit 20\n match community cl_VPNv4_2\n set local-preference 190\n!\nno platform punt-keepalive disable-kernel-core\n!\nno service dhcp\n!\nip dhcp bootp ignore\n!\nno service pad\n!\nrouter bgp 64512\n address-family ipv4\n  neighbor CSC route-map rm_CSC_PE_in in\n address-family vpnv4\n  neighbor RR route-map rm_RR_in in\n!\n```\n\n</details>\n<br>\n\n## \u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439\n\n\u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442\u0441\u044f \u043f\u0443\u0442\u0435\u043c \u0441\u0440\u0430\u0432\u043d\u0435\u043d\u0438\u044f \u0434\u0435\u0440\u0435\u0432\u044c\u0435\u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0438 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438. \u041d\u0430\u0438\u0432\u043d\u0430\u044f (\u0441\u044b\u0440\u0430\u044f, raw) \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435\u043c \u043e\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0445 \u0432 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043a\u043e\u043c\u0430\u043d\u0434 \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0442\u0435\u0445, \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043d\u0435\u0442 \u0432 \u0446\u0435\u043b\u0435\u0432\u043e\u0439. \u0423\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0441\u044f \u043f\u0443\u0442\u0435\u043c \u0434\u043e\u043f\u0438\u0441\u044b\u0432\u0430\u043d\u0438\u044f no/undo/... (\u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044f) \u043f\u0435\u0440\u0435\u0434 \u043a\u043e\u043c\u0430\u043d\u0434\u043e\u0439. \u0412\u043e \u043c\u043d\u043e\u0433\u0438\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445 \u0442\u0430\u043a\u043e\u0439 \u043f\u043e\u0434\u0445\u043e\u0434 \u0434\u0430\u0435\u0442 \u0440\u0430\u0431\u043e\u0447\u0438\u0439 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442, \u0430 \u0432 \u0442\u0435\u0445 \u0441\u043b\u0443\u0447\u0430\u044f\u0445, \u043a\u043e\u0433\u0434\u0430 \u0442\u0430\u043a\u043e\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u0441\u044b\u0440\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043c\u043e\u0434\u0438\u0444\u0438\u0446\u0438\u0440\u0443\u0435\u0442\u0441\u044f PostPrecessing \u043f\u0440\u0430\u0432\u0438\u043b\u0430\u043c\u0438. \u041d\u0430\u0431\u043e\u0440 \u043f\u0440\u0430\u0432\u0438\u043b \u043d\u0430\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043f\u043e\u0441\u0442\u0435\u043f\u0435\u043d\u043d\u043e \u043f\u043e \u043c\u0435\u0440\u0435 \u044d\u043a\u0441\u043f\u043b\u0443\u0430\u0442\u0430\u0446\u0438\u0438 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438 \u0438 \u043d\u0430\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u044f \u0441\u043b\u0443\u0447\u0430\u0435\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0442 \u0447\u0435\u0440\u0435\u0437 \u043d\u0430\u0438\u0432\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u0441 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043c no/undo/... \u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u0432\u044b\u0447\u0438\u0441\u043b\u044f\u0435\u0442\u0441\u044f \u0431\u0435\u0437 \u0443\u0447\u0435\u0442\u0430 \u043f\u043e\u0440\u044f\u0434\u043a\u0430 \u043a\u043e\u043c\u0430\u043d\u0434, \u0442.\u0435.\n\n```text\nsome command 1\nsome command 2\n```\n\n\u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0432\u043d\u0430\n\n```text\nsome command 2\nsome command 1\n```\n\n\u0414\u043b\u044f \u0441\u0435\u043a\u0446\u0438\u0439, \u0433\u0434\u0435 \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u0432\u0430\u0436\u0435\u043d (ACL \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440) \u0435\u0441\u0442\u044c \u043f\u0430\u0440\u0430\u0433\u0440\u0430\u0444 \u043d\u0438\u0436\u0435.\n\n### \u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 ([05.naive.diff.py](./examples/05.naive.diff.py))\n\n\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0435\u0442\u0441\u044f \u043f\u0443\u0442\u0435\u043c \u043f\u0440\u043e\u0441\u0442\u043e\u0433\u043e \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f no/undo/... \u043a \u043a\u043e\u043c\u0430\u043d\u0434\u0430\u043c, \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0431\u044b\u0442\u044c \u043d\u0435 \u0434\u043e\u043b\u0436\u043d\u043e, \u0438 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435\u043c \u0442\u0435\u0445 \u043a\u043e\u043c\u0430\u043d\u0434 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u043d\u0435\u0442 \u0432 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438. \u041e\u0441\u043d\u043e\u0432\u043d\u044b\u0435 \u043c\u043e\u043c\u0435\u043d\u0442\u044b:\n\n- \u0432\u0441\u0435 \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \"undo-\u043a\u043e\u043c\u0430\u043d\u0434\u044b\" \u043f\u043e\u043c\u0435\u0449\u0430\u044e\u0442\u0441\u044f \u0432 \u043a\u043e\u043d\u0435\u0446. \u0421\u0434\u0435\u043b\u0430\u043d\u043e \u0441 \u0442\u043e\u0439 \u0446\u0435\u043b\u044c\u044e, \u0447\u0442\u043e \u0431\u044b \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 (\u0430 \u043d\u0435 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435) \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0431\u044b\u043b\u043e \u0432 \u0441\u0430\u043c\u043e\u043c \u043a\u043e\u043d\u0446\u0435, \u0438 \u0447\u0442\u043e \u0431\u044b, \u043f\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438, \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0435\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u043a\u043e\u0433\u0434\u0430 prefix-list \u0443\u0434\u0430\u043b\u044f\u0435\u0442\u0441\u044f \u0440\u0430\u043d\u044c\u0448\u0435, \u0447\u0435\u043c \u043e\u043d \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f). \u0414\u0430\u043d\u043d\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u043c\u043e\u0436\u043d\u043e \u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u043b\u044e\u0447\u043e\u043c `reorder_root` \u043c\u0435\u0442\u043e\u0434\u0430 `diff()`.\n- \u043a\u043e \u0432\u0441\u0435\u043c \u0433\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u043c \"undo-\u043a\u043e\u043c\u0430\u043d\u0434\u0430\u043c\" \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0442\u0435\u0433 `clear`. \u0421\u0434\u0435\u043b\u0430\u043d\u043e \u0434\u043b\u044f \u0442\u043e\u0433\u043e, \u0447\u0442\u043e \u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0435\u0441\u043b\u0438 \u043d\u0435 \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u043e\u0432\u043e\u0434\u0438\u0442\u044c \u043e\u0447\u0438\u0441\u0442\u043a\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 (\u0438\u043b\u0438 \u043d\u0430\u043e\u0431\u043e\u0440\u043e\u0442, \u043f\u0440\u043e\u0432\u0435\u0441\u0442\u0438 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0447\u0438\u0441\u0442\u043a\u0443) \u043e\u0442 \u043d\u0435\u043e\u043f\u0438\u0441\u0430\u043d\u043d\u044b\u0445 \u0432 \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043a.\n\n<details>\n    <summary>\u041b\u0438\u0441\u0442\u0438\u043d\u0433 (click me)</summary>\n\n```python\nIn [1]: from conf_tree import ConfTreeEnv, Vendor\n   ...: \n   ...: \n   ...: def get_configs() -> tuple[str, str]:\n   ...:     with open(file=\"./examples/configs/cisco-naive-diff-target.txt\", mode=\"r\") as f:\n   ...:         target = f.read()\n   ...:     with open(file=\"./examples/configs/cisco-naive-diff-existed.txt\", mode=\"r\") as f:\n   ...:         existed = f.read()\n   ...: \n   ...:     return existed, target\n   ...: \n   ...: \n   ...: def get_ct_environment() -> ConfTreeEnv:\n   ...:     return ConfTreeEnv(vendor=Vendor.CISCO)\n   ...: \n\nIn [2]: existed_config, target_config = get_configs()\n   ...: env = get_ct_environment()\n   ...: existed = env.parse(existed_config)\n   ...: target = env.parse(target_config)\n\nIn [3]: print(\"\\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\")\n   ...: diff = env.diff(a=existed, b=target)\n   ...: print(diff.config)\n\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\ninterface FastEthernet0\n no switchport access vlan 100\n description User\n switchport access vlan 123\n!\nrouter bgp 64512\n address-family ipv4\n  no network 192.168.100.0 mask 255.255.255.0\n  network 192.168.200.1 mask 255.255.255.0\n!\nline vty 0 4\n transport input all\n!\nno router ospf 1\n!\n\nIn [4]: print(\"\\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439: \u0431\u0435\u0437 \u043e\u0447\u0438\u0441\u0442\u043a\u0438---\")\n   ...: diff_without_clear = env.search(diff, exclude_tags=[\"clear\"])\n   ...: print(diff_without_clear.config)\n   ...: \n\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439: \u0431\u0435\u0437 \u043e\u0447\u0438\u0441\u0442\u043a\u0438---\ninterface FastEthernet0\n no switchport access vlan 100\n description User\n switchport access vlan 123\n!\nrouter bgp 64512\n address-family ipv4\n  no network 192.168.100.0 mask 255.255.255.0\n  network 192.168.200.1 mask 255.255.255.0\n!\nline vty 0 4\n transport input all\n!\n\nIn [5]: print(\"\\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439: \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0447\u0438\u0441\u0442\u043a\u0430---\")\n   ...: diff_clear = env.search(diff, include_tags=[\"clear\"])\n   ...: print(diff_clear.config)\n   ...: \n\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439: \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0447\u0438\u0441\u0442\u043a\u0430---\nno router ospf 1\n!\n```\n\n</details>\n<br>\n\n### \u041f\u043e\u0441\u0442-\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u0440\u0430\u0437\u043d\u0438\u0446\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 ([06.postproc.diff.py](./examples/06.postproc.diff.py))\n\n\u041a\u043e\u0433\u0434\u0430 \u043d\u0430\u0438\u0432\u043d\u044b\u0439 \u0432\u0430\u0440\u0438\u0430\u043d\u0442 \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442, \u0440\u0430\u0437\u043d\u0438\u0446\u0443 \u043c\u043e\u0436\u043d\u043e \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043f\u0440\u0430\u0432\u0438\u043b \u0438 \u043f\u0440\u0438\u0434\u0430\u0442\u044c \u0435\u0439 \u043d\u0443\u0436\u043d\u044b\u0439 \u0432\u0438\u0434. \u041f\u0440\u0430\u0432\u0438\u043b \u043f\u043e\u0441\u0442-\u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e, \u043e\u043d\u0438 \u043d\u0435\u0437\u0430\u0432\u0438\u0441\u0438\u043c\u044b \u0434\u0440\u0443\u0433 \u043e\u0442 \u0434\u0440\u0443\u0433\u0430 \u0438 \u0447\u0435\u0440\u0435\u0437 \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 `register_rule` \u043f\u043e\u043c\u0435\u0449\u0430\u044e\u0442\u0441\u044f \u0432 \u043e\u0431\u0449\u0438\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0430\u0432\u0438\u043b.\n\n<details>\n    <summary>\u041b\u0438\u0441\u0442\u0438\u043d\u0433 (click me)</summary>\n\n```python\nIn [1]: import re\n   ...: \n   ...: from conf_tree import ConfTree, ConfTreeEnv, ConfTreePostProc, Vendor, register_rule\n\nIn [2]: @register_rule\n   ...: class CiscoPostProcBGP(ConfTreePostProc):\n   ...:     @classmethod\n   ...:     def _delete_nodes(cls, ct: ConfTree, regex: str) -> None:\n   ...:         nodes_to_delete: list[ConfTree] = []\n   ...:         for node in ct.children.values():\n   ...:             if len(node.children) != 0:\n   ...:                 cls._delete_nodes(node, regex)\n   ...:                 if len(node.children) == 0:\n   ...:                     nodes_to_delete.append(node)\n   ...:             else:\n   ...:                 if re.match(regex, node.line):\n   ...:                     nodes_to_delete.append(node)\n   ...:         for node in nodes_to_delete:\n   ...:             node.delete()\n   ...:\n   ...:     @classmethod\n   ...:     def process(cls, ct: ConfTree) -> None:\n   ...:         bgp_nodes = [node for node in ct.children.values() if node.line.startswith(\"router bgp \")]\n   ...:         if len(bgp_nodes) != 1:\n   ...:             return\n   ...:         bgp = bgp_nodes[0]\n   ...: \n   ...:         bgp_global = {node.line: node for node in bgp.children.values() if len(node.children) == 0}\n   ...:         bgp_af = {node.line: node for node in bgp.children.values() if len(node.children) != 0}\n   ...:         bgp.children = bgp_global | bgp_af\n   ...: \n   ...:         regexes_to_delete = set()\n   ...:         groups_to_delete = set()\n   ...:         peers_to_delete = set()\n   ...:         for node in bgp.children.values():\n   ...:             if node.line.startswith(\"no neighbor \") and node.line.endswith(\" peer-group\"):\n   ...:                 _, _, group, _ = node.line.split()\n   ...:                 groups_to_delete.add(group)\n   ...:         for node in bgp.children.values():\n   ...:             if (\n   ...:                 m := re.fullmatch(\n   ...:                     pattern=rf\"no neighbor (?P<peer>\\S+) peer-group (?:{'|'.join(groups_to_delete)})\",\n   ...:                     string=node.line,\n   ...:                 )\n   ...:             ) is not None:\n   ...:                 peers_to_delete.add(m.group(\"peer\"))\n   ...: \n   ...:         if len(groups_to_delete) != 0:\n   ...:             regexes_to_delete.add(rf\"no neighbor (?:{'|'.join(groups_to_delete)}) (?!peer-group)\")\n   ...:         if len(peers_to_delete) != 0:\n   ...:             regexes_to_delete.add(rf\"no neighbor (?:{'|'.join(peers_to_delete)})\")\n   ...: \n   ...:         if len(regexes_to_delete) != 0:\n   ...:             cls._delete_nodes(bgp, \"|\".join(regexes_to_delete))\n\nIn [3]: def get_configs() -> tuple[str, str]:\n   ...:     with open(file=\"./examples/configs/cisco-postproc-diff-target.txt\", mode=\"r\") as f:\n   ...:         target = f.read()\n   ...:     with open(file=\"./examples/configs/cisco-postproc-diff-existed.txt\", mode=\"r\") as f:\n   ...:         existed = f.read()\n   ...: \n   ...:     return existed, target\n   ...: \n\nIn [4]: def get_ct_environment_naive() -> ConfTreeEnv:\n   ...:     return ConfTreeEnv(vendor=Vendor.CISCO, post_proc_rules=[])\n   ...: \n   ...: \n   ...: def get_ct_environment_postproc() -> ConfTreeEnv:\n   ...:     # \u0434\u0435\u043a\u043e\u0440\u0430\u0442\u043e\u0440 register_rule \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u0432 \u043e\u0431\u0449\u0438\u0439 \u0441\u043f\u0438\u0441\u043e\u043a \u0438 \u043c\u043e\u0436\u043d\u043e \u0442\u0443\u0442 \u043d\u0435\n   ...:     # \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u044f\u0442\u044c \u0435\u0433\u043e \u0447\u0435\u0440\u0435\u0437 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442 post_proc_rules, \u043d\u043e \u0435\u0441\u043b\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \n   ...:     # \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u0430\u043a\u0438\u0435-\u0442\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0430, \u0442\u043e\u0433\u0434\u0430 \u044f\u0432\u043d\u043e \u0437\u0430\u0434\u0430\u0435\u043c \u0438\u0445 \n   ...:     # \u0438\u043b\u0438 \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u043c \u043f\u0443\u0441\u0442\u043e\u0439 \u0441\u043f\u0438\u0441\u043e\u043a, \u0447\u0442\u043e \u0431\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043d\u0430\u0438\u0432\u043d\u0443\u044e \u0440\u0430\u0437\u043d\u0438\u0446\u0443 \u0431\u0435\u0437 \u043e\u0431\u0440\u0430\u0431\u043e\u0442\u043a\u0438\n   ...:     return ConfTreeEnv(vendor=Vendor.CISCO, post_proc_rules=[CiscoPostProcBGP])\n   ...: \n\nIn [5]: existed_config, target_config = get_configs()\n\nIn [6]: print(\"\\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\")\n   ...: env_naive = get_ct_environment_naive()\n   ...: existed = env_naive.parse(existed_config)\n   ...: target = env_naive.parse(target_config)\n   ...: diff = env_naive.diff(a=existed, b=target)\n   ...: print(diff.config)\n   ...: \n\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\nrouter bgp 64512\n no neighbor RR peer-group\n no neighbor RR remote-as 64512\n no neighbor RR ebgp-multihop 255\n no neighbor RR update-source Loopback0\n no neighbor 192.168.255.2 peer-group RR\n no neighbor 192.168.255.3 peer-group RR\n address-family ipv4\n  no neighbor RR send-community both\n  no neighbor RR advertisement-interval 0\n  no neighbor 192.168.255.2 activate\n  no neighbor 192.168.255.3 activate\n  neighbor 192.168.255.1 send-community both\n!\n\nIn [7]: print(\"\\n---\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\")\n   ...: env_postproc = get_ct_environment_postproc()\n   ...: existed = env_postproc.parse(existed_config)\n   ...: target = env_postproc.parse(target_config)\n   ...: diff = env_postproc.diff(a=existed, b=target)\n   ...: print(diff.config)\n   ...: \n\n---\u041e\u0431\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\nrouter bgp 64512\n no neighbor RR peer-group\n address-family ipv4\n  neighbor 192.168.255.1 send-community both\n!\n```\n\n</details>\n<br>\n\n### \u0421\u0435\u043a\u0446\u0438\u0438 \u0431\u0435\u0437 \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u044b ([07.no.diff.section.py](./examples/07.no.diff.section.py))\n\n\u041d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0441\u0435\u043a\u0446\u0438\u0438 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 \u043d\u0443\u0436\u043d\u043e \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044c \u043f\u043e\u043b\u043d\u043e\u0441\u0442\u044c\u044e, \u0434\u0430\u0436\u0435 \u0435\u0441\u043b\u0438 \u043d\u0443\u0436\u043d\u043e \u043f\u043e\u043c\u0435\u043d\u044f\u0442\u044c \u043a\u0430\u043a\u043e\u0439-\u0442\u043e \u043e\u0434\u0438\u043d \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440. \u042d\u0442\u043e \u043c\u043e\u0433\u0443\u0442 \u0431\u044b\u0442\u044c RPL \u0441\u0435\u043a\u0446\u0438\u0438 \u0432 IOS-XR \u0438\u043b\u0438 XPL \u0432 Huawei (\u0441\u043c\u044b\u0441\u043b \u043e\u0434\u0438\u043d \u0438 \u0442\u043e\u0442 \u0436\u0435: \u043f\u0440\u0438 \u0432\u0445\u043e\u0434\u0435 \u0432 \u0441\u0435\u043a\u0446\u0438\u044e, \u043d\u0443\u0436\u043d\u043e \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c \u043f\u043e\u043b\u043d\u044b\u0435 \u0435\u0451 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438). \u0420\u0435\u0433\u0443\u043b\u044f\u0440\u043d\u043e\u0435 \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u0435, \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u0442\u0430\u043a\u0438\u0435 \u0441\u0435\u043a\u0446\u0438\u0438 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043e \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0430 \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f, \u0432 \u044d\u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0434\u043b\u044f \u044d\u0442\u0438\u0445 \u0441\u0435\u043a\u0446\u0438\u0439 \u0432 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0435 \u0440\u0430\u0437\u043d\u0438\u0446\u044b \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u0438\u043c\u0435\u043d\u044f\u0442\u044c\u0441\u044f \u0446\u0435\u043b\u0435\u0432\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0446\u0435\u043b\u0438\u043a\u043e\u043c, \u0430 \u043d\u0435 \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043c\u0435\u0436\u0434\u0443 \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u0438 \u0446\u0435\u043b\u0435\u0432\u043e\u0439.\n\n<details>\n    <summary>\u041b\u0438\u0441\u0442\u0438\u043d\u0433 (click me)</summary>\n\n```python\nIn [1]: from conf_tree import ConfTreeEnv, Vendor\n\nIn [2]: def get_configs() -> tuple[str, str]:\n   ...:     with open(file=\"./examples/configs/cisco-no-diff-section-target.txt\", mode=\"r\") as f:\n   ...:         target = f.read()\n   ...:     with open(file=\"./examples/configs/cisco-no-diff-section-existed.txt\", mode=\"r\") as f:\n   ...:         existed = f.read()\n   ...: \n   ...:     return existed, target\n   ...: \n\nIn [3]: def get_ct_environment_naive() -> ConfTreeEnv:\n   ...:     return ConfTreeEnv(vendor=Vendor.CISCO)\n   ...: \n\nIn [4]: def get_ct_environment_no_diff() -> ConfTreeEnv:\n   ...:     return ConfTreeEnv(\n   ...:         vendor=Vendor.CISCO,\n   ...:         no_diff_sections=[\n   ...:             r\"prefix-set \\S+\",\n   ...:             r\"route-policy \\S+\",\n   ...:         ],\n   ...:     )\n   ...: \n\nIn [5]: existed_config, target_config = get_configs()\n\nIn [6]: print(\"\\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\")\n   ...: env_naive = get_ct_environment_naive()\n   ...: existed = env_naive.parse(existed_config)\n   ...: target = env_naive.parse(target_config)\n   ...: diff = env_naive.diff(a=existed, b=target)\n   ...: print(diff.config)\n\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\ninterface BVI123\n no description User-OLD\n description User-NEW\n!\nprefix-set ps-google\n no 8.8.8.8/32\n 8.8.8.8/32,\n 8.8.4.4/32\n!\nroute-policy rp-google\n elseif destination in ps-some-networks then\n  drop\n!\n\nIn [7]: print(\"\\n---\u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c no-diff \u0441\u0435\u043a\u0446\u0438\u0439---\")\n   ...: env_no_diff = get_ct_environment_no_diff()\n   ...: existed = env_no_diff.parse(existed_config)\n   ...: target = env_no_diff.parse(target_config)\n   ...: diff = env_no_diff.diff(a=existed, b=target)\n   ...: print(diff.config)\n\n---\u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c no-diff \u0441\u0435\u043a\u0446\u0438\u0439---\ninterface BVI123\n no description User-OLD\n description User-NEW\n!\nprefix-set ps-google\n 8.8.8.8/32,\n 8.8.4.4/32\n!\nroute-policy rp-google\n if destination in ps-google then\n  drop\n elseif destination in ps-some-networks then\n  drop\n else\n  pass\n endif\n!\n```\n\n</details>\n<br>\n\n### \u0421\u0435\u043a\u0446\u0438\u0438, \u0433\u0434\u0435 \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u0438\u043c\u0435\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 ([08.ordered.diff.py](./examples/08.ordered.diff.py))\n\n\u0412 \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0441\u0435\u043a\u0446\u0438\u044f\u0445 (\u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 acl) \u043f\u043e\u0440\u044f\u0434\u043e\u043a \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u0438\u043c\u0435\u0435\u0442 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435. \u0420\u0435\u0433\u0443\u043b\u044f\u0440\u043d\u043e\u0435 \u0432\u044b\u0440\u0430\u0436\u0435\u043d\u0438\u0435, \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0449\u0435\u0435 \u0442\u0430\u043a\u0438\u0435 \u0441\u0435\u043a\u0446\u0438\u0438 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043e \u043f\u0440\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0438 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f. \u0412 \u044d\u0442\u043e\u043c \u0441\u043b\u0443\u0447\u0430\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0431\u0443\u0434\u0435\u0442 \u0441\u0442\u0430\u0440\u0430\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u0442\u0435\u043a\u0443\u0449\u0443\u044e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u043a \u0446\u0435\u043b\u0435\u0432\u043e\u0439 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c \u043f\u043e\u0440\u044f\u0434\u043a\u0430 \u043a\u043e\u043c\u0430\u043d\u0434. \u041d\u043e \u043d\u0435\u0441\u043c\u043e\u0442\u0440\u044f \u043d\u0430 \u044d\u0442\u043e, \u043a \u0442\u0430\u043a\u0438\u043c \u0441\u0435\u043a\u0446\u0438\u044f\u043c \u043d\u0443\u0436\u043d\u043e \u043e\u0442\u043d\u043e\u0441\u0438\u0442\u044c\u0441\u044f \u0441 \u043e\u0441\u043e\u0431\u043e\u0439 \u0432\u043d\u0438\u043c\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c\u044e \u0438, \u043f\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438, \u0438\u0437\u0431\u0435\u0433\u0430\u0442\u044c \u0438\u0445. \u0423\u043f\u043e\u043c\u044f\u043d\u0443\u0442\u044b\u0435 \u0432\u044b\u0448\u0435 ACL \u043c\u043e\u0433\u0443\u0442 \u0438\u043c\u0435\u0442\u044c entry-number \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u043e\u043c, \u0447\u0442\u043e \u0438\u0437\u0431\u0430\u0432\u043b\u044f\u0435\u0442 \u043e\u0442 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u043f\u043e\u0440\u044f\u0434\u043a\u0430, \u0442\u0430\u043a \u043a\u0430\u043a \u043d\u0435 \u0432\u0430\u0436\u043d\u043e \u0432 \u043a\u0430\u043a\u043e\u043c \u043f\u043e\u0440\u044f\u0434\u043a\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u0440\u0438\u043c\u0435\u043d\u0435\u043d\u044b \u043a\u043e\u043c\u0430\u043d\u0434\u044b, \u0437\u0430 \u0441\u0447\u0435\u0442 \u043d\u0430\u043b\u0438\u0447\u0438\u044f entry-number \u0432 ACE, \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0431\u0443\u0434\u0443\u0442 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u044b \u0432 \u043d\u0443\u0436\u043d\u043e\u0435 \u043c\u0435\u0441\u0442\u043e (\u043e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043d\u0435 \u0437\u0430\u0431\u044b\u0432\u0430\u0435\u043c \u043f\u0440\u043e \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u0438 \u0434\u0435\u043b\u0430\u0442\u044c re-sequence, \u044d\u0442\u0443 \u043a\u043e\u043c\u0430\u043d\u0434\u0443 \u043c\u043e\u0436\u043d\u043e, \u043d\u0430\u043f\u0440\u0438\u043c\u0435\u0440 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0447\u0435\u0440\u0435\u0437 post-processing \u043f\u0440\u0438 \u043c\u043e\u0434\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 acl, \u0442\u043e\u0433\u0434\u0430 \u0431\u0443\u0434\u0435\u0442 \u0432\u0441\u0435\u0433\u0434\u0430 \u043a\u043e\u043d\u0441\u0438\u0441\u0442\u0435\u043d\u0442\u043d\u044b\u0439 \u0448\u0430\u0433 \u043c\u0435\u0436\u0434\u0443 ACE).\n\n<details>\n    <summary>\u041b\u0438\u0441\u0442\u0438\u043d\u0433 (click me)</summary>\n\n```python\nIn [1]: from conf_tree import ConfTreeEnv, Vendor\n\nIn [2]: def get_configs() -> tuple[str, str]:\n   ...:     with open(file=\"./examples/configs/cisco-ordered-diff-target.txt\", mode=\"r\") as f:\n   ...:         target = f.read()\n   ...:     with open(file=\"./examples/configs/cisco-ordered-diff-existed.txt\", mode=\"r\") as f:\n   ...:         existed = f.read()\n   ...: \n   ...:     return existed, target\n   ...: \n\nIn [3]: def get_ct_environment_naive() -> ConfTreeEnv:\n   ...:     return ConfTreeEnv(vendor=Vendor.CISCO)\n   ...: \n   ...: \n   ...: def get_ct_environment_ordered() -> ConfTreeEnv:\n   ...:     return ConfTreeEnv(\n   ...:         vendor=Vendor.CISCO,\n   ...:         ordered_sections=[\n   ...:             r\"ip access-list standard \\S+$\",\n   ...:         ],\n   ...:     )\n   ...: \n\nIn [4]: existed_config, target_config = get_configs()\n\nIn [5]: print(\"\\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\")\n   ...: env_naive = get_ct_environment_naive()\n   ...: existed = env_naive.parse(existed_config)\n   ...: target = env_naive.parse(target_config)\n   ...: diff = env_naive.diff(a=existed, b=target)\n   ...: print(diff.config)\n\n---\u041d\u0430\u0438\u0432\u043d\u0430\u044f \u0440\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439---\nip access-list standard acl_TEST_STD\n permit 8.8.4.4\n!\nip access-list extended act_TEST_EXT\n 15 permit ip host 8.8.4.4 any\n!\n\nIn [6]: print(\"\\n---\u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c \u0441\u0435\u043a\u0446\u0438\u0439 \u0441\u043e \u0437\u043d\u0430\u0447\u0438\u043c\u044b\u043c \u043f\u043e\u0440\u044f\u0434\u043a\u043e\u043c---\")\n   ...: env_ordered = get_ct_environment_ordered()\n   ...: existed = env_ordered.parse(existed_config)\n   ...: target = env_ordered.parse(target_config)\n   ...: diff = env_ordered.diff(a=existed, b=target)\n   ...: print(diff.config)\n\n---\u0420\u0430\u0437\u043d\u0438\u0446\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0439 \u0441 \u0443\u0447\u0435\u0442\u043e\u043c \u0441\u0435\u043a\u0446\u0438\u0439 \u0441\u043e \u0437\u043d\u0430\u0447\u0438\u043c\u044b\u043c \u043f\u043e\u0440\u044f\u0434\u043a\u043e\u043c---\nip access-list standard acl_TEST_STD\n no deny   any\n permit 8.8.4.4\n deny   any\n!\nip access-list extended act_TEST_EXT\n 15 permit ip host 8.8.4.4 any\n!\n```\n\n</details>\n<br>\n\n## \u0418\u0441\u0442\u043e\u0440\u0438\u044f \u0432\u0435\u0440\u0441\u0438\u0439\n\n- 0.1.0 - \u043a\u043e\u0434 \u0437\u0430\u043b\u0438\u0442 \u043d\u0430 github, \u043f\u0440\u043e\u0442\u0435\u0441\u0442\u0438\u0440\u043e\u0432\u0430\u043d cicd\n- 0.1.1 - \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d readme, \u043d\u0435\u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0442\u0435\u0441\u0442\u044b\n\n## TODO\n\n- \u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0448\u0430\u0431\u043b\u043e\u043d \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434, \u0447\u0442\u043e \u0431\u044b \u043f\u0440\u0438 \u0432\u044b\u0447\u0438\u0441\u043b\u0435\u043d\u0438\u0438 \u0440\u0430\u0437\u043d\u0438\u0446\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043f\u043e\u043d\u044f\u0442\u044c \u0433\u0434\u0435 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u044b, \u0438 \u0435\u0441\u043b\u0438 \u0435\u0441\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c, \u043e\u0431\u043e\u0439\u0442\u0438\u0441\u044c \u0431\u0435\u0437 undo-\u043a\u043e\u043c\u0430\u043d\u0434\u044b, \u0442\u0430\u043a \u043a\u0430\u043a \u043a\u043e\u043c\u0430\u043d\u0434\u0430 \u0441 \u043d\u043e\u0432\u044b\u043c\u0438 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0430\u043c\u0438 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0448\u0435\u0442 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0435 \u043e\u043f\u0446\u0438\u0438.\n- \u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u0443\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043a\u0430\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0443\u0434\u0430\u043b\u044f\u0442\u044c \u043a\u043e\u043c\u0430\u043d\u0434\u044b (\u0447\u0430\u0441\u0442\u043e \u044d\u0442\u043e \u0434\u0435\u043b\u0430\u0435\u0442\u0441\u044f \u0431\u0435\u0437 \u0443\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u043e\u043f\u0446\u0438\u0439 \u0438 \u043f\u0440\u0438\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u043f\u0440\u0430\u0432\u0438\u0442\u044c \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0435 \u043a\u043e\u043c\u0430\u043d\u0434 \u0447\u0435\u0440\u0435\u0437 post-processing)\n\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Parsing, filtering and diff calculation for network device configurations",
    "version": "0.1.1",
    "project_urls": null,
    "split_keywords": [
        "cisco",
        " arista",
        " huawei",
        " network",
        " automation",
        " configuration"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "3c2fb341cd4ddfa77988f419642ad1670a2a4648ac30a87f4e329c967ecd8b14",
                "md5": "729cbb1e93c5f5bc6a8378f046dd98bc",
                "sha256": "d036b907d3a5c3e6ac7646af1711be79747f66cfa9bf2c9cf8fba573dcd7f1f0"
            },
            "downloads": -1,
            "filename": "conf_tree-0.1.1-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "729cbb1e93c5f5bc6a8378f046dd98bc",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.10",
            "size": 29923,
            "upload_time": "2024-12-02T10:24:56",
            "upload_time_iso_8601": "2024-12-02T10:24:56.482065Z",
            "url": "https://files.pythonhosted.org/packages/3c/2f/b341cd4ddfa77988f419642ad1670a2a4648ac30a87f4e329c967ecd8b14/conf_tree-0.1.1-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "a3fdfaf52839d1770822bddbf2c023973b17b7e0c043528ae79e9388a8c1ad9c",
                "md5": "1ab71e91b1818c451cd65609398e80b9",
                "sha256": "01a722d0f5866b98823c796a8b873ef19ea3061ef944dd79382a896f2f098c25"
            },
            "downloads": -1,
            "filename": "conf_tree-0.1.1.tar.gz",
            "has_sig": false,
            "md5_digest": "1ab71e91b1818c451cd65609398e80b9",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.10",
            "size": 31772,
            "upload_time": "2024-12-02T10:24:57",
            "upload_time_iso_8601": "2024-12-02T10:24:57.929016Z",
            "url": "https://files.pythonhosted.org/packages/a3/fd/faf52839d1770822bddbf2c023973b17b7e0c043528ae79e9388a8c1ad9c/conf_tree-0.1.1.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2024-12-02 10:24:57",
    "github": false,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "lcname": "conf-tree"
}
        
Elapsed time: 0.53467s