annet 0.16.25__tar.gz → 0.16.27__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of annet might be problematic. Click here for more details.

Files changed (195) hide show
  1. {annet-0.16.25/annet.egg-info → annet-0.16.27}/PKG-INFO +2 -2
  2. {annet-0.16.25 → annet-0.16.27}/README.md +8 -0
  3. {annet-0.16.25 → annet-0.16.27}/annet/adapters/file/provider.py +28 -10
  4. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/v37/storage.py +1 -1
  5. {annet-0.16.25 → annet-0.16.27}/annet/annlib/netdev/devdb/data/devdb.json +3 -2
  6. {annet-0.16.25 → annet-0.16.27}/annet/annlib/patching.py +50 -14
  7. {annet-0.16.25 → annet-0.16.27}/annet/bgp_models.py +28 -0
  8. {annet-0.16.25 → annet-0.16.27}/annet/mesh/__init__.py +4 -0
  9. {annet-0.16.25 → annet-0.16.27}/annet/mesh/basemodel.py +5 -0
  10. {annet-0.16.25 → annet-0.16.27}/annet/mesh/device_models.py +2 -0
  11. {annet-0.16.25 → annet-0.16.27}/annet/mesh/executor.py +90 -66
  12. {annet-0.16.25 → annet-0.16.27}/annet/mesh/peer_models.py +3 -3
  13. annet-0.16.27/annet/mesh/port_processor.py +18 -0
  14. {annet-0.16.25 → annet-0.16.27}/annet/mesh/registry.py +12 -4
  15. {annet-0.16.25 → annet-0.16.27}/annet/rpl/match_builder.py +30 -9
  16. {annet-0.16.25 → annet-0.16.27}/annet/rpl/routemap.py +5 -3
  17. {annet-0.16.25 → annet-0.16.27}/annet/rpl/statement_builder.py +31 -7
  18. annet-0.16.27/annet/rpl_generators/__init__.py +24 -0
  19. annet-0.16.27/annet/rpl_generators/aspath.py +57 -0
  20. annet-0.16.27/annet/rpl_generators/community.py +242 -0
  21. annet-0.16.27/annet/rpl_generators/cumulus_frr.py +458 -0
  22. annet-0.16.27/annet/rpl_generators/entities.py +70 -0
  23. annet-0.16.27/annet/rpl_generators/execute.py +12 -0
  24. annet-0.16.27/annet/rpl_generators/policy.py +676 -0
  25. annet-0.16.27/annet/rpl_generators/prefix_lists.py +158 -0
  26. annet-0.16.27/annet/rpl_generators/rd.py +40 -0
  27. {annet-0.16.25 → annet-0.16.27/annet.egg-info}/PKG-INFO +2 -2
  28. {annet-0.16.25 → annet-0.16.27}/annet.egg-info/SOURCES.txt +12 -1
  29. {annet-0.16.25 → annet-0.16.27}/annet.egg-info/requires.txt +1 -1
  30. annet-0.16.27/annet_generators/rpl_example/__init__.py +7 -0
  31. annet-0.16.27/annet_generators/rpl_example/generator.py +127 -0
  32. annet-0.16.27/annet_generators/rpl_example/items.py +21 -0
  33. annet-0.16.27/annet_generators/rpl_example/mesh.py +9 -0
  34. annet-0.16.27/annet_generators/rpl_example/route_policy.py +67 -0
  35. {annet-0.16.25 → annet-0.16.27}/setup.py +1 -1
  36. annet-0.16.25/annet_generators/rpl_example/__init__.py +0 -9
  37. annet-0.16.25/annet_generators/rpl_example/items.py +0 -31
  38. annet-0.16.25/annet_generators/rpl_example/policy_generator.py +0 -233
  39. annet-0.16.25/annet_generators/rpl_example/route_policy.py +0 -33
  40. {annet-0.16.25 → annet-0.16.27}/AUTHORS +0 -0
  41. {annet-0.16.25 → annet-0.16.27}/LICENSE +0 -0
  42. {annet-0.16.25 → annet-0.16.27}/MANIFEST.in +0 -0
  43. {annet-0.16.25 → annet-0.16.27}/annet/__init__.py +0 -0
  44. {annet-0.16.25 → annet-0.16.27}/annet/adapters/__init__.py +0 -0
  45. {annet-0.16.25 → annet-0.16.27}/annet/adapters/fetchers/__init__.py +0 -0
  46. {annet-0.16.25 → annet-0.16.27}/annet/adapters/fetchers/stub/__init__.py +0 -0
  47. {annet-0.16.25 → annet-0.16.27}/annet/adapters/fetchers/stub/fetcher.py +0 -0
  48. {annet-0.16.25 → annet-0.16.27}/annet/adapters/file/__init__.py +0 -0
  49. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/__init__.py +0 -0
  50. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/common/__init__.py +0 -0
  51. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/common/client.py +0 -0
  52. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/common/manufacturer.py +0 -0
  53. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/common/models.py +0 -0
  54. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/common/query.py +0 -0
  55. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/common/status_client.py +0 -0
  56. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/common/storage_opts.py +0 -0
  57. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/provider.py +0 -0
  58. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/v24/__init__.py +0 -0
  59. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/v24/storage.py +0 -0
  60. {annet-0.16.25 → annet-0.16.27}/annet/adapters/netbox/v37/__init__.py +0 -0
  61. {annet-0.16.25 → annet-0.16.27}/annet/annet.py +0 -0
  62. {annet-0.16.25 → annet-0.16.27}/annet/annlib/__init__.py +0 -0
  63. {annet-0.16.25 → annet-0.16.27}/annet/annlib/command.py +0 -0
  64. {annet-0.16.25 → annet-0.16.27}/annet/annlib/diff.py +0 -0
  65. {annet-0.16.25 → annet-0.16.27}/annet/annlib/errors.py +0 -0
  66. {annet-0.16.25 → annet-0.16.27}/annet/annlib/filter_acl.py +0 -0
  67. {annet-0.16.25 → annet-0.16.27}/annet/annlib/jsontools.py +0 -0
  68. {annet-0.16.25 → annet-0.16.27}/annet/annlib/lib.py +0 -0
  69. {annet-0.16.25 → annet-0.16.27}/annet/annlib/netdev/__init__.py +0 -0
  70. {annet-0.16.25 → annet-0.16.27}/annet/annlib/netdev/db.py +0 -0
  71. {annet-0.16.25 → annet-0.16.27}/annet/annlib/netdev/devdb/__init__.py +0 -0
  72. {annet-0.16.25 → annet-0.16.27}/annet/annlib/netdev/views/__init__.py +0 -0
  73. {annet-0.16.25 → annet-0.16.27}/annet/annlib/netdev/views/dump.py +0 -0
  74. {annet-0.16.25 → annet-0.16.27}/annet/annlib/netdev/views/hardware.py +0 -0
  75. {annet-0.16.25 → annet-0.16.27}/annet/annlib/output.py +0 -0
  76. {annet-0.16.25 → annet-0.16.27}/annet/annlib/rbparser/__init__.py +0 -0
  77. {annet-0.16.25 → annet-0.16.27}/annet/annlib/rbparser/acl.py +0 -0
  78. {annet-0.16.25 → annet-0.16.27}/annet/annlib/rbparser/deploying.py +0 -0
  79. {annet-0.16.25 → annet-0.16.27}/annet/annlib/rbparser/ordering.py +0 -0
  80. {annet-0.16.25 → annet-0.16.27}/annet/annlib/rbparser/platform.py +0 -0
  81. {annet-0.16.25 → annet-0.16.27}/annet/annlib/rbparser/syntax.py +0 -0
  82. {annet-0.16.25 → annet-0.16.27}/annet/annlib/rulebook/__init__.py +0 -0
  83. {annet-0.16.25 → annet-0.16.27}/annet/annlib/rulebook/common.py +0 -0
  84. {annet-0.16.25 → annet-0.16.27}/annet/annlib/tabparser.py +0 -0
  85. {annet-0.16.25 → annet-0.16.27}/annet/annlib/types.py +0 -0
  86. {annet-0.16.25 → annet-0.16.27}/annet/api/__init__.py +0 -0
  87. {annet-0.16.25 → annet-0.16.27}/annet/argparse.py +0 -0
  88. {annet-0.16.25 → annet-0.16.27}/annet/cli.py +0 -0
  89. {annet-0.16.25 → annet-0.16.27}/annet/cli_args.py +0 -0
  90. {annet-0.16.25 → annet-0.16.27}/annet/configs/context.yml +0 -0
  91. {annet-0.16.25 → annet-0.16.27}/annet/configs/logging.yaml +0 -0
  92. {annet-0.16.25 → annet-0.16.27}/annet/connectors.py +0 -0
  93. {annet-0.16.25 → annet-0.16.27}/annet/deploy.py +0 -0
  94. {annet-0.16.25 → annet-0.16.27}/annet/diff.py +0 -0
  95. {annet-0.16.25 → annet-0.16.27}/annet/executor.py +0 -0
  96. {annet-0.16.25 → annet-0.16.27}/annet/filtering.py +0 -0
  97. {annet-0.16.25 → annet-0.16.27}/annet/gen.py +0 -0
  98. {annet-0.16.25 → annet-0.16.27}/annet/generators/__init__.py +0 -0
  99. {annet-0.16.25 → annet-0.16.27}/annet/generators/base.py +0 -0
  100. {annet-0.16.25 → annet-0.16.27}/annet/generators/common/__init__.py +0 -0
  101. {annet-0.16.25 → annet-0.16.27}/annet/generators/common/initial.py +0 -0
  102. {annet-0.16.25 → annet-0.16.27}/annet/generators/entire.py +0 -0
  103. {annet-0.16.25 → annet-0.16.27}/annet/generators/exceptions.py +0 -0
  104. {annet-0.16.25 → annet-0.16.27}/annet/generators/jsonfragment.py +0 -0
  105. {annet-0.16.25 → annet-0.16.27}/annet/generators/partial.py +0 -0
  106. {annet-0.16.25 → annet-0.16.27}/annet/generators/perf.py +0 -0
  107. {annet-0.16.25 → annet-0.16.27}/annet/generators/ref.py +0 -0
  108. {annet-0.16.25 → annet-0.16.27}/annet/generators/result.py +0 -0
  109. {annet-0.16.25 → annet-0.16.27}/annet/hardware.py +0 -0
  110. {annet-0.16.25 → annet-0.16.27}/annet/implicit.py +0 -0
  111. {annet-0.16.25 → annet-0.16.27}/annet/lib.py +0 -0
  112. {annet-0.16.25 → annet-0.16.27}/annet/mesh/match_args.py +0 -0
  113. {annet-0.16.25 → annet-0.16.27}/annet/mesh/models_converter.py +0 -0
  114. {annet-0.16.25 → annet-0.16.27}/annet/output.py +0 -0
  115. {annet-0.16.25 → annet-0.16.27}/annet/parallel.py +0 -0
  116. {annet-0.16.25 → annet-0.16.27}/annet/patching.py +0 -0
  117. {annet-0.16.25 → annet-0.16.27}/annet/reference.py +0 -0
  118. {annet-0.16.25 → annet-0.16.27}/annet/rpl/__init__.py +0 -0
  119. {annet-0.16.25 → annet-0.16.27}/annet/rpl/action.py +0 -0
  120. {annet-0.16.25 → annet-0.16.27}/annet/rpl/condition.py +0 -0
  121. {annet-0.16.25 → annet-0.16.27}/annet/rpl/policy.py +0 -0
  122. {annet-0.16.25 → annet-0.16.27}/annet/rpl/result.py +0 -0
  123. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/__init__.py +0 -0
  124. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/arista/__init__.py +0 -0
  125. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/arista/iface.py +0 -0
  126. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/aruba/__init__.py +0 -0
  127. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/aruba/ap_env.py +0 -0
  128. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/aruba/misc.py +0 -0
  129. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/b4com/__init__.py +0 -0
  130. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/b4com/file.py +0 -0
  131. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/cisco/__init__.py +0 -0
  132. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/cisco/iface.py +0 -0
  133. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/cisco/misc.py +0 -0
  134. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/cisco/vlandb.py +0 -0
  135. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/common.py +0 -0
  136. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/deploying.py +0 -0
  137. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/huawei/__init__.py +0 -0
  138. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/huawei/aaa.py +0 -0
  139. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/huawei/bgp.py +0 -0
  140. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/huawei/iface.py +0 -0
  141. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/huawei/misc.py +0 -0
  142. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/huawei/vlandb.py +0 -0
  143. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/juniper/__init__.py +0 -0
  144. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/nexus/__init__.py +0 -0
  145. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/nexus/iface.py +0 -0
  146. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/patching.py +0 -0
  147. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/ribbon/__init__.py +0 -0
  148. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/routeros/__init__.py +0 -0
  149. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/routeros/file.py +0 -0
  150. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/arista.deploy +0 -0
  151. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/arista.order +0 -0
  152. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/arista.rul +0 -0
  153. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/aruba.deploy +0 -0
  154. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/aruba.order +0 -0
  155. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/aruba.rul +0 -0
  156. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/b4com.deploy +0 -0
  157. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/b4com.order +0 -0
  158. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/b4com.rul +0 -0
  159. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/cisco.deploy +0 -0
  160. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/cisco.order +0 -0
  161. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/cisco.rul +0 -0
  162. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/huawei.deploy +0 -0
  163. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/huawei.order +0 -0
  164. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/huawei.rul +0 -0
  165. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/juniper.rul +0 -0
  166. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/nexus.deploy +0 -0
  167. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/nexus.order +0 -0
  168. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/nexus.rul +0 -0
  169. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/nokia.rul +0 -0
  170. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/optixtrans.deploy +0 -0
  171. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/optixtrans.order +0 -0
  172. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/optixtrans.rul +0 -0
  173. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/pc.deploy +0 -0
  174. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/pc.order +0 -0
  175. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/pc.rul +0 -0
  176. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/ribbon.deploy +0 -0
  177. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/ribbon.rul +0 -0
  178. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/routeros.order +0 -0
  179. {annet-0.16.25 → annet-0.16.27}/annet/rulebook/texts/routeros.rul +0 -0
  180. {annet-0.16.25 → annet-0.16.27}/annet/storage.py +0 -0
  181. {annet-0.16.25 → annet-0.16.27}/annet/tabparser.py +0 -0
  182. {annet-0.16.25 → annet-0.16.27}/annet/text_term_format.py +0 -0
  183. {annet-0.16.25 → annet-0.16.27}/annet/tracing.py +0 -0
  184. {annet-0.16.25 → annet-0.16.27}/annet/types.py +0 -0
  185. {annet-0.16.25 → annet-0.16.27}/annet.egg-info/dependency_links.txt +0 -0
  186. {annet-0.16.25 → annet-0.16.27}/annet.egg-info/entry_points.txt +0 -0
  187. {annet-0.16.25 → annet-0.16.27}/annet.egg-info/top_level.txt +0 -0
  188. {annet-0.16.25 → annet-0.16.27}/annet_generators/__init__.py +0 -0
  189. {annet-0.16.25 → annet-0.16.27}/annet_generators/example/__init__.py +0 -0
  190. {annet-0.16.25 → annet-0.16.27}/annet_generators/example/lldp.py +0 -0
  191. {annet-0.16.25 → annet-0.16.27}/annet_generators/mesh_example/__init__.py +0 -0
  192. {annet-0.16.25 → annet-0.16.27}/annet_generators/mesh_example/bgp.py +0 -0
  193. {annet-0.16.25 → annet-0.16.27}/annet_generators/mesh_example/mesh_logic.py +0 -0
  194. {annet-0.16.25 → annet-0.16.27}/requirements.txt +0 -0
  195. {annet-0.16.25 → annet-0.16.27}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: annet
3
- Version: 0.16.25
3
+ Version: 0.16.27
4
4
  Summary: annet
5
5
  Home-page: https://github.com/annetutil/annet
6
6
  License: MIT
@@ -23,4 +23,4 @@ Requires-Dist: yarl>=1.8.2
23
23
  Requires-Dist: adaptix==3.0.0b7
24
24
  Requires-Dist: dataclass-rest==0.4
25
25
  Provides-Extra: netbox
26
- Requires-Dist: annetbox[sync]>=0.1.8; extra == "netbox"
26
+ Requires-Dist: annetbox[sync]>=0.1.10; extra == "netbox"
@@ -1,5 +1,13 @@
1
1
  # Annet - configuration generation and deploying utility for network equipment
2
2
 
3
+ [![Telegram](https://img.shields.io/badge/💬-Telegram-blue)](https://t.me/annet_sup)
4
+ [![PyPI version](https://badge.fury.io/py/annet.svg)](https://pypi.python.org/pypi/annet)
5
+ [![Downloads](https://img.shields.io/pypi/dm/annet.svg)](https://pypistats.org/packages/annet)
6
+ [![License](https://img.shields.io/github/license/annetutil/annet)](https://github.com/annetutil/annet/blob/master/LICENSE)
7
+ [![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/annetutil/annet/setup.yml)](https://github.com/annetutil/annet/actions)
8
+ [![Doc](https://img.shields.io/github/actions/workflow/status/annetutil/annet/docs.yaml?label=docs)](https://annetutil.github.io/annet)
9
+
10
+
3
11
  Annet is a configuration generator that can translate differences between old and new configurations into sequence of commands. This feature is vital for CLI-based devices, such as Huawei, Cisco IOS, Cisco NX-OS, Juniper. Devices configured via separate config files, Linux, FreeBSD and Cumulus are also supported.
4
12
 
5
13
  It works this way. Annet `gen`erates configuration for a device by running Python code, which usually goes to the Network Source of Truth, like NetBox. Annet then gets the `diff`erence by getting the configuration from the device and comparing it. Finally, Annet translates the difference into a sequence of commands, called a `patch`. After `deploy`ing these commands, the diff will be empty.
@@ -1,7 +1,7 @@
1
1
  from annet.annlib.netdev.views.dump import DumpableView
2
2
  from annet.storage import Query
3
3
  from dataclasses import dataclass, fields
4
- from typing import List, Iterable, Optional, Any
4
+ from typing import List, Iterable, Optional, Any, Sequence
5
5
  from annet.storage import StorageProvider, Storage
6
6
  from annet.connectors import AdapterWithName
7
7
  from annet.storage import Device as DeviceCls
@@ -56,6 +56,15 @@ class DeviceStorage:
56
56
  class Device(DeviceCls, DumpableView):
57
57
  dev: DeviceStorage
58
58
 
59
+ def __hash__(self):
60
+ return hash((self.id, type(self)))
61
+
62
+ def __eq__(self, other):
63
+ return type(self) is type(other) and self.fqdn == other.fqdn and self.vendor == other.vendor
64
+
65
+ def is_pc(self) -> bool:
66
+ return False
67
+
59
68
  @property
60
69
  def hostname(self) -> str:
61
70
  return self.dev.hostname
@@ -68,15 +77,6 @@ class Device(DeviceCls, DumpableView):
68
77
  def id(self):
69
78
  return self.dev.id
70
79
 
71
- def __hash__(self):
72
- return hash((self.id, type(self)))
73
-
74
- def __eq__(self, other):
75
- return type(self) is type(other) and self.fqdn == other.fqdn and self.vendor == other.vendor
76
-
77
- def is_pc(self) -> bool:
78
- return False
79
-
80
80
  @property
81
81
  def storage(self) -> Storage:
82
82
  return self
@@ -93,6 +93,18 @@ class Device(DeviceCls, DumpableView):
93
93
  def neighbours_ids(self):
94
94
  pass
95
95
 
96
+ def make_lag(self, lag: int, ports: Sequence[str], lag_min_links: Optional[int]) -> Interface:
97
+ raise NotImplementedError
98
+
99
+ def add_svi(self, svi: int) -> Interface:
100
+ raise NotImplementedError
101
+
102
+ def add_subif(self, interface: str, subif: int) -> Interface:
103
+ raise NotImplementedError
104
+
105
+ def neighbours_fqdns(self) -> list[str]:
106
+ return []
107
+
96
108
 
97
109
  @dataclass
98
110
  class Devices:
@@ -199,6 +211,12 @@ class FS(Storage):
199
211
  def flush_perf(self):
200
212
  pass
201
213
 
214
+ def resolve_all_fdnds(self) -> list[str]:
215
+ return [d.fqdn for d in self.inventory.devices]
216
+
217
+ def search_connections(self, device: "Device", neighbor: "Device") -> list[tuple["Interface", "Interface"]]:
218
+ return []
219
+
202
220
 
203
221
  def filter_query(devices: list[Device], query: Query) -> list[Device]:
204
222
  result: list[Device] = []
@@ -122,7 +122,7 @@ class NetboxStorageV37(Storage):
122
122
  if self._all_fqdns is None:
123
123
  self._all_fqdns = [
124
124
  d.name
125
- for d in self.netbox.dcim_all_devices().results
125
+ for d in self.netbox.dcim_all_devices_brief().results
126
126
  ]
127
127
  return self._all_fqdns
128
128
 
@@ -104,9 +104,9 @@
104
104
  "Nokia.NS7750": " 7750",
105
105
  "Nokia.SR_1s": "SR-1s",
106
106
 
107
- "PC": "^(PC|pc|[Mm]ellanox SN|[Ee]dge-?[Cc]ore|[Mm]oxa|[Aa]sterfusion CX|[Uu]fi[Ss]pace)",
107
+ "PC": "^(PC|pc|[Mm]ellanox SN|[Ee]dge-?[Cc]ore|[Mm]oxa|NVIDIA|[Aa]sterfusion CX|[Uu]fi[Ss]pace)",
108
108
 
109
- "PC.Whitebox": "([Mm]ellanox SN|[Ee]dge-?[Cc]ore|[Aa]sterfusion CX|[Uu]fi[Ss]pace)",
109
+ "PC.Whitebox": "([Mm]ellanox SN|[Ee]dge-?[Cc]ore|NVIDIA|[Aa]sterfusion CX|[Uu]fi[Ss]pace)",
110
110
  "PC.Whitebox.Mellanox": "[Mm]ellanox",
111
111
  "PC.Whitebox.Mellanox.SN": " SN",
112
112
  "PC.Whitebox.Mellanox.SN.SN2100": " SN2100",
@@ -118,6 +118,7 @@
118
118
  "PC.Whitebox.Edgecore.AS9736": "AS9736",
119
119
  "PC.Whitebox.NVIDIA": "NVIDIA",
120
120
  "PC.Whitebox.NVIDIA.SN": " SN",
121
+ "PC.Whitebox.NVIDIA.SN.SN5400": " SN5400",
121
122
  "PC.Whitebox.NVIDIA.SN.SN5600": " SN5600",
122
123
  "PC.Moxa": "[Mm]oxa",
123
124
  "PC.Moxa.NPort": " [Nn][Pp]ort",
@@ -54,11 +54,30 @@ class PatchItem:
54
54
  row: str
55
55
  child: "Union[PatchTree, None]"
56
56
  context: Dict[str, str]
57
+ sort_key: Tuple[Any, ...]
57
58
 
58
- def __init__(self, row, child, context):
59
+ def __init__(self, row, child, context, sort_key):
59
60
  self.row = row
60
61
  self.child = child
61
62
  self.context = context
63
+ self.sort_key = sort_key
64
+
65
+ def to_json(self) -> dict[str, Any]:
66
+ return {
67
+ "row": self.row,
68
+ "child": self.child.to_json() if self.child is not None else None,
69
+ "context": self.context,
70
+ "sort_key": self.sort_key,
71
+ }
72
+
73
+ @classmethod
74
+ def from_json(cls, data: dict[str, Any]) -> "PatchItem":
75
+ return cls(
76
+ row=data["row"],
77
+ child=PatchTree.from_json(data["child"]) if data["child"] is not None else None,
78
+ context=data["context"],
79
+ sort_key=data["sort_key"],
80
+ )
62
81
 
63
82
  def __str__(self):
64
83
  return (
@@ -66,6 +85,7 @@ class PatchItem:
66
85
  f' row="{self.row}",\n'
67
86
  f" child={textwrap.indent(str(self.child), ' ').strip()},\n"
68
87
  f" context={self.context}\n"
88
+ f" sort_key={self.sort_key}\n"
69
89
  f")"
70
90
  )
71
91
 
@@ -78,15 +98,15 @@ class PatchTree:
78
98
  if row:
79
99
  self.add(row, {})
80
100
 
81
- def add(self, row: str, context: Dict[str, str]) -> None:
82
- self.itms.append(PatchItem(row, None, context))
101
+ def add(self, row: str, context: Dict[str, str], sort_key=()) -> None:
102
+ self.itms.append(PatchItem(row, None, context, sort_key))
83
103
 
84
- def add_block(self, row: str, subtree: "Optional[PatchTree]" = None, context: Dict[str, str] = None) -> "PatchTree":
104
+ def add_block(self, row: str, subtree: "Optional[PatchTree]" = None, context: Dict[str, str] = None, sort_key=()) -> "PatchTree":
85
105
  if subtree is None:
86
106
  subtree = PatchTree()
87
107
  if context is None:
88
108
  context = {}
89
- self.itms.append(PatchItem(row, subtree, context))
109
+ self.itms.append(PatchItem(row, subtree, context, sort_key))
90
110
  return subtree
91
111
 
92
112
  def items(self) -> "Iterator[Tuple[str, Union[PatchTree, None]]]":
@@ -106,6 +126,21 @@ class PatchTree:
106
126
  ret[str(row)] = None
107
127
  return ret
108
128
 
129
+ def to_json(self) -> list[dict[str, Any]]:
130
+ return [i.to_json() for i in self.itms]
131
+
132
+ @classmethod
133
+ def from_json(cls, data: list[dict[str, Any]]) -> "PatchTree":
134
+ ret = cls()
135
+ ret.itms = [PatchItem.from_json(i) for i in data]
136
+ return ret
137
+
138
+ def sort(self) -> None:
139
+ self.itms.sort(key=operator.attrgetter("sort_key"))
140
+ for item in self.itms:
141
+ if item.child:
142
+ item.child.sort()
143
+
109
144
  def __bool__(self):
110
145
  return bool(self.itms)
111
146
 
@@ -409,18 +444,19 @@ def make_patch(pre, rb, hw, add_comments, orderer=None, _root_pre=None, do_commi
409
444
  "context": attrs["context"],
410
445
  })
411
446
  tree = PatchTree()
412
- sorted_patch = sorted(patch, key=(lambda item: (
413
- (item["order"] if item["order_direct"] else -item["order"]),
414
- item["raw_rule"],
415
- item["order_direct"],
416
- )))
417
- for item in sorted_patch:
447
+ for item in patch:
448
+ sort_key = (
449
+ (item["order"] if item["order_direct"] else -item["order"]),
450
+ item["raw_rule"],
451
+ item["order_direct"],
452
+ )
418
453
  if (not item["children"] and not item["parent"]) or not item["direct"]:
419
- tree.add(item["row"], item["context"])
454
+ tree.add(item["row"], item["context"], sort_key)
420
455
  else:
421
- tree.add_block(item["row"], item["children"], item["context"])
456
+ tree.add_block(item["row"], item["children"], item["context"], sort_key)
422
457
  if item["force_commit"]:
423
- tree.add("commit", item["context"])
458
+ tree.add("commit", item["context"], sort_key)
459
+ tree.sort()
424
460
  return tree
425
461
 
426
462
 
@@ -1,3 +1,4 @@
1
+ from collections.abc import Sequence, Iterable
1
2
  from dataclasses import dataclass, field
2
3
  from typing import Literal, Union, Optional
3
4
 
@@ -244,6 +245,7 @@ class VrfOptions:
244
245
  ipv6_labeled_unicast: FamilyOptions
245
246
 
246
247
  vrf_name_global: Optional[str] = None
248
+ as_path_relax: bool = False
247
249
  rt_import: list[str] = field(default_factory=list)
248
250
  rt_export: list[str] = field(default_factory=list)
249
251
  rt_import_v4: list[str] = field(default_factory=list)
@@ -261,6 +263,7 @@ class GlobalOptions:
261
263
  ipv4_labeled_unicast: FamilyOptions
262
264
  ipv6_labeled_unicast: FamilyOptions
263
265
 
266
+ as_path_relax: bool = False
264
267
  local_as: ASN = ASN(None)
265
268
  loops: int = 0
266
269
  multipath: int = 0
@@ -268,3 +271,28 @@ class GlobalOptions:
268
271
  vrf: dict[str, VrfOptions] = field(default_factory=dict)
269
272
 
270
273
  groups: list[PeerGroup] = field(default_factory=list)
274
+
275
+
276
+ @dataclass
277
+ class BgpConfig:
278
+ global_options: GlobalOptions
279
+ peers: list[Peer]
280
+
281
+
282
+ def _used_policies(peer: Union[Peer, PeerGroup]) -> Iterable[str]:
283
+ if peer.import_policy:
284
+ yield peer.import_policy
285
+ if peer.export_policy:
286
+ yield peer.export_policy
287
+
288
+
289
+ def extract_policies(config: BgpConfig) -> Sequence[str]:
290
+ result: list[str] = []
291
+ for vrf in config.global_options.vrf.values():
292
+ for group in vrf.groups:
293
+ result.extend(_used_policies(group))
294
+ for group in config.global_options.groups:
295
+ result.extend(_used_policies(group))
296
+ for peer in config.peers:
297
+ result.extend(_used_policies(peer))
298
+ return result
@@ -10,6 +10,9 @@ __all__ = [
10
10
  "Match",
11
11
  "VirtualLocal",
12
12
  "VirtualPeer",
13
+ "PortProcessor",
14
+ "separate_ports",
15
+ "united_ports"
13
16
  ]
14
17
 
15
18
  from .executor import MeshExecutor
@@ -17,3 +20,4 @@ from .match_args import Left, Right, Match
17
20
  from .registry import (
18
21
  DirectPeer, IndirectPeer, MeshSession, GlobalOptions, MeshRulesRegistry, VirtualLocal, VirtualPeer,
19
22
  )
23
+ from .port_processor import PortProcessor, united_ports, separate_ports
@@ -60,6 +60,11 @@ class Concat(Merger):
60
60
  return x + y # type: ignore[operator]
61
61
 
62
62
 
63
+ class Unite(Merger):
64
+ def _merge(self, name: str, x: T, y: T) -> T:
65
+ return x | y # type: ignore[operator]
66
+
67
+
63
68
  class Merge(Merger):
64
69
  def _merge(self, name: str, x: "ModelT", y: "ModelT") -> "ModelT": # type: ignore[override]
65
70
  return merge(x, y)
@@ -61,6 +61,7 @@ class VrfOptions(_FamiliesMixin, BaseMeshModel):
61
61
 
62
62
  vrf_name: str
63
63
  vrf_name_global: Optional[str]
64
+ as_path_relax: bool
64
65
  import_policy: Optional[str]
65
66
  export_policy: Optional[str]
66
67
  rt_import: Annotated[tuple[str, ...], Concat()]
@@ -78,6 +79,7 @@ class GlobalOptionsDTO(_FamiliesMixin, BaseMeshModel):
78
79
  kwargs.setdefault("vrf", KeyDefaultDict(lambda x: VrfOptions(vrf_name=x)))
79
80
  super().__init__(**kwargs)
80
81
 
82
+ as_path_relax: bool
81
83
  local_as: Union[int, str]
82
84
  loops: int
83
85
  multipath: int
@@ -2,7 +2,7 @@ from dataclasses import dataclass
2
2
  from logging import getLogger
3
3
  from typing import Annotated, Callable, Optional, Union
4
4
 
5
- from annet.bgp_models import Peer, GlobalOptions
5
+ from annet.bgp_models import Peer, GlobalOptions, BgpConfig
6
6
  from annet.storage import Device, Storage
7
7
  from .basemodel import merge, BaseMeshModel, Merge, UseLast, MergeForbiddenError
8
8
  from .device_models import GlobalOptionsDTO
@@ -12,6 +12,7 @@ from .registry import (
12
12
  DirectPeer,
13
13
  GlobalOptions as MeshGlobalOptions,
14
14
  IndirectPeer,
15
+ MatchedDirectPair,
15
16
  MeshRulesRegistry,
16
17
  MeshSession,
17
18
  VirtualLocal,
@@ -21,12 +22,6 @@ from .registry import (
21
22
  logger = getLogger(__name__)
22
23
 
23
24
 
24
- @dataclass
25
- class MeshExecutionResult:
26
- global_options: GlobalOptions
27
- peers: list[Peer]
28
-
29
-
30
25
  @dataclass(frozen=True)
31
26
  class PeerKey:
32
27
  fqdn: str
@@ -38,6 +33,7 @@ class Pair(BaseMeshModel):
38
33
  local: Annotated[Union[DirectPeerDTO, IndirectPeerDTO], Merge()]
39
34
  connected: Annotated[Union[DirectPeerDTO, IndirectPeerDTO], Merge()]
40
35
  device: Annotated[Device, UseLast()]
36
+ ports: list[str]
41
37
 
42
38
 
43
39
  class VirtualPair(BaseMeshModel):
@@ -76,72 +72,92 @@ class MeshExecutor:
76
72
  except AttributeError:
77
73
  return str(handler)
78
74
 
75
+ def _execute_direct_pair(
76
+ self,
77
+ device: Device,
78
+ neighbor_device: Device,
79
+ rule: MatchedDirectPair,
80
+ ports: list[tuple[str, str]],
81
+ all_connected_ports: list[tuple[str, str]],
82
+ ) -> Pair:
83
+ session = MeshSession()
84
+ handler_name = self._handler_name(rule.handler)
85
+ logger.debug("Running direct handler: %s", handler_name)
86
+ if rule.direct_order:
87
+ peer_device = DirectPeer(rule.match_left, device, [], [])
88
+ peer_neighbor = DirectPeer(rule.match_right, neighbor_device, [], [])
89
+ else:
90
+ peer_neighbor = DirectPeer(rule.match_left, neighbor_device, [], [])
91
+ peer_device = DirectPeer(rule.match_right, device, [], [])
92
+
93
+ for local_port, remote_port in ports:
94
+ peer_device.ports.append(local_port)
95
+ peer_neighbor.ports.append(remote_port)
96
+ for local_port, remote_port in all_connected_ports:
97
+ peer_device.all_connected_ports.append(local_port)
98
+ peer_neighbor.all_connected_ports.append(remote_port)
99
+
100
+ if rule.direct_order:
101
+ rule.handler(peer_device, peer_neighbor, session)
102
+ else:
103
+ rule.handler(peer_neighbor, peer_device, session)
104
+
105
+ try:
106
+ neighbor_dto = merge(DirectPeerDTO(), peer_neighbor, session)
107
+ except MergeForbiddenError as e:
108
+ raise ValueError(
109
+ f"Handler `{handler_name}` provided session data conflicting with "
110
+ f"peer data for device `{neighbor_device.fqdn}`:\n" + str(e)
111
+ ) from e
112
+ try:
113
+ device_dto = merge(DirectPeerDTO(), peer_device, session)
114
+ except MergeForbiddenError as e:
115
+ raise ValueError(
116
+ f"Handler `{handler_name}` provided session data conflicting with "
117
+ f"peer data for device `{device.fqdn}`:\n" + str(e)
118
+ ) from e
119
+
120
+ return Pair(local=device_dto, connected=neighbor_dto, device=neighbor_device, ports=peer_device.ports)
121
+
79
122
  def _execute_direct(self, device: Device) -> list[Pair]:
80
123
  # we can have multiple rules for the same pair
81
124
  # we merge them according to remote fqdn
82
125
  neighbor_peers: dict[PeerKey, Pair] = {}
83
126
  # TODO batch resolve
84
127
  for rule in self._registry.lookup_direct(device.fqdn, device.neighbours_fqdns):
85
- session = MeshSession()
86
128
  handler_name = self._handler_name(rule.handler)
87
- logger.debug("Running direct handler: %s", handler_name)
88
129
  if rule.direct_order:
89
130
  neighbor_device = self._storage.make_devices([rule.name_right])[0]
90
- peer_device = DirectPeer(rule.match_left, device, [])
91
- peer_neighbor = DirectPeer(rule.match_right, neighbor_device, [])
92
131
  else:
93
132
  neighbor_device = self._storage.make_devices([rule.name_left])[0]
94
- peer_neighbor = DirectPeer(rule.match_left, neighbor_device, [])
95
- peer_device = DirectPeer(rule.match_right, device, [])
96
-
97
- interfaces = self._storage.search_connections(device, neighbor_device)
98
- for local_port, remote_port in interfaces:
99
- peer_device.ports.append(local_port.name)
100
- peer_neighbor.ports.append(remote_port.name)
101
-
102
- if rule.direct_order:
103
- rule.handler(peer_device, peer_neighbor, session)
104
- else:
105
- rule.handler(peer_neighbor, peer_device, session)
106
-
107
- try:
108
- neighbor_dto = merge(DirectPeerDTO(), peer_neighbor, session)
109
- except MergeForbiddenError as e:
110
- raise ValueError(
111
- f"Handler `{handler_name}` provided session data conflicting with "
112
- f"peer data for device `{neighbor_device.fqdn}`:\n" + str(e)
113
- ) from e
114
- try:
115
- device_dto = merge(DirectPeerDTO(), peer_device, session)
116
- except MergeForbiddenError as e:
117
- raise ValueError(
118
- f"Handler `{handler_name}` provided session data conflicting with "
119
- f"peer data for device `{device.fqdn}`:\n" + str(e)
120
- ) from e
121
-
122
- pair = Pair(local=device_dto, connected=neighbor_dto, device=neighbor_device)
123
- addr = getattr(neighbor_dto, "addr", None)
124
- if addr is None:
125
- raise ValueError(f"Handler `{handler_name}` returned no peer addr")
126
- peer_key = PeerKey(
127
- fqdn=neighbor_device.fqdn,
128
- addr=addr,
129
- vrf=getattr(neighbor_dto, "vrf", "")
130
- )
131
- try:
132
- if peer_key in neighbor_peers:
133
- pair = merge(neighbor_peers[peer_key], pair)
134
- except MergeForbiddenError as e:
135
- if rule.direct_order:
136
- pair_names = device.fqdn, neighbor_device.fqdn
137
- else:
138
- pair_names = neighbor_device.fqdn, device.fqdn
139
- raise ValueError(
140
- f"Handler `{handler_name}` provides data conflicting with "
141
- f"previously loaded for device pair {pair_names} "
142
- f"with addr={peer_key.addr}, vrf{peer_key.vrf}:\n" + str(e)
143
- ) from e
144
- neighbor_peers[peer_key] = pair
133
+ all_connected_ports = [
134
+ (p1.name, p2.name)
135
+ for p1, p2 in self._storage.search_connections(device, neighbor_device)
136
+ ]
137
+ for ports in rule.port_processor(all_connected_ports):
138
+ pair = self._execute_direct_pair(device, neighbor_device, rule, ports, all_connected_ports)
139
+ addr = getattr(pair.connected, "addr", None)
140
+ if addr is None:
141
+ raise ValueError(f"Handler `{handler_name}` returned no peer addr")
142
+ peer_key = PeerKey(
143
+ fqdn=pair.device.fqdn,
144
+ addr=addr,
145
+ vrf=getattr(pair.connected, "vrf", "")
146
+ )
147
+ try:
148
+ if peer_key in neighbor_peers:
149
+ pair = merge(neighbor_peers[peer_key], pair)
150
+ except MergeForbiddenError as e:
151
+ if rule.direct_order:
152
+ pair_names = device.fqdn, pair.device.fqdn
153
+ else:
154
+ pair_names = pair.device.fqdn, device.fqdn
155
+ raise ValueError(
156
+ f"Handler `{handler_name}` provides data conflicting with "
157
+ f"previously loaded for device pair {pair_names} "
158
+ f"with addr={peer_key.addr}, vrf{peer_key.vrf}:\n" + str(e)
159
+ ) from e
160
+ neighbor_peers[peer_key] = pair
145
161
  return list(neighbor_peers.values())
146
162
 
147
163
  def _execute_virtual(self, device: Device) -> list[VirtualPair]:
@@ -249,8 +265,15 @@ class MeshExecutor:
249
265
  def _to_bgp_global(self, global_options: GlobalOptionsDTO) -> GlobalOptions:
250
266
  return to_bgp_global_options(global_options)
251
267
 
252
- def _apply_interface_changes(self, device: Device, neighbor: Device, changes: InterfaceChanges) -> str:
253
- port_pairs = self._storage.search_connections(device, neighbor)
268
+ def _apply_interface_changes(
269
+ self, device: Device, neighbor: Device, ports: list[str], changes: InterfaceChanges,
270
+ ) -> str:
271
+ # filter ports according to processed in pair
272
+ port_pairs = [
273
+ p
274
+ for p in self._storage.search_connections(device, neighbor)
275
+ if p[0].name in ports
276
+ ]
254
277
  if len(port_pairs) > 1:
255
278
  if changes.lag is changes.svi is None:
256
279
  raise ValueError(
@@ -279,7 +302,7 @@ class MeshExecutor:
279
302
  def _apply_virtual_interface_changes(self, device: Device, local: VirtualLocalDTO) -> str:
280
303
  return device.add_svi(local.svi).name # we check if SVI configured in execute method
281
304
 
282
- def execute_for(self, device: Device) -> MeshExecutionResult:
305
+ def execute_for(self, device: Device) -> BgpConfig:
283
306
  all_fqdns = self._storage.resolve_all_fdnds()
284
307
 
285
308
  global_options = self._to_bgp_global(self._execute_globals(device))
@@ -289,6 +312,7 @@ class MeshExecutor:
289
312
  target_interface = self._apply_interface_changes(
290
313
  device,
291
314
  direct_pair.device,
315
+ direct_pair.ports,
292
316
  to_interface_changes(direct_pair.local),
293
317
  )
294
318
  peers.append(self._to_bgp_peer(direct_pair, target_interface))
@@ -303,7 +327,7 @@ class MeshExecutor:
303
327
  for connected_pair in self._execute_indirect(device, all_fqdns):
304
328
  peers.append(self._to_bgp_peer(connected_pair, None))
305
329
 
306
- return MeshExecutionResult(
330
+ return BgpConfig(
307
331
  global_options=global_options,
308
332
  peers=peers,
309
333
  )
@@ -1,6 +1,6 @@
1
1
  from typing import Literal, Annotated, Union, Optional
2
2
 
3
- from .basemodel import BaseMeshModel, Concat
3
+ from .basemodel import BaseMeshModel, Concat, Unite
4
4
  from ..bgp_models import BFDTimers
5
5
 
6
6
  FamilyName = Literal["ipv4_unicast", "ipv6_unicast", "ipv4_labeled_unicast", "ipv6_labeled_unicast"]
@@ -25,7 +25,7 @@ class MeshSession(_SharedOptionsDTO):
25
25
  """
26
26
  asnum: Union[int, str]
27
27
  vrf: str
28
- families: Annotated[set[FamilyName], Concat()]
28
+ families: Annotated[set[FamilyName], Unite()]
29
29
  group_name: str
30
30
 
31
31
  bmp_monitor: bool
@@ -116,7 +116,7 @@ class VirtualPeerDTO(MeshSession):
116
116
 
117
117
  class MeshPeerGroup(_OptionsDTO):
118
118
  name: str
119
- families: Annotated[set[FamilyName], Concat()]
119
+ families: Annotated[set[FamilyName], Unite()]
120
120
  local_as: Union[int, str]
121
121
  remote_as: Union[int, str]
122
122
  internal_name: str
@@ -0,0 +1,18 @@
1
+ from abc import abstractmethod
2
+ from typing import Protocol, Sequence
3
+
4
+ PortPair = tuple[str, str]
5
+
6
+
7
+ class PortProcessor(Protocol):
8
+ @abstractmethod
9
+ def __call__(self, pairs: Sequence[PortPair]) -> Sequence[list[PortPair]]:
10
+ raise NotImplementedError
11
+
12
+
13
+ def united_ports(pairs: Sequence[PortPair]) -> Sequence[list[PortPair]]:
14
+ return [list(pairs)]
15
+
16
+
17
+ def separate_ports(pairs: Sequence[PortPair]) -> Sequence[list[PortPair]]:
18
+ return [[pair] for pair in pairs]