annet 0.16.29__tar.gz → 0.16.31__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 (191) hide show
  1. {annet-0.16.29/annet.egg-info → annet-0.16.31}/PKG-INFO +8 -2
  2. {annet-0.16.29 → annet-0.16.31}/annet/adapters/file/provider.py +3 -0
  3. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/common/manufacturer.py +2 -0
  4. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/common/models.py +7 -1
  5. {annet-0.16.29 → annet-0.16.31}/annet/annlib/netdev/devdb/data/devdb.json +1 -0
  6. {annet-0.16.29 → annet-0.16.31}/annet/api/__init__.py +24 -15
  7. {annet-0.16.29 → annet-0.16.31}/annet/bgp_models.py +17 -0
  8. {annet-0.16.29 → annet-0.16.31}/annet/mesh/basemodel.py +3 -0
  9. {annet-0.16.29 → annet-0.16.31}/annet/mesh/executor.py +71 -10
  10. {annet-0.16.29 → annet-0.16.31}/annet/mesh/peer_models.py +4 -0
  11. {annet-0.16.29 → annet-0.16.31}/annet/mesh/registry.py +15 -0
  12. {annet-0.16.29 → annet-0.16.31}/annet/rpl_generators/community.py +1 -1
  13. {annet-0.16.29 → annet-0.16.31}/annet/rpl_generators/cumulus_frr.py +30 -16
  14. {annet-0.16.29 → annet-0.16.31}/annet/rpl_generators/entities.py +25 -12
  15. {annet-0.16.29 → annet-0.16.31}/annet/rpl_generators/policy.py +26 -15
  16. {annet-0.16.29 → annet-0.16.31}/annet/rpl_generators/prefix_lists.py +26 -16
  17. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/cisco.deploy +1 -1
  18. {annet-0.16.29 → annet-0.16.31}/annet/storage.py +4 -0
  19. {annet-0.16.29 → annet-0.16.31/annet.egg-info}/PKG-INFO +8 -2
  20. {annet-0.16.29 → annet-0.16.31}/AUTHORS +0 -0
  21. {annet-0.16.29 → annet-0.16.31}/LICENSE +0 -0
  22. {annet-0.16.29 → annet-0.16.31}/MANIFEST.in +0 -0
  23. {annet-0.16.29 → annet-0.16.31}/README.md +0 -0
  24. {annet-0.16.29 → annet-0.16.31}/annet/__init__.py +0 -0
  25. {annet-0.16.29 → annet-0.16.31}/annet/adapters/__init__.py +0 -0
  26. {annet-0.16.29 → annet-0.16.31}/annet/adapters/fetchers/__init__.py +0 -0
  27. {annet-0.16.29 → annet-0.16.31}/annet/adapters/fetchers/stub/__init__.py +0 -0
  28. {annet-0.16.29 → annet-0.16.31}/annet/adapters/fetchers/stub/fetcher.py +0 -0
  29. {annet-0.16.29 → annet-0.16.31}/annet/adapters/file/__init__.py +0 -0
  30. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/__init__.py +0 -0
  31. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/common/__init__.py +0 -0
  32. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/common/client.py +0 -0
  33. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/common/query.py +0 -0
  34. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/common/status_client.py +0 -0
  35. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/common/storage_opts.py +0 -0
  36. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/provider.py +0 -0
  37. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/v24/__init__.py +0 -0
  38. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/v24/storage.py +0 -0
  39. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/v37/__init__.py +0 -0
  40. {annet-0.16.29 → annet-0.16.31}/annet/adapters/netbox/v37/storage.py +0 -0
  41. {annet-0.16.29 → annet-0.16.31}/annet/annet.py +0 -0
  42. {annet-0.16.29 → annet-0.16.31}/annet/annlib/__init__.py +0 -0
  43. {annet-0.16.29 → annet-0.16.31}/annet/annlib/command.py +0 -0
  44. {annet-0.16.29 → annet-0.16.31}/annet/annlib/diff.py +0 -0
  45. {annet-0.16.29 → annet-0.16.31}/annet/annlib/errors.py +0 -0
  46. {annet-0.16.29 → annet-0.16.31}/annet/annlib/filter_acl.py +0 -0
  47. {annet-0.16.29 → annet-0.16.31}/annet/annlib/jsontools.py +0 -0
  48. {annet-0.16.29 → annet-0.16.31}/annet/annlib/lib.py +0 -0
  49. {annet-0.16.29 → annet-0.16.31}/annet/annlib/netdev/__init__.py +0 -0
  50. {annet-0.16.29 → annet-0.16.31}/annet/annlib/netdev/db.py +0 -0
  51. {annet-0.16.29 → annet-0.16.31}/annet/annlib/netdev/devdb/__init__.py +0 -0
  52. {annet-0.16.29 → annet-0.16.31}/annet/annlib/netdev/views/__init__.py +0 -0
  53. {annet-0.16.29 → annet-0.16.31}/annet/annlib/netdev/views/dump.py +0 -0
  54. {annet-0.16.29 → annet-0.16.31}/annet/annlib/netdev/views/hardware.py +0 -0
  55. {annet-0.16.29 → annet-0.16.31}/annet/annlib/output.py +0 -0
  56. {annet-0.16.29 → annet-0.16.31}/annet/annlib/patching.py +0 -0
  57. {annet-0.16.29 → annet-0.16.31}/annet/annlib/rbparser/__init__.py +0 -0
  58. {annet-0.16.29 → annet-0.16.31}/annet/annlib/rbparser/acl.py +0 -0
  59. {annet-0.16.29 → annet-0.16.31}/annet/annlib/rbparser/deploying.py +0 -0
  60. {annet-0.16.29 → annet-0.16.31}/annet/annlib/rbparser/ordering.py +0 -0
  61. {annet-0.16.29 → annet-0.16.31}/annet/annlib/rbparser/platform.py +0 -0
  62. {annet-0.16.29 → annet-0.16.31}/annet/annlib/rbparser/syntax.py +0 -0
  63. {annet-0.16.29 → annet-0.16.31}/annet/annlib/rulebook/__init__.py +0 -0
  64. {annet-0.16.29 → annet-0.16.31}/annet/annlib/rulebook/common.py +0 -0
  65. {annet-0.16.29 → annet-0.16.31}/annet/annlib/tabparser.py +0 -0
  66. {annet-0.16.29 → annet-0.16.31}/annet/annlib/types.py +0 -0
  67. {annet-0.16.29 → annet-0.16.31}/annet/argparse.py +0 -0
  68. {annet-0.16.29 → annet-0.16.31}/annet/cli.py +0 -0
  69. {annet-0.16.29 → annet-0.16.31}/annet/cli_args.py +0 -0
  70. {annet-0.16.29 → annet-0.16.31}/annet/configs/context.yml +0 -0
  71. {annet-0.16.29 → annet-0.16.31}/annet/configs/logging.yaml +0 -0
  72. {annet-0.16.29 → annet-0.16.31}/annet/connectors.py +0 -0
  73. {annet-0.16.29 → annet-0.16.31}/annet/deploy.py +0 -0
  74. {annet-0.16.29 → annet-0.16.31}/annet/diff.py +0 -0
  75. {annet-0.16.29 → annet-0.16.31}/annet/executor.py +0 -0
  76. {annet-0.16.29 → annet-0.16.31}/annet/filtering.py +0 -0
  77. {annet-0.16.29 → annet-0.16.31}/annet/gen.py +0 -0
  78. {annet-0.16.29 → annet-0.16.31}/annet/generators/__init__.py +0 -0
  79. {annet-0.16.29 → annet-0.16.31}/annet/generators/base.py +0 -0
  80. {annet-0.16.29 → annet-0.16.31}/annet/generators/common/__init__.py +0 -0
  81. {annet-0.16.29 → annet-0.16.31}/annet/generators/common/initial.py +0 -0
  82. {annet-0.16.29 → annet-0.16.31}/annet/generators/entire.py +0 -0
  83. {annet-0.16.29 → annet-0.16.31}/annet/generators/exceptions.py +0 -0
  84. {annet-0.16.29 → annet-0.16.31}/annet/generators/jsonfragment.py +0 -0
  85. {annet-0.16.29 → annet-0.16.31}/annet/generators/partial.py +0 -0
  86. {annet-0.16.29 → annet-0.16.31}/annet/generators/perf.py +0 -0
  87. {annet-0.16.29 → annet-0.16.31}/annet/generators/ref.py +0 -0
  88. {annet-0.16.29 → annet-0.16.31}/annet/generators/result.py +0 -0
  89. {annet-0.16.29 → annet-0.16.31}/annet/hardware.py +0 -0
  90. {annet-0.16.29 → annet-0.16.31}/annet/implicit.py +0 -0
  91. {annet-0.16.29 → annet-0.16.31}/annet/lib.py +0 -0
  92. {annet-0.16.29 → annet-0.16.31}/annet/mesh/__init__.py +0 -0
  93. {annet-0.16.29 → annet-0.16.31}/annet/mesh/device_models.py +0 -0
  94. {annet-0.16.29 → annet-0.16.31}/annet/mesh/match_args.py +0 -0
  95. {annet-0.16.29 → annet-0.16.31}/annet/mesh/models_converter.py +0 -0
  96. {annet-0.16.29 → annet-0.16.31}/annet/mesh/port_processor.py +0 -0
  97. {annet-0.16.29 → annet-0.16.31}/annet/output.py +0 -0
  98. {annet-0.16.29 → annet-0.16.31}/annet/parallel.py +0 -0
  99. {annet-0.16.29 → annet-0.16.31}/annet/patching.py +0 -0
  100. {annet-0.16.29 → annet-0.16.31}/annet/reference.py +0 -0
  101. {annet-0.16.29 → annet-0.16.31}/annet/rpl/__init__.py +0 -0
  102. {annet-0.16.29 → annet-0.16.31}/annet/rpl/action.py +0 -0
  103. {annet-0.16.29 → annet-0.16.31}/annet/rpl/condition.py +0 -0
  104. {annet-0.16.29 → annet-0.16.31}/annet/rpl/match_builder.py +0 -0
  105. {annet-0.16.29 → annet-0.16.31}/annet/rpl/policy.py +0 -0
  106. {annet-0.16.29 → annet-0.16.31}/annet/rpl/result.py +0 -0
  107. {annet-0.16.29 → annet-0.16.31}/annet/rpl/routemap.py +0 -0
  108. {annet-0.16.29 → annet-0.16.31}/annet/rpl/statement_builder.py +0 -0
  109. {annet-0.16.29 → annet-0.16.31}/annet/rpl_generators/__init__.py +0 -0
  110. {annet-0.16.29 → annet-0.16.31}/annet/rpl_generators/aspath.py +0 -0
  111. {annet-0.16.29 → annet-0.16.31}/annet/rpl_generators/execute.py +0 -0
  112. {annet-0.16.29 → annet-0.16.31}/annet/rpl_generators/rd.py +0 -0
  113. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/__init__.py +0 -0
  114. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/arista/__init__.py +0 -0
  115. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/arista/iface.py +0 -0
  116. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/aruba/__init__.py +0 -0
  117. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/aruba/ap_env.py +0 -0
  118. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/aruba/misc.py +0 -0
  119. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/b4com/__init__.py +0 -0
  120. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/b4com/file.py +0 -0
  121. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/cisco/__init__.py +0 -0
  122. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/cisco/iface.py +0 -0
  123. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/cisco/misc.py +0 -0
  124. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/cisco/vlandb.py +0 -0
  125. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/common.py +0 -0
  126. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/deploying.py +0 -0
  127. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/huawei/__init__.py +0 -0
  128. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/huawei/aaa.py +0 -0
  129. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/huawei/bgp.py +0 -0
  130. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/huawei/iface.py +0 -0
  131. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/huawei/misc.py +0 -0
  132. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/huawei/vlandb.py +0 -0
  133. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/juniper/__init__.py +0 -0
  134. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/nexus/__init__.py +0 -0
  135. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/nexus/iface.py +0 -0
  136. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/patching.py +0 -0
  137. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/ribbon/__init__.py +0 -0
  138. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/routeros/__init__.py +0 -0
  139. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/routeros/file.py +0 -0
  140. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/arista.deploy +0 -0
  141. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/arista.order +0 -0
  142. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/arista.rul +0 -0
  143. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/aruba.deploy +0 -0
  144. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/aruba.order +0 -0
  145. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/aruba.rul +0 -0
  146. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/b4com.deploy +0 -0
  147. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/b4com.order +0 -0
  148. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/b4com.rul +0 -0
  149. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/cisco.order +0 -0
  150. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/cisco.rul +0 -0
  151. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/huawei.deploy +0 -0
  152. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/huawei.order +0 -0
  153. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/huawei.rul +0 -0
  154. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/juniper.rul +0 -0
  155. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/nexus.deploy +0 -0
  156. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/nexus.order +0 -0
  157. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/nexus.rul +0 -0
  158. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/nokia.rul +0 -0
  159. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/optixtrans.deploy +0 -0
  160. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/optixtrans.order +0 -0
  161. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/optixtrans.rul +0 -0
  162. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/pc.deploy +0 -0
  163. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/pc.order +0 -0
  164. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/pc.rul +0 -0
  165. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/ribbon.deploy +0 -0
  166. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/ribbon.rul +0 -0
  167. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/routeros.order +0 -0
  168. {annet-0.16.29 → annet-0.16.31}/annet/rulebook/texts/routeros.rul +0 -0
  169. {annet-0.16.29 → annet-0.16.31}/annet/tabparser.py +0 -0
  170. {annet-0.16.29 → annet-0.16.31}/annet/text_term_format.py +0 -0
  171. {annet-0.16.29 → annet-0.16.31}/annet/tracing.py +0 -0
  172. {annet-0.16.29 → annet-0.16.31}/annet/types.py +0 -0
  173. {annet-0.16.29 → annet-0.16.31}/annet.egg-info/SOURCES.txt +0 -0
  174. {annet-0.16.29 → annet-0.16.31}/annet.egg-info/dependency_links.txt +0 -0
  175. {annet-0.16.29 → annet-0.16.31}/annet.egg-info/entry_points.txt +0 -0
  176. {annet-0.16.29 → annet-0.16.31}/annet.egg-info/requires.txt +0 -0
  177. {annet-0.16.29 → annet-0.16.31}/annet.egg-info/top_level.txt +0 -0
  178. {annet-0.16.29 → annet-0.16.31}/annet_generators/__init__.py +0 -0
  179. {annet-0.16.29 → annet-0.16.31}/annet_generators/example/__init__.py +0 -0
  180. {annet-0.16.29 → annet-0.16.31}/annet_generators/example/lldp.py +0 -0
  181. {annet-0.16.29 → annet-0.16.31}/annet_generators/mesh_example/__init__.py +0 -0
  182. {annet-0.16.29 → annet-0.16.31}/annet_generators/mesh_example/bgp.py +0 -0
  183. {annet-0.16.29 → annet-0.16.31}/annet_generators/mesh_example/mesh_logic.py +0 -0
  184. {annet-0.16.29 → annet-0.16.31}/annet_generators/rpl_example/__init__.py +0 -0
  185. {annet-0.16.29 → annet-0.16.31}/annet_generators/rpl_example/generator.py +0 -0
  186. {annet-0.16.29 → annet-0.16.31}/annet_generators/rpl_example/items.py +0 -0
  187. {annet-0.16.29 → annet-0.16.31}/annet_generators/rpl_example/mesh.py +0 -0
  188. {annet-0.16.29 → annet-0.16.31}/annet_generators/rpl_example/route_policy.py +0 -0
  189. {annet-0.16.29 → annet-0.16.31}/requirements.txt +0 -0
  190. {annet-0.16.29 → annet-0.16.31}/setup.cfg +0 -0
  191. {annet-0.16.29 → annet-0.16.31}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: annet
3
- Version: 0.16.29
3
+ Version: 0.16.31
4
4
  Summary: annet
5
5
  Home-page: https://github.com/annetutil/annet
6
6
  License: MIT
@@ -24,3 +24,9 @@ Requires-Dist: adaptix==3.0.0b7
24
24
  Requires-Dist: dataclass-rest==0.4
25
25
  Provides-Extra: netbox
26
26
  Requires-Dist: annetbox[sync]>=0.1.10; extra == "netbox"
27
+ Dynamic: home-page
28
+ Dynamic: license
29
+ Dynamic: provides-extra
30
+ Dynamic: requires-dist
31
+ Dynamic: requires-python
32
+ Dynamic: summary
@@ -102,6 +102,9 @@ class Device(DeviceCls, DumpableView):
102
102
  def add_subif(self, interface: str, subif: int) -> Interface:
103
103
  raise NotImplementedError
104
104
 
105
+ def find_interface(self, name: str) -> Optional[Interface]:
106
+ raise NotImplementedError
107
+
105
108
  def neighbours_fqdns(self) -> list[str]:
106
109
  return []
107
110
 
@@ -54,6 +54,8 @@ def get_breed(manufacturer: str, model: str):
54
54
  return "bcom-os"
55
55
  elif manufacturer == "MikroTik":
56
56
  return "routeros"
57
+ elif manufacturer == "Moxa":
58
+ return "moxa"
57
59
  elif manufacturer == "PC":
58
60
  return "pc"
59
61
  return ""
@@ -214,7 +214,7 @@ class NetboxDevice(Entity):
214
214
  return type(self) is type(other) and self.url == other.url
215
215
 
216
216
  def is_pc(self) -> bool:
217
- return self.device_type.manufacturer.name == "Mellanox" or self.breed == "pc"
217
+ return self.device_type.manufacturer.name in ("Mellanox", "Moxa") or self.breed == "pc"
218
218
 
219
219
  def _make_interface(self, name: str, type: InterfaceType) -> Interface:
220
220
  return Interface(
@@ -281,3 +281,9 @@ class NetboxDevice(Entity):
281
281
  )
282
282
  self.interfaces.append(target_port)
283
283
  return target_port
284
+
285
+ def find_interface(self, name: str) -> Optional[Interface]:
286
+ for iface in self.interfaces:
287
+ if iface.name == name:
288
+ return iface
289
+ return None
@@ -116,6 +116,7 @@
116
116
  "PC.Whitebox.Edgecore": "[Ee]dge-?[Cc]ore",
117
117
  "PC.Whitebox.Edgecore.AS": "AS",
118
118
  "PC.Whitebox.Edgecore.AS9736": "AS9736",
119
+ "PC.Whitebox.Edgecore.AS9817": "AS9817",
119
120
  "PC.Whitebox.NVIDIA": "NVIDIA",
120
121
  "PC.Whitebox.NVIDIA.SN": " SN",
121
122
  "PC.Whitebox.NVIDIA.SN.SN5400": " SN5400",
@@ -274,12 +274,14 @@ def patch(args: cli_args.ShowPatchOptions, loader: ann_gen.Loader):
274
274
 
275
275
  def _patch_worker(device_id, args: cli_args.ShowPatchOptions, stdin, loader: ann_gen.Loader, filterer: filtering.Filterer):
276
276
  for res, _, patch_tree in res_diff_patch(device_id, args, stdin, loader, filterer):
277
+ old_files = res.old_files
277
278
  new_files = res.get_new_files(args.acl_safe)
278
279
  new_json_fragment_files = res.get_new_file_fragments(args.acl_safe)
279
280
  if new_files:
280
281
  for path, (cfg_text, _cmds) in new_files.items():
281
282
  label = res.device.hostname + os.sep + path
282
- yield label, cfg_text, False
283
+ if old_files.get(path) != cfg_text:
284
+ yield label, cfg_text, False
283
285
  elif res.old_json_fragment_files or new_json_fragment_files:
284
286
  for path, (new_json_cfg, _cmds) in new_json_fragment_files.items():
285
287
  label = res.device.hostname + os.sep + path
@@ -468,6 +470,9 @@ class PCDeployerJob(DeployerJob):
468
470
  elif not new_files and not new_json_fragment_files:
469
471
  return
470
472
 
473
+ enable_reload = self.args.entire_reload is not cli_args.EntireReloadFlag.no
474
+ force_reload = self.args.entire_reload is cli_args.EntireReloadFlag.force
475
+
471
476
  upload_files: Dict[str, bytes] = {}
472
477
  reload_cmds: Dict[str, bytes] = {}
473
478
  generator_types: Dict[str, GeneratorType] = {}
@@ -483,18 +488,23 @@ class PCDeployerJob(DeployerJob):
483
488
  old_text = jsontools.format_json(old_json_cfg)
484
489
  new_text = jsontools.format_json(file_content_or_json_cfg)
485
490
  diff_content = "\n".join(_diff_file(old_text, new_text))
486
- if diff_content:
487
- self._has_diff = True
488
- upload_files[file], reload_cmds[file] = file_content.encode(), cmds.encode()
489
- generator_types[file] = generator_type
490
- self.cmd_lines.append("= Deploy cmds %s/%s " % (device.hostname, file))
491
- self.cmd_lines.extend([cmds, ""])
492
- self.cmd_lines.append("= %s/%s " % (device.hostname, file))
493
- self.cmd_lines.extend([file_content, ""])
494
- self.diff_lines.append("= %s/%s " % (device.hostname, file))
495
- self.diff_lines.extend([diff_content, ""])
496
-
497
- if upload_files:
491
+
492
+ if diff_content or force_reload:
493
+ self._has_diff |= True
494
+
495
+ upload_files[file] = file_content.encode()
496
+ generator_types[file] = generator_type
497
+ self.cmd_lines.append("= %s/%s " % (device.hostname, file))
498
+ self.cmd_lines.extend([file_content, ""])
499
+ self.diff_lines.append("= %s/%s " % (device.hostname, file))
500
+ self.diff_lines.extend([diff_content, ""])
501
+
502
+ if enable_reload:
503
+ reload_cmds[file] = cmds.encode()
504
+ self.cmd_lines.append("= Deploy cmds %s/%s " % (device.hostname, file))
505
+ self.cmd_lines.extend([cmds, ""])
506
+
507
+ if self._has_diff:
498
508
  self.deploy_cmds[device] = {
499
509
  "files": upload_files,
500
510
  "cmds": reload_cmds,
@@ -534,13 +544,12 @@ class Deployer:
534
544
  self._filterer = filtering.filterer_connector.get()
535
545
 
536
546
  def parse_result(self, job: DeployerJob, result: ann_gen.OldNewResult):
537
- entire_reload = self.args.entire_reload
538
547
  logger = get_logger(job.device.hostname)
539
548
 
540
549
  job.parse_result(result)
541
550
  self.failed_configs.update(job.failed_configs)
542
551
 
543
- if job.has_diff() or entire_reload is entire_reload.force:
552
+ if job.has_diff():
544
553
  self.cmd_lines.extend(job.cmd_lines)
545
554
  self.deploy_cmds.update(job.deploy_cmds)
546
555
  self.diffs.update(job.diffs)
@@ -286,13 +286,30 @@ def _used_policies(peer: Union[Peer, PeerGroup]) -> Iterable[str]:
286
286
  yield peer.export_policy
287
287
 
288
288
 
289
+ def _used_redistribute_policies(opts: Union[GlobalOptions, VrfOptions]) -> Iterable[str]:
290
+ for red in opts.ipv4_unicast.redistributes:
291
+ if red.policy:
292
+ yield red.policy
293
+ for red in opts.ipv6_unicast.redistributes:
294
+ if red.policy:
295
+ yield red.policy
296
+ for red in opts.ipv4_labeled_unicast.redistributes:
297
+ if red.policy:
298
+ yield red.policy
299
+ for red in opts.ipv6_labeled_unicast.redistributes:
300
+ if red.policy:
301
+ yield red.policy
302
+
303
+
289
304
  def extract_policies(config: BgpConfig) -> Sequence[str]:
290
305
  result: list[str] = []
291
306
  for vrf in config.global_options.vrf.values():
292
307
  for group in vrf.groups:
293
308
  result.extend(_used_policies(group))
309
+ result.extend(_used_redistribute_policies(vrf))
294
310
  for group in config.global_options.groups:
295
311
  result.extend(_used_policies(group))
296
312
  for peer in config.peers:
297
313
  result.extend(_used_policies(peer))
314
+ result.extend(_used_redistribute_policies(config.global_options))
298
315
  return result
@@ -133,6 +133,9 @@ class BaseMeshModel:
133
133
  raise AttributeError(f"{self.__class__.__name__} has no field {key}")
134
134
  super().__setattr__(key, value)
135
135
 
136
+ def is_empty(self):
137
+ return not self.__dict__
138
+
136
139
 
137
140
  ModelT = TypeVar("ModelT", bound=BaseMeshModel)
138
141
 
@@ -57,6 +57,11 @@ class MeshExecutor:
57
57
  rule_global_opts = MeshGlobalOptions(rule.match, device)
58
58
  logger.debug("Running device handler: %s", handler_name)
59
59
  rule.handler(rule_global_opts)
60
+
61
+ if rule_global_opts.is_empty():
62
+ # nothing was set
63
+ continue
64
+
60
65
  try:
61
66
  global_opts = merge(global_opts, rule_global_opts)
62
67
  except MergeForbiddenError as e:
@@ -79,7 +84,7 @@ class MeshExecutor:
79
84
  rule: MatchedDirectPair,
80
85
  ports: list[tuple[str, str]],
81
86
  all_connected_ports: list[tuple[str, str]],
82
- ) -> Pair:
87
+ ) -> Optional[Pair]:
83
88
  session = MeshSession()
84
89
  handler_name = self._handler_name(rule.handler)
85
90
  logger.debug("Running direct handler: %s", handler_name)
@@ -102,6 +107,10 @@ class MeshExecutor:
102
107
  else:
103
108
  rule.handler(peer_neighbor, peer_device, session)
104
109
 
110
+ if peer_neighbor.is_empty() and peer_device.is_empty() and session.is_empty():
111
+ # nothing was set
112
+ return None
113
+
105
114
  try:
106
115
  neighbor_dto = merge(DirectPeerDTO(), peer_neighbor, session)
107
116
  except MergeForbiddenError as e:
@@ -124,18 +133,29 @@ class MeshExecutor:
124
133
  # we merge them according to remote fqdn
125
134
  neighbor_peers: dict[PeerKey, Pair] = {}
126
135
  # TODO batch resolve
127
- for rule in self._registry.lookup_direct(device.fqdn, device.neighbours_fqdns):
136
+ rules = self._registry.lookup_direct(device.fqdn, device.neighbours_fqdns)
137
+ fqdns = {
138
+ rule.name_right if rule.direct_order else rule.name_left
139
+ for rule in rules
140
+ }
141
+ neigbors = {
142
+ d.fqdn: d for d in self._storage.make_devices(list(fqdns))
143
+ }
144
+ for rule in rules:
128
145
  handler_name = self._handler_name(rule.handler)
129
146
  if rule.direct_order:
130
- neighbor_device = self._storage.make_devices([rule.name_right])[0]
147
+ neighbor_device = neigbors[rule.name_right]
131
148
  else:
132
- neighbor_device = self._storage.make_devices([rule.name_left])[0]
149
+ neighbor_device = neigbors[rule.name_left]
133
150
  all_connected_ports = [
134
151
  (p1.name, p2.name)
135
152
  for p1, p2 in self._storage.search_connections(device, neighbor_device)
136
153
  ]
137
154
  for ports in rule.port_processor(all_connected_ports):
138
155
  pair = self._execute_direct_pair(device, neighbor_device, rule, ports, all_connected_ports)
156
+ if pair is None:
157
+ # nothing was set
158
+ continue
139
159
  addr = getattr(pair.connected, "addr", None)
140
160
  if addr is None:
141
161
  raise ValueError(f"Handler `{handler_name}` returned no peer addr")
@@ -171,6 +191,9 @@ class MeshExecutor:
171
191
  peer_virtual = VirtualPeer(num=order_number)
172
192
 
173
193
  rule.handler(peer_device, peer_virtual, session)
194
+ if peer_virtual.is_empty() and peer_device.is_empty() and session.is_empty():
195
+ # nothing was set
196
+ continue
174
197
 
175
198
  try:
176
199
  virtual_dto = merge(VirtualPeerDTO(), peer_virtual, session)
@@ -200,21 +223,34 @@ class MeshExecutor:
200
223
  # we can have multiple rules for the same pair
201
224
  # we merge them according to remote fqdn
202
225
  connected_peers: dict[PeerKey, Pair] = {}
203
- for rule in self._registry.lookup_indirect(device.fqdn, all_fqdns):
226
+
227
+ rules = self._registry.lookup_indirect(device.fqdn, all_fqdns)
228
+ fqdns = {
229
+ rule.name_right if rule.direct_order else rule.name_left
230
+ for rule in rules
231
+ }
232
+ connected_devices = {
233
+ d.fqdn: d for d in self._storage.make_devices(list(fqdns))
234
+ }
235
+ for rule in rules:
204
236
  session = MeshSession()
205
237
  handler_name = self._handler_name(rule.handler)
206
238
  logger.debug("Running indirect handler: %s", handler_name)
207
239
  if rule.direct_order:
208
- connected_device = self._storage.make_devices([rule.name_right])[0]
240
+ connected_device = connected_devices[rule.name_right]
209
241
  peer_device = IndirectPeer(rule.match_left, device)
210
242
  peer_connected = IndirectPeer(rule.match_right, connected_device)
211
243
  rule.handler(peer_device, peer_connected, session)
212
244
  else:
213
- connected_device = self._storage.make_devices([rule.name_left])[0]
245
+ connected_device = connected_devices[rule.name_left]
214
246
  peer_connected = IndirectPeer(rule.match_left, connected_device)
215
247
  peer_device = IndirectPeer(rule.match_right, device)
216
248
  rule.handler(peer_connected, peer_device, session)
217
249
 
250
+ if peer_connected.is_empty() and peer_device.is_empty() and session.is_empty():
251
+ # nothing was set
252
+ continue
253
+
218
254
  try:
219
255
  connected_dto = merge(IndirectPeerDTO(), peer_connected, session)
220
256
  except MergeForbiddenError as e:
@@ -265,7 +301,7 @@ class MeshExecutor:
265
301
  def _to_bgp_global(self, global_options: GlobalOptionsDTO) -> GlobalOptions:
266
302
  return to_bgp_global_options(global_options)
267
303
 
268
- def _apply_interface_changes(
304
+ def _apply_direct_interface_changes(
269
305
  self, device: Device, neighbor: Device, ports: list[str], changes: InterfaceChanges,
270
306
  ) -> str:
271
307
  # filter ports according to processed in pair
@@ -299,6 +335,24 @@ class MeshExecutor:
299
335
  target_interface.add_addr(changes.addr, changes.vrf)
300
336
  return target_interface.name
301
337
 
338
+ def _apply_indirect_interface_changes(
339
+ self, device: Device, neighbor: Device, ifname: Optional[str], changes: InterfaceChanges,
340
+ ) -> Optional[str]:
341
+ if changes.lag is not None:
342
+ raise ValueError("LAG creation unsupported for indirect peers")
343
+ elif changes.subif is not None:
344
+ target_interface = device.add_subif(ifname, changes.subif)
345
+ elif changes.svi is not None:
346
+ target_interface = device.add_svi(changes.svi)
347
+ elif not ifname:
348
+ return None
349
+ else:
350
+ target_interface = device.find_interface(ifname)
351
+ if not target_interface:
352
+ raise ValueError(f"Interface {ifname} not found for device {device.fqdn}")
353
+ target_interface.add_addr(changes.addr, changes.vrf)
354
+ return target_interface.name
355
+
302
356
  def _apply_virtual_interface_changes(self, device: Device, local: VirtualLocalDTO) -> str:
303
357
  return device.add_svi(local.svi).name # we check if SVI configured in execute method
304
358
 
@@ -308,8 +362,9 @@ class MeshExecutor:
308
362
  global_options = self._to_bgp_global(self._execute_globals(device))
309
363
 
310
364
  peers = []
365
+ target_interface: Optional[str]
311
366
  for direct_pair in self._execute_direct(device):
312
- target_interface = self._apply_interface_changes(
367
+ target_interface = self._apply_direct_interface_changes(
313
368
  device,
314
369
  direct_pair.device,
315
370
  direct_pair.ports,
@@ -325,7 +380,13 @@ class MeshExecutor:
325
380
  peers.append(self._virtual_to_bgp_peer(virtual_pair, target_interface))
326
381
 
327
382
  for connected_pair in self._execute_indirect(device, all_fqdns):
328
- peers.append(self._to_bgp_peer(connected_pair, None))
383
+ target_interface = self._apply_indirect_interface_changes(
384
+ device,
385
+ connected_pair.device,
386
+ getattr(connected_pair.local, "ifname", None),
387
+ to_interface_changes(connected_pair.local),
388
+ )
389
+ peers.append(self._to_bgp_peer(connected_pair, target_interface))
329
390
 
330
391
  return BgpConfig(
331
392
  global_options=global_options,
@@ -95,6 +95,10 @@ class IndirectPeerDTO(MeshSession, _OptionsDTO):
95
95
  description: str
96
96
  update_source: str
97
97
 
98
+ ifname: Optional[str]
99
+ subif: int
100
+ svi: Optional[int]
101
+
98
102
 
99
103
  class VirtualLocalDTO(_OptionsDTO):
100
104
  asnum: int
@@ -21,6 +21,9 @@ class DirectPeer(DirectPeerDTO):
21
21
  self.ports = ports
22
22
  self.all_connected_ports = all_connected_ports
23
23
 
24
+ def is_empty(self):
25
+ return self.__dict__.keys() == {"match", "device", "ports", "all_connected_ports"}
26
+
24
27
 
25
28
  class IndirectPeer(IndirectPeerDTO):
26
29
  match: MatchedArgs
@@ -31,6 +34,9 @@ class IndirectPeer(IndirectPeerDTO):
31
34
  self.match = match
32
35
  self.device = device
33
36
 
37
+ def is_empty(self):
38
+ return self.__dict__.keys() == {"match", "device"}
39
+
34
40
 
35
41
  class VirtualLocal(VirtualLocalDTO):
36
42
  match: MatchedArgs
@@ -41,10 +47,16 @@ class VirtualLocal(VirtualLocalDTO):
41
47
  self.match = match
42
48
  self.device = device
43
49
 
50
+ def is_empty(self):
51
+ return self.__dict__.keys() == {"match", "device"}
52
+
44
53
 
45
54
  class VirtualPeer(VirtualPeerDTO):
46
55
  num: int
47
56
 
57
+ def is_empty(self):
58
+ return self.__dict__.keys() == {"num"}
59
+
48
60
 
49
61
  class GlobalOptions(GlobalOptionsDTO):
50
62
  match: MatchedArgs
@@ -55,6 +67,9 @@ class GlobalOptions(GlobalOptionsDTO):
55
67
  self.match = match
56
68
  self.device = device
57
69
 
70
+ def is_empty(self):
71
+ return self.__dict__.keys() == {"match", "device"}
72
+
58
73
 
59
74
  GlobalHandler = Callable[[GlobalOptions], None]
60
75
 
@@ -164,7 +164,7 @@ class CommunityListGenerator(PartialGenerator, ABC):
164
164
  yield self._huawei_community_filter(10, community_list, " ".join(members))
165
165
  elif community_list.logic == CommunityLogic.OR:
166
166
  for i, member in enumerate(members):
167
- member_id = (i + 1) * 10 + 5
167
+ member_id = (i + 1) * 10
168
168
  yield self._huawei_community_filter(member_id, community_list, member)
169
169
  else:
170
170
  raise NotImplementedError(f"Community logic {community_list.logic} is not implemented for huawei")
@@ -11,10 +11,10 @@ from annet.rpl.statement_builder import NextHopActionValue, AsPathActionValue, C
11
11
  from .aspath import get_used_as_path_filters
12
12
  from .community import get_used_united_community_lists
13
13
  from .entities import (
14
- AsPathFilter, IpPrefixList, mangle_ranged_prefix_list_name, CommunityList, CommunityLogic, CommunityType,
15
- mangle_united_community_list_name,
14
+ AsPathFilter, IpPrefixList, CommunityList, CommunityLogic, CommunityType,
15
+ mangle_united_community_list_name, PrefixListNameGenerator,
16
16
  )
17
- from .prefix_lists import get_used_prefix_lists
17
+ from .prefix_lists import get_used_prefix_lists, new_prefix_list_name_generator
18
18
 
19
19
  FRR_RESULT_MAP = {
20
20
  ResultType.ALLOW: "permit",
@@ -51,6 +51,12 @@ class CumulusPolicyGenerator(ABC):
51
51
  def get_prefix_lists(self, device: Any) -> Sequence[IpPrefixList]:
52
52
  raise NotImplementedError()
53
53
 
54
+ def get_used_prefix_lists(self, device: Any, name_generator: PrefixListNameGenerator) -> Sequence[IpPrefixList]:
55
+ return get_used_prefix_lists(
56
+ prefix_lists=self.get_prefix_lists(device),
57
+ name_generator=name_generator,
58
+ )
59
+
54
60
  @abstractmethod
55
61
  def get_community_lists(self, device: Any) -> list[CommunityList]:
56
62
  raise NotImplementedError()
@@ -61,11 +67,13 @@ class CumulusPolicyGenerator(ABC):
61
67
 
62
68
  def generate_cumulus_rpl(self, device: Any) -> Iterator[Sequence[str]]:
63
69
  policies = self.get_policies(device)
70
+ prefix_list_name_generator = new_prefix_list_name_generator(policies)
71
+
64
72
  communities = {c.name: c for c in self.get_community_lists(device)}
65
73
  yield from self._cumulus_as_path_filters(device, policies)
66
74
  yield from self._cumulus_communities(device, communities, policies)
67
- yield from self._cumulus_prefix_lists(device, policies)
68
- yield from self._cumulus_policy_config(device, communities, policies)
75
+ yield from self._cumulus_prefix_lists(device, policies, prefix_list_name_generator)
76
+ yield from self._cumulus_policy_config(device, communities, policies, prefix_list_name_generator)
69
77
 
70
78
  def _cumulus_as_path_filters(
71
79
  self,
@@ -100,11 +108,12 @@ class CumulusPolicyGenerator(ABC):
100
108
  ("le", str(match.less_equal)) if match.less_equal is not None else ()
101
109
  )
102
110
 
103
- def _cumulus_prefix_lists(self, device: Any, policies: list[RoutingPolicy]) -> Iterable[Sequence[str]]:
104
- plists = {p.name: p for p in get_used_prefix_lists(
105
- prefix_lists=self.get_prefix_lists(device),
106
- policies=policies,
107
- )}
111
+ def _cumulus_prefix_lists(
112
+ self, device: Any,
113
+ policies: list[RoutingPolicy],
114
+ prefix_list_name_generator: PrefixListNameGenerator,
115
+ ) -> Iterable[Sequence[str]]:
116
+ plists = {p.name: p for p in self.get_used_prefix_lists(device, prefix_list_name_generator)}
108
117
  if not plists.values():
109
118
  return
110
119
 
@@ -114,7 +123,7 @@ class CumulusPolicyGenerator(ABC):
114
123
  cond: SingleCondition[PrefixMatchValue]
115
124
  for cond in statement.match.find_all(MatchField.ip_prefix):
116
125
  for name in cond.value.names:
117
- mangled_name = mangle_ranged_prefix_list_name(
126
+ mangled_name = prefix_list_name_generator.get_prefix_name(
118
127
  name=name,
119
128
  greater_equal=cond.value.greater_equal,
120
129
  less_equal=cond.value.less_equal,
@@ -125,7 +134,7 @@ class CumulusPolicyGenerator(ABC):
125
134
  precessed_names.add(mangled_name)
126
135
  for cond in statement.match.find_all(MatchField.ipv6_prefix):
127
136
  for name in cond.value.names:
128
- mangled_name = mangle_ranged_prefix_list_name(
137
+ mangled_name = prefix_list_name_generator.get_prefix_name(
129
138
  name=name,
130
139
  greater_equal=cond.value.greater_equal,
131
140
  less_equal=cond.value.less_equal,
@@ -217,6 +226,7 @@ class CumulusPolicyGenerator(ABC):
217
226
  self,
218
227
  device: Any,
219
228
  condition: SingleCondition[Any],
229
+ prefix_list_name_generator: PrefixListNameGenerator,
220
230
  ) -> Iterator[Sequence[str]]:
221
231
  if condition.field == MatchField.community:
222
232
  for comm_name in self._get_match_community_names(condition):
@@ -236,7 +246,7 @@ class CumulusPolicyGenerator(ABC):
236
246
  return
237
247
  if condition.field == MatchField.ip_prefix:
238
248
  for name in condition.value.names:
239
- mangled_name = mangle_ranged_prefix_list_name(
249
+ mangled_name = prefix_list_name_generator.get_prefix_name(
240
250
  name=name,
241
251
  greater_equal=condition.value.greater_equal,
242
252
  less_equal=condition.value.less_equal,
@@ -245,7 +255,7 @@ class CumulusPolicyGenerator(ABC):
245
255
  return
246
256
  if condition.field == MatchField.ipv6_prefix:
247
257
  for name in condition.value.names:
248
- mangled_name = mangle_ranged_prefix_list_name(
258
+ mangled_name = prefix_list_name_generator.get_prefix_name(
249
259
  name=name,
250
260
  greater_equal=condition.value.greater_equal,
251
261
  less_equal=condition.value.less_equal,
@@ -421,11 +431,12 @@ class CumulusPolicyGenerator(ABC):
421
431
  device: Any,
422
432
  policy: RoutingPolicy,
423
433
  statement: RoutingPolicyStatement,
434
+ prefix_list_name_generator: PrefixListNameGenerator,
424
435
  ) -> Iterable[Sequence[str]]:
425
436
  yield "route-map", policy.name, FRR_RESULT_MAP[statement.result], str(statement.number)
426
437
 
427
438
  for condition in statement.match:
428
- for row in self._cumulus_policy_match(device, condition):
439
+ for row in self._cumulus_policy_match(device, condition, prefix_list_name_generator):
429
440
  yield FRR_INDENT, *row
430
441
  for action in statement.then:
431
442
  for row in self._cumulus_policy_then(communities, device, action):
@@ -439,6 +450,7 @@ class CumulusPolicyGenerator(ABC):
439
450
  device: Any,
440
451
  communities: dict[str, CommunityList],
441
452
  policies: list[RoutingPolicy],
453
+ prefix_list_name_generator: PrefixListNameGenerator,
442
454
  ) -> Iterable[Sequence[str]]:
443
455
  """ Route maps configuration """
444
456
 
@@ -454,5 +466,7 @@ class CumulusPolicyGenerator(ABC):
454
466
  raise RuntimeError(
455
467
  f"Multiple statements have same number {statement.number} for policy `{policy.name}`: "
456
468
  f"`{statement.name}` and `{applied_stmts[statement.number]}`")
457
- yield from self._cumulus_policy_statement(communities, device, policy, statement)
469
+ yield from self._cumulus_policy_statement(
470
+ communities, device, policy, statement, prefix_list_name_generator,
471
+ )
458
472
  applied_stmts[statement.number] = statement.name
@@ -1,3 +1,4 @@
1
+ from collections import defaultdict
1
2
  from collections.abc import Sequence
2
3
  from dataclasses import dataclass
3
4
  from enum import Enum
@@ -56,15 +57,27 @@ def mangle_united_community_list_name(values: Sequence[str]) -> str:
56
57
  return "_OR_".join(values)
57
58
 
58
59
 
59
- def mangle_ranged_prefix_list_name(name: str, greater_equal: Optional[int], less_equal: Optional[int]) -> str:
60
- if greater_equal is less_equal is None:
61
- return name
62
- if greater_equal is None:
63
- ge_str = "unset"
64
- else:
65
- ge_str = str(greater_equal)
66
- if less_equal is None:
67
- le_str = "unset"
68
- else:
69
- le_str = str(less_equal)
70
- return f"{name}_{ge_str}_{le_str}"
60
+ class PrefixListNameGenerator:
61
+ def __init__(self):
62
+ self._prefix_lists = defaultdict(set)
63
+
64
+ def add_prefix(self, name: str, greater_equal: Optional[int], less_equal: Optional[int]) -> None:
65
+ self._prefix_lists[name].add((greater_equal, less_equal))
66
+
67
+ def is_used(self, name: str):
68
+ return name in self._prefix_lists
69
+
70
+ def get_prefix_name(self, name: str, greater_equal: Optional[int], less_equal: Optional[int]) -> str:
71
+ if len(self._prefix_lists[name]) == 1:
72
+ return name
73
+ if greater_equal is less_equal is None:
74
+ return name
75
+ if greater_equal is None:
76
+ ge_str = "unset"
77
+ else:
78
+ ge_str = str(greater_equal)
79
+ if less_equal is None:
80
+ le_str = "unset"
81
+ else:
82
+ le_str = str(less_equal)
83
+ return f"{name}_{ge_str}_{le_str}"