annet 1.0.3__tar.gz → 1.1.0__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 (197) hide show
  1. {annet-1.0.3/annet.egg-info → annet-1.1.0}/PKG-INFO +1 -2
  2. {annet-1.0.3 → annet-1.1.0}/README.md +5 -0
  3. {annet-1.0.3 → annet-1.1.0}/annet/annlib/netdev/devdb/data/devdb.json +3 -0
  4. {annet-1.0.3 → annet-1.1.0}/annet/annlib/patching.py +8 -5
  5. {annet-1.0.3 → annet-1.1.0}/annet/annlib/rbparser/ordering.py +5 -0
  6. {annet-1.0.3 → annet-1.1.0}/annet/annlib/rbparser/platform.py +2 -2
  7. {annet-1.0.3 → annet-1.1.0}/annet/annlib/tabparser.py +83 -29
  8. {annet-1.0.3 → annet-1.1.0}/annet/bgp_models.py +83 -1
  9. annet-1.1.0/annet/executor.py +194 -0
  10. {annet-1.0.3 → annet-1.1.0}/annet/mesh/device_models.py +12 -0
  11. {annet-1.0.3 → annet-1.1.0}/annet/mesh/models_converter.py +3 -1
  12. annet-1.1.0/annet/rulebook/arista/aaa.py +63 -0
  13. annet-1.1.0/annet/rulebook/juniper/__init__.py +156 -0
  14. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/arista.order +12 -12
  15. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/arista.rul +1 -1
  16. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/huawei.order +2 -2
  17. annet-1.1.0/annet/rulebook/texts/juniper.order +4 -0
  18. {annet-1.0.3 → annet-1.1.0/annet.egg-info}/PKG-INFO +1 -2
  19. {annet-1.0.3 → annet-1.1.0}/annet.egg-info/SOURCES.txt +2 -1
  20. {annet-1.0.3 → annet-1.1.0}/annet.egg-info/requires.txt +0 -1
  21. {annet-1.0.3 → annet-1.1.0}/requirements.txt +0 -1
  22. annet-1.0.3/annet/executor.py +0 -551
  23. annet-1.0.3/annet/rulebook/juniper/__init__.py +0 -107
  24. annet-1.0.3/annet/rulebook/ribbon/__init__.py +0 -12
  25. {annet-1.0.3 → annet-1.1.0}/AUTHORS +0 -0
  26. {annet-1.0.3 → annet-1.1.0}/LICENSE +0 -0
  27. {annet-1.0.3 → annet-1.1.0}/MANIFEST.in +0 -0
  28. {annet-1.0.3 → annet-1.1.0}/annet/__init__.py +0 -0
  29. {annet-1.0.3 → annet-1.1.0}/annet/adapters/__init__.py +0 -0
  30. {annet-1.0.3 → annet-1.1.0}/annet/adapters/fetchers/__init__.py +0 -0
  31. {annet-1.0.3 → annet-1.1.0}/annet/adapters/fetchers/stub/__init__.py +0 -0
  32. {annet-1.0.3 → annet-1.1.0}/annet/adapters/fetchers/stub/fetcher.py +0 -0
  33. {annet-1.0.3 → annet-1.1.0}/annet/adapters/file/__init__.py +0 -0
  34. {annet-1.0.3 → annet-1.1.0}/annet/adapters/file/provider.py +0 -0
  35. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/__init__.py +0 -0
  36. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/common/__init__.py +0 -0
  37. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/common/client.py +0 -0
  38. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/common/manufacturer.py +0 -0
  39. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/common/models.py +0 -0
  40. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/common/query.py +0 -0
  41. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/common/status_client.py +0 -0
  42. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/common/storage_opts.py +0 -0
  43. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/provider.py +0 -0
  44. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/v24/__init__.py +0 -0
  45. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/v24/storage.py +0 -0
  46. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/v37/__init__.py +0 -0
  47. {annet-1.0.3 → annet-1.1.0}/annet/adapters/netbox/v37/storage.py +0 -0
  48. {annet-1.0.3 → annet-1.1.0}/annet/annet.py +0 -0
  49. {annet-1.0.3 → annet-1.1.0}/annet/annlib/__init__.py +0 -0
  50. {annet-1.0.3 → annet-1.1.0}/annet/annlib/command.py +0 -0
  51. {annet-1.0.3 → annet-1.1.0}/annet/annlib/diff.py +0 -0
  52. {annet-1.0.3 → annet-1.1.0}/annet/annlib/errors.py +0 -0
  53. {annet-1.0.3 → annet-1.1.0}/annet/annlib/filter_acl.py +0 -0
  54. {annet-1.0.3 → annet-1.1.0}/annet/annlib/jsontools.py +0 -0
  55. {annet-1.0.3 → annet-1.1.0}/annet/annlib/lib.py +0 -0
  56. {annet-1.0.3 → annet-1.1.0}/annet/annlib/netdev/__init__.py +0 -0
  57. {annet-1.0.3 → annet-1.1.0}/annet/annlib/netdev/db.py +0 -0
  58. {annet-1.0.3 → annet-1.1.0}/annet/annlib/netdev/devdb/__init__.py +0 -0
  59. {annet-1.0.3 → annet-1.1.0}/annet/annlib/netdev/views/__init__.py +0 -0
  60. {annet-1.0.3 → annet-1.1.0}/annet/annlib/netdev/views/dump.py +0 -0
  61. {annet-1.0.3 → annet-1.1.0}/annet/annlib/netdev/views/hardware.py +0 -0
  62. {annet-1.0.3 → annet-1.1.0}/annet/annlib/output.py +0 -0
  63. {annet-1.0.3 → annet-1.1.0}/annet/annlib/rbparser/__init__.py +0 -0
  64. {annet-1.0.3 → annet-1.1.0}/annet/annlib/rbparser/acl.py +0 -0
  65. {annet-1.0.3 → annet-1.1.0}/annet/annlib/rbparser/deploying.py +0 -0
  66. {annet-1.0.3 → annet-1.1.0}/annet/annlib/rbparser/syntax.py +0 -0
  67. {annet-1.0.3 → annet-1.1.0}/annet/annlib/rulebook/__init__.py +0 -0
  68. {annet-1.0.3 → annet-1.1.0}/annet/annlib/rulebook/common.py +0 -0
  69. {annet-1.0.3 → annet-1.1.0}/annet/annlib/types.py +0 -0
  70. {annet-1.0.3 → annet-1.1.0}/annet/api/__init__.py +0 -0
  71. {annet-1.0.3 → annet-1.1.0}/annet/argparse.py +0 -0
  72. {annet-1.0.3 → annet-1.1.0}/annet/cli.py +0 -0
  73. {annet-1.0.3 → annet-1.1.0}/annet/cli_args.py +0 -0
  74. {annet-1.0.3 → annet-1.1.0}/annet/configs/context.yml +0 -0
  75. {annet-1.0.3 → annet-1.1.0}/annet/configs/logging.yaml +0 -0
  76. {annet-1.0.3 → annet-1.1.0}/annet/connectors.py +0 -0
  77. {annet-1.0.3 → annet-1.1.0}/annet/deploy.py +0 -0
  78. {annet-1.0.3 → annet-1.1.0}/annet/deploy_ui.py +0 -0
  79. {annet-1.0.3 → annet-1.1.0}/annet/diff.py +0 -0
  80. {annet-1.0.3 → annet-1.1.0}/annet/filtering.py +0 -0
  81. {annet-1.0.3 → annet-1.1.0}/annet/gen.py +0 -0
  82. {annet-1.0.3 → annet-1.1.0}/annet/generators/__init__.py +0 -0
  83. {annet-1.0.3 → annet-1.1.0}/annet/generators/base.py +0 -0
  84. {annet-1.0.3 → annet-1.1.0}/annet/generators/common/__init__.py +0 -0
  85. {annet-1.0.3 → annet-1.1.0}/annet/generators/common/initial.py +0 -0
  86. {annet-1.0.3 → annet-1.1.0}/annet/generators/entire.py +0 -0
  87. {annet-1.0.3 → annet-1.1.0}/annet/generators/exceptions.py +0 -0
  88. {annet-1.0.3 → annet-1.1.0}/annet/generators/jsonfragment.py +0 -0
  89. {annet-1.0.3 → annet-1.1.0}/annet/generators/partial.py +0 -0
  90. {annet-1.0.3 → annet-1.1.0}/annet/generators/perf.py +0 -0
  91. {annet-1.0.3 → annet-1.1.0}/annet/generators/ref.py +0 -0
  92. {annet-1.0.3 → annet-1.1.0}/annet/generators/result.py +0 -0
  93. {annet-1.0.3 → annet-1.1.0}/annet/hardware.py +0 -0
  94. {annet-1.0.3 → annet-1.1.0}/annet/implicit.py +0 -0
  95. {annet-1.0.3 → annet-1.1.0}/annet/lib.py +0 -0
  96. {annet-1.0.3 → annet-1.1.0}/annet/mesh/__init__.py +0 -0
  97. {annet-1.0.3 → annet-1.1.0}/annet/mesh/basemodel.py +0 -0
  98. {annet-1.0.3 → annet-1.1.0}/annet/mesh/executor.py +0 -0
  99. {annet-1.0.3 → annet-1.1.0}/annet/mesh/match_args.py +0 -0
  100. {annet-1.0.3 → annet-1.1.0}/annet/mesh/peer_models.py +0 -0
  101. {annet-1.0.3 → annet-1.1.0}/annet/mesh/port_processor.py +0 -0
  102. {annet-1.0.3 → annet-1.1.0}/annet/mesh/registry.py +0 -0
  103. {annet-1.0.3 → annet-1.1.0}/annet/output.py +0 -0
  104. {annet-1.0.3 → annet-1.1.0}/annet/parallel.py +0 -0
  105. {annet-1.0.3 → annet-1.1.0}/annet/patching.py +0 -0
  106. {annet-1.0.3 → annet-1.1.0}/annet/reference.py +0 -0
  107. {annet-1.0.3 → annet-1.1.0}/annet/rpl/__init__.py +0 -0
  108. {annet-1.0.3 → annet-1.1.0}/annet/rpl/action.py +0 -0
  109. {annet-1.0.3 → annet-1.1.0}/annet/rpl/condition.py +0 -0
  110. {annet-1.0.3 → annet-1.1.0}/annet/rpl/match_builder.py +0 -0
  111. {annet-1.0.3 → annet-1.1.0}/annet/rpl/policy.py +0 -0
  112. {annet-1.0.3 → annet-1.1.0}/annet/rpl/result.py +0 -0
  113. {annet-1.0.3 → annet-1.1.0}/annet/rpl/routemap.py +0 -0
  114. {annet-1.0.3 → annet-1.1.0}/annet/rpl/statement_builder.py +0 -0
  115. {annet-1.0.3 → annet-1.1.0}/annet/rpl_generators/__init__.py +0 -0
  116. {annet-1.0.3 → annet-1.1.0}/annet/rpl_generators/aspath.py +0 -0
  117. {annet-1.0.3 → annet-1.1.0}/annet/rpl_generators/community.py +0 -0
  118. {annet-1.0.3 → annet-1.1.0}/annet/rpl_generators/cumulus_frr.py +0 -0
  119. {annet-1.0.3 → annet-1.1.0}/annet/rpl_generators/entities.py +0 -0
  120. {annet-1.0.3 → annet-1.1.0}/annet/rpl_generators/execute.py +0 -0
  121. {annet-1.0.3 → annet-1.1.0}/annet/rpl_generators/policy.py +0 -0
  122. {annet-1.0.3 → annet-1.1.0}/annet/rpl_generators/prefix_lists.py +0 -0
  123. {annet-1.0.3 → annet-1.1.0}/annet/rpl_generators/rd.py +0 -0
  124. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/__init__.py +0 -0
  125. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/arista/__init__.py +0 -0
  126. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/arista/iface.py +0 -0
  127. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/aruba/__init__.py +0 -0
  128. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/aruba/ap_env.py +0 -0
  129. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/aruba/misc.py +0 -0
  130. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/b4com/__init__.py +0 -0
  131. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/b4com/file.py +0 -0
  132. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/b4com/iface.py +0 -0
  133. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/cisco/__init__.py +0 -0
  134. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/cisco/iface.py +0 -0
  135. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/cisco/misc.py +0 -0
  136. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/cisco/vlandb.py +0 -0
  137. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/common.py +0 -0
  138. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/deploying.py +0 -0
  139. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/huawei/__init__.py +0 -0
  140. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/huawei/aaa.py +0 -0
  141. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/huawei/bgp.py +0 -0
  142. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/huawei/iface.py +0 -0
  143. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/huawei/misc.py +0 -0
  144. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/huawei/vlandb.py +0 -0
  145. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/nexus/__init__.py +0 -0
  146. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/nexus/iface.py +0 -0
  147. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/patching.py +0 -0
  148. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/routeros/__init__.py +0 -0
  149. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/routeros/file.py +0 -0
  150. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/arista.deploy +0 -0
  151. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/aruba.deploy +0 -0
  152. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/aruba.order +0 -0
  153. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/aruba.rul +0 -0
  154. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/b4com.deploy +0 -0
  155. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/b4com.order +0 -0
  156. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/b4com.rul +0 -0
  157. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/cisco.deploy +0 -0
  158. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/cisco.order +0 -0
  159. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/cisco.rul +0 -0
  160. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/huawei.deploy +0 -0
  161. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/huawei.rul +0 -0
  162. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/juniper.rul +0 -0
  163. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/nexus.deploy +0 -0
  164. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/nexus.order +0 -0
  165. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/nexus.rul +0 -0
  166. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/nokia.rul +0 -0
  167. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/optixtrans.deploy +0 -0
  168. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/optixtrans.order +0 -0
  169. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/optixtrans.rul +0 -0
  170. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/pc.deploy +0 -0
  171. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/pc.order +0 -0
  172. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/pc.rul +0 -0
  173. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/ribbon.deploy +0 -0
  174. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/ribbon.rul +0 -0
  175. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/routeros.order +0 -0
  176. {annet-1.0.3 → annet-1.1.0}/annet/rulebook/texts/routeros.rul +0 -0
  177. {annet-1.0.3 → annet-1.1.0}/annet/storage.py +0 -0
  178. {annet-1.0.3 → annet-1.1.0}/annet/tabparser.py +0 -0
  179. {annet-1.0.3 → annet-1.1.0}/annet/text_term_format.py +0 -0
  180. {annet-1.0.3 → annet-1.1.0}/annet/tracing.py +0 -0
  181. {annet-1.0.3 → annet-1.1.0}/annet/types.py +0 -0
  182. {annet-1.0.3 → annet-1.1.0}/annet.egg-info/dependency_links.txt +0 -0
  183. {annet-1.0.3 → annet-1.1.0}/annet.egg-info/entry_points.txt +0 -0
  184. {annet-1.0.3 → annet-1.1.0}/annet.egg-info/top_level.txt +0 -0
  185. {annet-1.0.3 → annet-1.1.0}/annet_generators/__init__.py +0 -0
  186. {annet-1.0.3 → annet-1.1.0}/annet_generators/example/__init__.py +0 -0
  187. {annet-1.0.3 → annet-1.1.0}/annet_generators/example/lldp.py +0 -0
  188. {annet-1.0.3 → annet-1.1.0}/annet_generators/mesh_example/__init__.py +0 -0
  189. {annet-1.0.3 → annet-1.1.0}/annet_generators/mesh_example/bgp.py +0 -0
  190. {annet-1.0.3 → annet-1.1.0}/annet_generators/mesh_example/mesh_logic.py +0 -0
  191. {annet-1.0.3 → annet-1.1.0}/annet_generators/rpl_example/__init__.py +0 -0
  192. {annet-1.0.3 → annet-1.1.0}/annet_generators/rpl_example/generator.py +0 -0
  193. {annet-1.0.3 → annet-1.1.0}/annet_generators/rpl_example/items.py +0 -0
  194. {annet-1.0.3 → annet-1.1.0}/annet_generators/rpl_example/mesh.py +0 -0
  195. {annet-1.0.3 → annet-1.1.0}/annet_generators/rpl_example/route_policy.py +0 -0
  196. {annet-1.0.3 → annet-1.1.0}/setup.cfg +0 -0
  197. {annet-1.0.3 → annet-1.1.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: annet
3
- Version: 1.0.3
3
+ Version: 1.1.0
4
4
  Summary: annet
5
5
  Home-page: https://github.com/annetutil/annet
6
6
  License: MIT
@@ -15,7 +15,6 @@ Requires-Dist: PyYAML>=6.0.1
15
15
  Requires-Dist: Pygments>=2.14.0
16
16
  Requires-Dist: Mako>=1.2.4
17
17
  Requires-Dist: Jinja2>=3.1.2
18
- Requires-Dist: psutil>=5.8.0
19
18
  Requires-Dist: packaging>=23.2
20
19
  Requires-Dist: contextlog>=1.1
21
20
  Requires-Dist: valkit>=0.1.4
@@ -68,3 +68,8 @@ sphinx-build -M html docs docs-build
68
68
  ```
69
69
 
70
70
  3. Open rendered html in browser docs-build/html/index.html
71
+
72
+ ## Links
73
+
74
+ * [Online Documentation](https://annetutil.github.io/annet/)
75
+ * [Tutorial](https://annetutil.github.io/annet/main/usage/tutorial.html)
@@ -132,6 +132,9 @@
132
132
  "PC.Whitebox.Ufispace.S": " S",
133
133
  "PC.Whitebox.Ufispace.S.S9100": " S91\\d\\d",
134
134
  "PC.Whitebox.Ufispace.S.S9100.S9110_32X": " S9110-32X",
135
+ "PC.Whitebox.Ufispace.S.S9300": " S93\\d\\d",
136
+ "PC.Whitebox.Ufispace.S.S9300.S9301_32DB": " S9301-32DB",
137
+ "PC.Whitebox.Ufispace.S.S9300.S9321_64EO": " S9321-64EO",
135
138
  "PC.Nebius": "^Nebius",
136
139
  "PC.Nebius.NB-E-BR-DCU-AST2600": "^Nebius NB-E-BR-DCU-AST2600",
137
140
 
@@ -186,6 +186,9 @@ class Orderer:
186
186
  block_exit = platform.VENDOR_EXIT[self.vendor]
187
187
 
188
188
  for (order, (raw_rule, rule)) in enumerate(ordering.items()):
189
+ if rule["attrs"]["global"]:
190
+ children.append((raw_rule, rule))
191
+
189
192
  direct_matched = bool(rule["attrs"]["direct_regexp"].match(row))
190
193
  if not rule["attrs"]["order_reverse"] and (direct_matched or rule["attrs"]["reverse_regexp"].match(row)):
191
194
  # если не указано order_reverse - правило считается прямым
@@ -395,7 +398,7 @@ def make_patch(pre, rb, hw, add_comments, orderer=None, _root_pre=None, do_commi
395
398
  for (key, diff) in content["items"].items():
396
399
  # чтобы logic не мог поменять атрибуты
397
400
  rule_pre = content.copy()
398
- attrs = rule_pre["attrs"].copy()
401
+ attrs = copy.deepcopy(rule_pre["attrs"])
399
402
 
400
403
  iterable = attrs["logic"](
401
404
  rule=attrs,
@@ -544,8 +547,7 @@ def _select_match(matches, rules):
544
547
  for (rule, is_cr_allowed) in map(operator.itemgetter(0), matches):
545
548
  if is_cr_allowed:
546
549
  local_children = merge_dicts(local_children, rule["children"]["local"])
547
- # optional break on is_cr_allowed==False?
548
-
550
+ # optional break on is_cr_allowed==False?
549
551
  global_children = merge_dicts(global_children, rule["children"]["global"])
550
552
 
551
553
  global_children = merge_dicts(global_children, rules["global"])
@@ -555,9 +557,10 @@ def _select_match(matches, rules):
555
557
  "global": global_children,
556
558
  }
557
559
 
558
- match = {"attrs": f_rule["attrs"]}
560
+ match = {"attrs": copy.deepcopy(f_rule["attrs"])}
559
561
  match.update(f_other)
560
- return (match, children_rules)
562
+
563
+ return match, children_rules
561
564
 
562
565
 
563
566
  def _rules_local_global(rules):
@@ -16,6 +16,10 @@ def compile_ordering_text(text, vendor):
16
16
  "validator": valid_bool,
17
17
  "default": False,
18
18
  },
19
+ "global": {
20
+ "validator": valid_bool,
21
+ "default": False,
22
+ }
19
23
  }),
20
24
  reverse_prefix=platform.VENDOR_REVERSES[vendor],
21
25
  )
@@ -44,6 +48,7 @@ def _compile_ordering(tree, reverse_prefix):
44
48
  syntax.compile_row_regexp(re.sub(r"^%s\s+" % (reverse_prefix), "", attrs["row"]))
45
49
  ),
46
50
  "order_reverse": attrs["params"]["order_reverse"],
51
+ "global": attrs["params"]["global"],
47
52
  "raw_rule": attrs["raw_rule"],
48
53
  "context": attrs["context"],
49
54
  },
@@ -25,7 +25,7 @@ VENDOR_DIFF = {
25
25
  "routeros": "common.default_diff",
26
26
  "aruba": "aruba.default_diff",
27
27
  "pc": "common.default_diff",
28
- "ribbon": "ribbon.default_diff",
28
+ "ribbon": "common.default_diff",
29
29
  "b4com": "common.default_diff",
30
30
  }
31
31
 
@@ -40,7 +40,7 @@ VENDOR_DIFF_ORDERED = {
40
40
  "routeros": "common.ordered_diff",
41
41
  "aruba": "common.ordered_diff",
42
42
  "pc": "common.ordered_diff",
43
- "ribbon": "ribbon.default_diff",
43
+ "ribbon": "common.ordered_diff",
44
44
  "b4com": "common.ordered_diff",
45
45
  }
46
46
 
@@ -1,6 +1,8 @@
1
1
  import dataclasses
2
2
  import itertools
3
+ import json
3
4
  import re
5
+ import textwrap
4
6
  from collections import OrderedDict as odict
5
7
  from typing import TYPE_CHECKING, Any, Dict, Iterable, Optional, Tuple, Union, List
6
8
 
@@ -70,10 +72,10 @@ class CommonFormatter:
70
72
  self._block_end = ""
71
73
  self._statement_end = ""
72
74
 
73
- def split(self, text):
75
+ def split(self, text: str):
74
76
  return list(filter(None, text.split("\n")))
75
77
 
76
- def join(self, config):
78
+ def join(self, config: "PatchTree"):
77
79
  return "\n".join(
78
80
  _filtered_block_marks(
79
81
  self._indent_blocks(self._blocks(config, is_patch=False))
@@ -86,14 +88,14 @@ class CommonFormatter:
86
88
  def diff(self, diff):
87
89
  return list(self.diff_generator(diff))
88
90
 
89
- def patch(self, patch):
91
+ def patch(self, patch: "PatchTree") -> str:
90
92
  return "\n".join(
91
93
  _filtered_block_marks(
92
94
  self._indent_blocks(self._blocks(patch, is_patch=True))
93
95
  )
94
96
  )
95
97
 
96
- def cmd_paths(self, patch):
98
+ def cmd_paths(self, patch: "PatchTree") -> odict:
97
99
  ret = odict()
98
100
  path = []
99
101
  for row, context in self.blocks_and_context(patch, is_patch=True):
@@ -175,7 +177,7 @@ class CommonFormatter:
175
177
  )
176
178
  yield BlockEnd, None
177
179
 
178
- def _blocks(self, tree, is_patch):
180
+ def _blocks(self, tree: "PatchTree", is_patch: bool):
179
181
  for row, _context in self.blocks_and_context(tree, is_patch):
180
182
  yield row
181
183
 
@@ -386,7 +388,32 @@ class AsrFormatter(BlockExitFormatter):
386
388
 
387
389
 
388
390
  class JuniperFormatter(CommonFormatter):
389
- patch_set_prefix = "set "
391
+ patch_set_prefix = "set"
392
+
393
+ @dataclasses.dataclass
394
+ class Comment:
395
+ begin = "/*"
396
+ end = "*/"
397
+
398
+ row: str
399
+ comment: str
400
+
401
+ def __post_init__(self):
402
+ self.row = self.row.strip()
403
+ self.comment = self.comment.strip()
404
+
405
+ @classmethod
406
+ def loads(cls, value: str):
407
+ return cls(
408
+ **json.loads(
409
+ value.removeprefix(cls.begin)
410
+ .removesuffix(cls.end)
411
+ .strip()
412
+ )
413
+ )
414
+
415
+ def dumps(self):
416
+ return json.dumps({"row": self.row, "comment": self.comment})
390
417
 
391
418
  def __init__(self, indent=" "):
392
419
  super().__init__(indent)
@@ -395,20 +422,32 @@ class JuniperFormatter(CommonFormatter):
395
422
  self._statement_end = ";"
396
423
  self._endofline_comment = "; ##"
397
424
 
398
- def split(self, text):
399
- sub_regexs = (
425
+ self._sub_regexs = (
400
426
  (re.compile(self._block_begin + r"\s*" + self._block_end + r"$"), ""), # collapse empty blocks
401
427
  (re.compile(self._block_begin + "(\t# .+)?$"), ""),
402
428
  (re.compile(self._statement_end + r"$"), ""),
403
429
  (re.compile(r"\s*" + self._block_end + "(\t# .+)?$"), ""),
404
430
  (re.compile(self._endofline_comment + r".*$"), ""),
405
431
  )
406
- split = []
407
- for line in text.split("\n"):
408
- for (regex, repl_line) in sub_regexs:
409
- line = regex.sub(repl_line, line)
410
- split.append(line)
411
- return list(filter(None, split))
432
+
433
+ def sub_regexs(self, value: str) -> str:
434
+ for (regex, repl_line) in self._sub_regexs:
435
+ value = regex.sub(repl_line, value)
436
+ return value
437
+
438
+ def split(self, text: str) -> list[str]:
439
+ comment_begin, comment_end = map(re.escape, (self.Comment.begin, self.Comment.end))
440
+ comment_regexp = re.compile(fr"(\s+{comment_begin})((?:(?!{comment_end}).)*)({comment_end})")
441
+
442
+ result = []
443
+ lines = text.split("\n")
444
+ for i, line in enumerate(lines):
445
+ line = self.sub_regexs(line)
446
+ if i + 1 < len(lines) and (m := comment_regexp.match(line)):
447
+ line = f"{m.group(1)} {self.Comment(self.sub_regexs(lines[i + 1]), m.group(2)).dumps()} {m.group(3)}"
448
+ result.append(line)
449
+
450
+ return list(filter(None, result))
412
451
 
413
452
  def join(self, config):
414
453
  return "\n".join(_filtered_block_marks(self._formatted_blocks(self._indented_blocks(config))))
@@ -433,30 +472,45 @@ class JuniperFormatter(CommonFormatter):
433
472
  yield line + self._statement_end
434
473
  yield self._indent * level + self._block_end
435
474
  elif isinstance(line, str):
436
- yield line + self._statement_end
475
+ yield line + ("" if line.endswith(self.Comment.end) else self._statement_end)
437
476
  line = new_line
438
477
  if isinstance(line, str):
439
478
  yield line + self._statement_end
440
479
 
441
- def cmd_paths(self, patch, _prev=""):
480
+ def cmd_paths(self, patch, _prev=tuple()):
442
481
  commands = odict()
443
482
  for item in patch.itms:
444
483
  key, childs, context = item.row, item.child, item.context
484
+
445
485
  if childs:
446
- for k, v in self.cmd_paths(childs, _prev + " " + key).items():
486
+ for k, v in self.cmd_paths(childs, (*_prev, key.strip())).items():
447
487
  commands[k] = v
448
488
  else:
449
- if key.startswith("delete"):
450
- cmd = "delete" + _prev + " " + key.replace("delete", "", 1).strip()
489
+ if "comment" in context:
490
+ value = (
491
+ ""
492
+ if key.startswith("delete")
493
+ else context["comment"]
494
+ )
495
+
496
+ cmd = "\n".join(
497
+ (
498
+ "edit " + " ".join(_prev),
499
+ " ".join(("annotate", context["row"].split(" ")[0], f'"{value}"')),
500
+ "exit"
501
+ )
502
+ )
503
+ elif key.startswith("delete"):
504
+ cmd = " ".join(("delete", *_prev, key.replace("delete", "", 1).strip()))
451
505
  elif key.startswith("activate"):
452
- cmd = "activate" + _prev + " " + key.replace("activate", "", 1).strip()
506
+ cmd = " ".join(("activate", *_prev, key.replace("activate", "", 1).strip()))
453
507
  elif key.startswith("deactivate"):
454
- cmd = "deactivate" + _prev + " " + key.replace("deactivate", "", 1).strip()
508
+ cmd = " ".join(("deactivate", *_prev, key.replace("deactivate", "", 1).strip()))
455
509
  else:
456
- cmd = (self.patch_set_prefix + _prev.strip()).strip() + " " + key
510
+ cmd = " ".join((self.patch_set_prefix, *_prev, key.strip()))
511
+
457
512
  # Expanding [ a b c ] junipers list of arguments
458
- matches = re.search(r"^(.*)\s+\[(.+)\]$", cmd)
459
- if matches:
513
+ if matches := re.search(r"^(.*)\s+\[(.+)\]$", cmd):
460
514
  for c in matches.group(2).split(" "):
461
515
  if c.strip():
462
516
  cmd = " ".join([matches.group(1), c])
@@ -490,7 +544,7 @@ class JuniperList:
490
544
 
491
545
 
492
546
  class NokiaFormatter(JuniperFormatter):
493
- patch_set_prefix = "/configure "
547
+ patch_set_prefix = "/configure"
494
548
 
495
549
  def __init__(self, *args, **kwargs):
496
550
  super().__init__(*args, **kwargs)
@@ -517,18 +571,18 @@ class NokiaFormatter(JuniperFormatter):
517
571
  finish = finish if finish is not None else len(ret)
518
572
  return ret[start:finish]
519
573
 
520
- def cmd_paths(self, patch, _prev=""):
574
+ def cmd_paths(self, patch, _prev=tuple()):
521
575
  commands = odict()
522
576
  for item in patch.itms:
523
577
  key, childs, context = item.row, item.child, item.context
524
578
  if childs:
525
- for k, v in self.cmd_paths(childs, _prev + " " + key).items():
579
+ for k, v in self.cmd_paths(childs, (*_prev, key.strip())).items():
526
580
  commands[k] = v
527
581
  else:
528
582
  if key.startswith("delete"):
529
- cmd = "/configure delete" + _prev + " " + key.replace("delete", "", 1).strip()
583
+ cmd = " ".join((self.patch_set_prefix, "delete", *_prev, key.replace("delete", "", 1).strip()))
530
584
  else:
531
- cmd = self.patch_set_prefix + _prev.strip() + " " + key
585
+ cmd = " ".join((self.patch_set_prefix, *_prev, key.strip()))
532
586
  # Expanding [ a b c ] junipers list of arguments
533
587
  matches = re.search(r"^(.*)\s+\[(.+)\]$", cmd)
534
588
  if matches:
@@ -3,6 +3,77 @@ from dataclasses import dataclass, field
3
3
  from typing import Literal, Union, Optional
4
4
 
5
5
 
6
+ class VidRange:
7
+ def __init__(self, start: int, stop: int) -> None:
8
+ self.start = start
9
+ self.stop = stop
10
+
11
+ def is_single(self):
12
+ return self.start == self.stop
13
+
14
+ def __iter__(self):
15
+ return iter(range(self.start, self.stop + 1))
16
+
17
+ def __str__(self):
18
+ if self.is_single():
19
+ return str(self.start)
20
+ return f"{self.start}-{self.stop}"
21
+
22
+ def __repr__(self):
23
+ return f"VlanRange({self.start}, {self.stop})"
24
+
25
+ def __eq__(self, other: object) -> bool:
26
+ if type(other) is VidRange:
27
+ return self.start == other.start and self.stop == other.stop
28
+ return NotImplemented
29
+
30
+
31
+ def _parse_vlan_ranges(ranges: str) -> Iterable[VidRange]:
32
+ for range in ranges.split(","):
33
+ start, sep, stop = range.strip().partition("-")
34
+ try:
35
+ if not sep:
36
+ int_start = int(start)
37
+ yield VidRange(int_start, int_start)
38
+ elif not stop or not start:
39
+ raise ValueError(f"Cannot parse range {range!r}. Expected `start-stop`")
40
+ else:
41
+ yield VidRange(int(start), int(stop))
42
+ except ValueError:
43
+ raise ValueError(f"Cannot parse range {range!r}. Expected `vid1-vid2` or `vid`")
44
+
45
+
46
+ class VidCollection:
47
+ @staticmethod
48
+ def parse(ranges: int | str) -> "VidCollection":
49
+ if isinstance(ranges, int):
50
+ return VidCollection([VidRange(ranges, ranges)])
51
+ elif isinstance(ranges, str):
52
+ return VidCollection(list(_parse_vlan_ranges(ranges)))
53
+ elif isinstance(ranges, VidCollection):
54
+ return VidCollection(ranges.ranges)
55
+ else:
56
+ raise TypeError(f"Expected str or int, got {type(ranges)}")
57
+
58
+ def __init__(self, ranges: list[VidRange]) -> None:
59
+ self.ranges = ranges
60
+
61
+ def __str__(self):
62
+ return ",".join(map(str, self.ranges))
63
+
64
+ def __repr__(self):
65
+ return f"VlanCollection({str(self)!r})"
66
+
67
+ def __iter__(self):
68
+ for range in self.ranges:
69
+ yield from range
70
+
71
+ def __eq__(self, other: object) -> bool:
72
+ if type(other) is VidCollection:
73
+ return self.ranges == other.ranges
74
+ return False
75
+
76
+
6
77
  class ASN(int):
7
78
  """
8
79
  Stores ASN number and formats it as в AS1.AS2
@@ -235,6 +306,17 @@ class PeerGroup:
235
306
  mtu: int = 0
236
307
 
237
308
 
309
+ @dataclass
310
+ class L2VpnOptions:
311
+ name: str
312
+ vid: VidCollection
313
+ l2vni: int # VNI, possible values are 1 to 2**24-1
314
+ route_distinguisher: str = "" # like in VrfOptions
315
+ rt_import: list[str] = field(default_factory=list) # like in VrfOptions
316
+ rt_export: list[str] = field(default_factory=list) # like in VrfOptions
317
+ advertise_host_routes: bool = True # advertise IP+MAC routes into L3VNI
318
+
319
+
238
320
  @dataclass
239
321
  class VrfOptions:
240
322
  vrf_name: str
@@ -274,8 +356,8 @@ class GlobalOptions:
274
356
  multipath: int = 0
275
357
  router_id: str = ""
276
358
  vrf: dict[str, VrfOptions] = field(default_factory=dict)
277
-
278
359
  groups: list[PeerGroup] = field(default_factory=list)
360
+ l2vpn: dict[str, L2VpnOptions] = field(default_factory=dict)
279
361
 
280
362
 
281
363
  @dataclass
@@ -0,0 +1,194 @@
1
+ import asyncio
2
+ import os
3
+ import statistics
4
+ from abc import ABC, abstractmethod
5
+ from functools import partial
6
+ from operator import itemgetter
7
+ from typing import Any, Dict, List, Optional, Union
8
+
9
+ import colorama
10
+ from annet.annlib.command import Command, CommandList, Question # noqa: F401
11
+
12
+
13
+ class CommandResult(ABC):
14
+ @abstractmethod
15
+ def get_out(self) -> str:
16
+ pass
17
+
18
+
19
+ class Connector(ABC):
20
+ @abstractmethod
21
+ async def cmd(self, cmd: Union[Command, str]) -> CommandResult:
22
+ pass
23
+
24
+ @abstractmethod
25
+ async def download(self, files: List[str]) -> Dict[str, str]:
26
+ pass
27
+
28
+ @abstractmethod
29
+ async def upload(self, files: Dict[str, str]):
30
+ pass
31
+
32
+ @abstractmethod
33
+ def get_conn_trace(self) -> str:
34
+ pass
35
+
36
+ @abstractmethod
37
+ async def aclose(self) -> str:
38
+ pass
39
+
40
+
41
+ class ExecutorException(Exception):
42
+ def __init__(self, *args: List[Any], auxiliary: Optional[Any] = None, **kwargs: object):
43
+ self.auxiliary = auxiliary
44
+ super().__init__(*args, **kwargs)
45
+
46
+ def __repr__(self) -> str:
47
+ return "%s(args=%r,auxiliary=%s)" % (self.__class__.__name__, self.args, self.auxiliary)
48
+
49
+
50
+ class ExecException(ExecutorException):
51
+ def __init__(self, msg: str, cmd: str, res: str, **kwargs):
52
+ super().__init__(**kwargs)
53
+ self.args = msg, cmd, res
54
+ self.kwargs = kwargs
55
+ self.msg = msg
56
+ self.cmd = cmd
57
+ self.res = res
58
+
59
+ def __str__(self) -> str:
60
+ return str(self.msg)
61
+
62
+ def __repr__(self) -> str:
63
+ return "%s<%s, %s>" % (self.__class__.__name__, self.msg, self.cmd)
64
+
65
+
66
+ class BadCommand(ExecException):
67
+ pass
68
+
69
+
70
+ class NonzeroRetcode(ExecException):
71
+ pass
72
+
73
+
74
+ class CommitException(ExecException):
75
+ pass
76
+
77
+
78
+ def _show_type_summary(caption, items, total, stat_items=None):
79
+ if items:
80
+ if not stat_items:
81
+ stat = ""
82
+ else:
83
+ avg = statistics.mean(stat_items)
84
+ stat = " %(min).1f/%(max).1f/%(avg).1f/%(stdev)s (min/max/avg/stdev)" % dict(
85
+ min=min(stat_items),
86
+ max=max(stat_items),
87
+ avg=avg,
88
+ stdev="-" if len(stat_items) < 2 else "%.1f" % statistics.stdev(stat_items, xbar=avg)
89
+ )
90
+
91
+ print("%-8s %d of %d%s" % (caption, len(items), total, stat))
92
+
93
+
94
+ def show_bulk_report(hostnames, res, durations, log_dir):
95
+ total = len(hostnames)
96
+ if not total:
97
+ return
98
+
99
+ colorama.init()
100
+
101
+ print("\n====== bulk deploy report ======")
102
+
103
+ done = [host for (host, hres) in res.items() if not isinstance(hres, Exception)]
104
+ cancelled = [host for (host, hres) in res.items() if isinstance(hres, asyncio.CancelledError)]
105
+ failed = [host for (host, hres) in res.items() if isinstance(hres, Exception) and host not in cancelled]
106
+ lost = [host for host in hostnames if host not in res]
107
+ limit = 30
108
+
109
+ _show_type_summary("Done :", done, total, [durations[h] for h in done])
110
+ _print_limit(done, partial(_print_hostname, style=colorama.Fore.GREEN), limit, total)
111
+
112
+ _show_type_summary("Failed :", failed, total, [durations[h] for h in failed])
113
+
114
+ _print_limit(failed, partial(_print_failed, res=res), limit, total)
115
+
116
+ _show_type_summary("Cancelled :", cancelled, total, [durations[h] for h in cancelled if durations[h] is not None])
117
+ _print_limit(cancelled, partial(_print_hostname, style=colorama.Fore.RED), limit, total)
118
+
119
+ _show_type_summary("Lost :", lost, total)
120
+ _print_limit(lost, _print_hostname, limit, total)
121
+
122
+ err_limit = 5
123
+ if failed:
124
+ errs = {}
125
+ for hostname in failed:
126
+ fmt_err = _format_exc(res[hostname])
127
+ if fmt_err in errs:
128
+ errs[fmt_err] += 1
129
+ else:
130
+ errs[fmt_err] = 1
131
+ print("Top errors :")
132
+ for fmt_err, n in sorted(errs.items(), key=itemgetter(1), reverse=True)[:err_limit]:
133
+ print(" %-4d %s" % (n, fmt_err))
134
+ print("\n", end="")
135
+
136
+ if log_dir:
137
+ print("See deploy logs in %s/\n" % os.path.relpath(log_dir))
138
+
139
+
140
+ def _format_exc(exc):
141
+ if isinstance(exc, ExecException):
142
+ cmd = str(exc.cmd)
143
+ if len(cmd) > 50:
144
+ cmd = cmd[:50] + "~.."
145
+ return "'%s', cmd '%s'" % (exc.msg, cmd)
146
+ elif isinstance(exc, ExecutorException):
147
+ return "%s%r" % (exc.__class__.__name__, exc.args) # исключить многословный auxiliary
148
+ else:
149
+ return repr(exc)
150
+
151
+
152
+ def _print_hostname(host, style=None):
153
+ if style:
154
+ host = style + host + colorama.Style.RESET_ALL
155
+ print(" %s" % host)
156
+
157
+
158
+ def _print_limit(items, printer, limit, total, end="\n"):
159
+ if not items:
160
+ return
161
+ if len(items) > limit and len(items) > total * 0.7:
162
+ print(" ... %d hosts" % len(items))
163
+ for host in items[:limit]:
164
+ printer(host)
165
+ if len(items) > limit:
166
+ print(" ... %d more hosts" % (len(items) - limit))
167
+
168
+ print(end, end="")
169
+
170
+
171
+ def _print_failed(host, res):
172
+ exc = res[host]
173
+ color = colorama.Fore.YELLOW if isinstance(exc, Warning) else colorama.Fore.RED
174
+ print(" %s - %s" % (color + host + colorama.Style.RESET_ALL, _format_exc(exc)))
175
+
176
+
177
+ class DeferredFileWrite:
178
+ def __init__(self, file, mode="r"):
179
+ self._file = file
180
+ wrapper = {"w": "a", "wb": "ab"}
181
+ if mode in wrapper:
182
+ self._mode = wrapper[mode]
183
+ else:
184
+ raise Exception()
185
+
186
+ def write(self, data):
187
+ with open(self._file, self._mode) as fh:
188
+ fh.write(data)
189
+
190
+ def close(self):
191
+ pass
192
+
193
+ def flush(self):
194
+ pass
@@ -76,10 +76,21 @@ class VrfOptions(_FamiliesMixin, BaseMeshModel):
76
76
  groups: Annotated[dict[str, MeshPeerGroup], DictMerge(Merge())]
77
77
 
78
78
 
79
+ class L2VpnOptions(BaseMeshModel):
80
+ name: str
81
+ vid: str | int # VLAN ID, possible values are 1 to 4094, ranges can be set as strings
82
+ l2vni: int # VNI, possible values are 1 to 2**24-1
83
+ route_distinguisher: str # like in VrfOptions
84
+ rt_import: Annotated[tuple[str, ...], Concat()] # like in VrfOptions
85
+ rt_export: Annotated[tuple[str, ...], Concat()] # like in VrfOptions
86
+ advertise_host_routes: bool # advertise IP+MAC routes into L3VNI
87
+
88
+
79
89
  class GlobalOptionsDTO(_FamiliesMixin, BaseMeshModel):
80
90
  def __init__(self, **kwargs):
81
91
  kwargs.setdefault("groups", KeyDefaultDict(lambda x: MeshPeerGroup(name=x)))
82
92
  kwargs.setdefault("vrf", KeyDefaultDict(lambda x: VrfOptions(vrf_name=x)))
93
+ kwargs.setdefault("l2vpn", KeyDefaultDict(lambda x: L2VpnOptions(name=x)))
83
94
  super().__init__(**kwargs)
84
95
 
85
96
  as_path_relax: bool
@@ -89,3 +100,4 @@ class GlobalOptionsDTO(_FamiliesMixin, BaseMeshModel):
89
100
  router_id: str
90
101
  vrf: Annotated[dict[str, VrfOptions], DictMerge(Merge())]
91
102
  groups: Annotated[dict[str, MeshPeerGroup], DictMerge(Merge())]
103
+ l2vpn: Annotated[dict[str, L2VpnOptions], DictMerge(Merge())]
@@ -7,7 +7,7 @@ from adaptix import Retort, loader, Chain, name_mapping, as_is_loader
7
7
  from .peer_models import DirectPeerDTO, IndirectPeerDTO, VirtualPeerDTO, VirtualLocalDTO
8
8
  from ..bgp_models import (
9
9
  Aggregate, GlobalOptions, VrfOptions, FamilyOptions, Peer, PeerGroup, ASN, PeerOptions,
10
- Redistribute, BFDTimers,
10
+ Redistribute, BFDTimers, L2VpnOptions, VidCollection,
11
11
  )
12
12
 
13
13
 
@@ -49,8 +49,10 @@ retort = Retort(
49
49
  recipe=[
50
50
  loader(InterfaceChanges, ObjMapping, Chain.FIRST),
51
51
  loader(ASN, ASN),
52
+ loader(VidCollection, VidCollection.parse),
52
53
  loader(GlobalOptions, ObjMapping, Chain.FIRST),
53
54
  loader(VrfOptions, ObjMapping, Chain.FIRST),
55
+ loader(L2VpnOptions, ObjMapping, Chain.FIRST),
54
56
  loader(FamilyOptions, ObjMapping, Chain.FIRST),
55
57
  loader(Aggregate, ObjMapping, Chain.FIRST),
56
58
  loader(PeerOptions, ObjMapping, Chain.FIRST),