annet 0.16.13__tar.gz → 0.16.14__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 (168) hide show
  1. {annet-0.16.13/annet.egg-info → annet-0.16.14}/PKG-INFO +1 -1
  2. {annet-0.16.13 → annet-0.16.14}/README.md +1 -1
  3. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/common/storage_opts.py +7 -2
  4. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/provider.py +3 -1
  5. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/v37/storage.py +26 -19
  6. {annet-0.16.13 → annet-0.16.14}/annet/bgp_models.py +14 -11
  7. {annet-0.16.13 → annet-0.16.14}/annet/implicit.py +4 -1
  8. {annet-0.16.13 → annet-0.16.14}/annet/mesh/__init__.py +5 -2
  9. {annet-0.16.13 → annet-0.16.14}/annet/mesh/device_models.py +34 -10
  10. {annet-0.16.13 → annet-0.16.14}/annet/mesh/executor.py +72 -11
  11. {annet-0.16.13 → annet-0.16.14}/annet/mesh/models_converter.py +10 -6
  12. {annet-0.16.13 → annet-0.16.14}/annet/mesh/peer_models.py +27 -3
  13. {annet-0.16.13 → annet-0.16.14}/annet/mesh/registry.py +61 -4
  14. {annet-0.16.13 → annet-0.16.14/annet.egg-info}/PKG-INFO +1 -1
  15. {annet-0.16.13 → annet-0.16.14}/annet_generators/mesh_example/bgp.py +1 -0
  16. {annet-0.16.13 → annet-0.16.14}/annet_generators/mesh_example/mesh_logic.py +9 -1
  17. {annet-0.16.13 → annet-0.16.14}/AUTHORS +0 -0
  18. {annet-0.16.13 → annet-0.16.14}/LICENSE +0 -0
  19. {annet-0.16.13 → annet-0.16.14}/MANIFEST.in +0 -0
  20. {annet-0.16.13 → annet-0.16.14}/annet/__init__.py +0 -0
  21. {annet-0.16.13 → annet-0.16.14}/annet/adapters/__init__.py +0 -0
  22. {annet-0.16.13 → annet-0.16.14}/annet/adapters/fetchers/__init__.py +0 -0
  23. {annet-0.16.13 → annet-0.16.14}/annet/adapters/fetchers/stub/__init__.py +0 -0
  24. {annet-0.16.13 → annet-0.16.14}/annet/adapters/fetchers/stub/fetcher.py +0 -0
  25. {annet-0.16.13 → annet-0.16.14}/annet/adapters/file/__init__.py +0 -0
  26. {annet-0.16.13 → annet-0.16.14}/annet/adapters/file/provider.py +0 -0
  27. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/__init__.py +0 -0
  28. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/common/__init__.py +0 -0
  29. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/common/client.py +0 -0
  30. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/common/manufacturer.py +0 -0
  31. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/common/models.py +0 -0
  32. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/common/query.py +0 -0
  33. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/common/status_client.py +0 -0
  34. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/v24/__init__.py +0 -0
  35. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/v24/storage.py +0 -0
  36. {annet-0.16.13 → annet-0.16.14}/annet/adapters/netbox/v37/__init__.py +0 -0
  37. {annet-0.16.13 → annet-0.16.14}/annet/annet.py +0 -0
  38. {annet-0.16.13 → annet-0.16.14}/annet/annlib/__init__.py +0 -0
  39. {annet-0.16.13 → annet-0.16.14}/annet/annlib/command.py +0 -0
  40. {annet-0.16.13 → annet-0.16.14}/annet/annlib/diff.py +0 -0
  41. {annet-0.16.13 → annet-0.16.14}/annet/annlib/errors.py +0 -0
  42. {annet-0.16.13 → annet-0.16.14}/annet/annlib/filter_acl.py +0 -0
  43. {annet-0.16.13 → annet-0.16.14}/annet/annlib/jsontools.py +0 -0
  44. {annet-0.16.13 → annet-0.16.14}/annet/annlib/lib.py +0 -0
  45. {annet-0.16.13 → annet-0.16.14}/annet/annlib/netdev/__init__.py +0 -0
  46. {annet-0.16.13 → annet-0.16.14}/annet/annlib/netdev/db.py +0 -0
  47. {annet-0.16.13 → annet-0.16.14}/annet/annlib/netdev/devdb/__init__.py +0 -0
  48. {annet-0.16.13 → annet-0.16.14}/annet/annlib/netdev/devdb/data/devdb.json +0 -0
  49. {annet-0.16.13 → annet-0.16.14}/annet/annlib/netdev/views/__init__.py +0 -0
  50. {annet-0.16.13 → annet-0.16.14}/annet/annlib/netdev/views/dump.py +0 -0
  51. {annet-0.16.13 → annet-0.16.14}/annet/annlib/netdev/views/hardware.py +0 -0
  52. {annet-0.16.13 → annet-0.16.14}/annet/annlib/output.py +0 -0
  53. {annet-0.16.13 → annet-0.16.14}/annet/annlib/patching.py +0 -0
  54. {annet-0.16.13 → annet-0.16.14}/annet/annlib/rbparser/__init__.py +0 -0
  55. {annet-0.16.13 → annet-0.16.14}/annet/annlib/rbparser/acl.py +0 -0
  56. {annet-0.16.13 → annet-0.16.14}/annet/annlib/rbparser/deploying.py +0 -0
  57. {annet-0.16.13 → annet-0.16.14}/annet/annlib/rbparser/ordering.py +0 -0
  58. {annet-0.16.13 → annet-0.16.14}/annet/annlib/rbparser/platform.py +0 -0
  59. {annet-0.16.13 → annet-0.16.14}/annet/annlib/rbparser/syntax.py +0 -0
  60. {annet-0.16.13 → annet-0.16.14}/annet/annlib/rulebook/__init__.py +0 -0
  61. {annet-0.16.13 → annet-0.16.14}/annet/annlib/rulebook/common.py +0 -0
  62. {annet-0.16.13 → annet-0.16.14}/annet/annlib/tabparser.py +0 -0
  63. {annet-0.16.13 → annet-0.16.14}/annet/annlib/types.py +0 -0
  64. {annet-0.16.13 → annet-0.16.14}/annet/api/__init__.py +0 -0
  65. {annet-0.16.13 → annet-0.16.14}/annet/argparse.py +0 -0
  66. {annet-0.16.13 → annet-0.16.14}/annet/cli.py +0 -0
  67. {annet-0.16.13 → annet-0.16.14}/annet/cli_args.py +0 -0
  68. {annet-0.16.13 → annet-0.16.14}/annet/configs/context.yml +0 -0
  69. {annet-0.16.13 → annet-0.16.14}/annet/configs/logging.yaml +0 -0
  70. {annet-0.16.13 → annet-0.16.14}/annet/connectors.py +0 -0
  71. {annet-0.16.13 → annet-0.16.14}/annet/deploy.py +0 -0
  72. {annet-0.16.13 → annet-0.16.14}/annet/diff.py +0 -0
  73. {annet-0.16.13 → annet-0.16.14}/annet/executor.py +0 -0
  74. {annet-0.16.13 → annet-0.16.14}/annet/filtering.py +0 -0
  75. {annet-0.16.13 → annet-0.16.14}/annet/gen.py +0 -0
  76. {annet-0.16.13 → annet-0.16.14}/annet/generators/__init__.py +0 -0
  77. {annet-0.16.13 → annet-0.16.14}/annet/generators/base.py +0 -0
  78. {annet-0.16.13 → annet-0.16.14}/annet/generators/common/__init__.py +0 -0
  79. {annet-0.16.13 → annet-0.16.14}/annet/generators/common/initial.py +0 -0
  80. {annet-0.16.13 → annet-0.16.14}/annet/generators/entire.py +0 -0
  81. {annet-0.16.13 → annet-0.16.14}/annet/generators/exceptions.py +0 -0
  82. {annet-0.16.13 → annet-0.16.14}/annet/generators/jsonfragment.py +0 -0
  83. {annet-0.16.13 → annet-0.16.14}/annet/generators/partial.py +0 -0
  84. {annet-0.16.13 → annet-0.16.14}/annet/generators/perf.py +0 -0
  85. {annet-0.16.13 → annet-0.16.14}/annet/generators/ref.py +0 -0
  86. {annet-0.16.13 → annet-0.16.14}/annet/generators/result.py +0 -0
  87. {annet-0.16.13 → annet-0.16.14}/annet/hardware.py +0 -0
  88. {annet-0.16.13 → annet-0.16.14}/annet/lib.py +0 -0
  89. {annet-0.16.13 → annet-0.16.14}/annet/mesh/basemodel.py +0 -0
  90. {annet-0.16.13 → annet-0.16.14}/annet/mesh/match_args.py +0 -0
  91. {annet-0.16.13 → annet-0.16.14}/annet/output.py +0 -0
  92. {annet-0.16.13 → annet-0.16.14}/annet/parallel.py +0 -0
  93. {annet-0.16.13 → annet-0.16.14}/annet/patching.py +0 -0
  94. {annet-0.16.13 → annet-0.16.14}/annet/reference.py +0 -0
  95. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/__init__.py +0 -0
  96. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/arista/__init__.py +0 -0
  97. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/arista/iface.py +0 -0
  98. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/aruba/__init__.py +0 -0
  99. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/aruba/ap_env.py +0 -0
  100. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/aruba/misc.py +0 -0
  101. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/b4com/__init__.py +0 -0
  102. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/b4com/file.py +0 -0
  103. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/cisco/__init__.py +0 -0
  104. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/cisco/iface.py +0 -0
  105. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/cisco/misc.py +0 -0
  106. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/cisco/vlandb.py +0 -0
  107. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/common.py +0 -0
  108. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/deploying.py +0 -0
  109. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/huawei/__init__.py +0 -0
  110. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/huawei/aaa.py +0 -0
  111. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/huawei/bgp.py +0 -0
  112. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/huawei/iface.py +0 -0
  113. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/huawei/misc.py +0 -0
  114. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/huawei/vlandb.py +0 -0
  115. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/juniper/__init__.py +0 -0
  116. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/nexus/__init__.py +0 -0
  117. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/nexus/iface.py +0 -0
  118. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/patching.py +0 -0
  119. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/ribbon/__init__.py +0 -0
  120. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/routeros/__init__.py +0 -0
  121. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/routeros/file.py +0 -0
  122. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/arista.deploy +0 -0
  123. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/arista.order +0 -0
  124. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/arista.rul +0 -0
  125. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/aruba.deploy +0 -0
  126. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/aruba.order +0 -0
  127. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/aruba.rul +0 -0
  128. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/b4com.deploy +0 -0
  129. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/b4com.order +0 -0
  130. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/b4com.rul +0 -0
  131. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/cisco.deploy +0 -0
  132. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/cisco.order +0 -0
  133. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/cisco.rul +0 -0
  134. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/huawei.deploy +0 -0
  135. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/huawei.order +0 -0
  136. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/huawei.rul +0 -0
  137. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/juniper.rul +0 -0
  138. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/nexus.deploy +0 -0
  139. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/nexus.order +0 -0
  140. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/nexus.rul +0 -0
  141. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/nokia.rul +0 -0
  142. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/optixtrans.deploy +0 -0
  143. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/optixtrans.order +0 -0
  144. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/optixtrans.rul +0 -0
  145. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/pc.deploy +0 -0
  146. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/pc.order +0 -0
  147. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/pc.rul +0 -0
  148. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/ribbon.deploy +0 -0
  149. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/ribbon.rul +0 -0
  150. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/routeros.order +0 -0
  151. {annet-0.16.13 → annet-0.16.14}/annet/rulebook/texts/routeros.rul +0 -0
  152. {annet-0.16.13 → annet-0.16.14}/annet/storage.py +0 -0
  153. {annet-0.16.13 → annet-0.16.14}/annet/tabparser.py +0 -0
  154. {annet-0.16.13 → annet-0.16.14}/annet/text_term_format.py +0 -0
  155. {annet-0.16.13 → annet-0.16.14}/annet/tracing.py +0 -0
  156. {annet-0.16.13 → annet-0.16.14}/annet/types.py +0 -0
  157. {annet-0.16.13 → annet-0.16.14}/annet.egg-info/SOURCES.txt +0 -0
  158. {annet-0.16.13 → annet-0.16.14}/annet.egg-info/dependency_links.txt +0 -0
  159. {annet-0.16.13 → annet-0.16.14}/annet.egg-info/entry_points.txt +0 -0
  160. {annet-0.16.13 → annet-0.16.14}/annet.egg-info/requires.txt +0 -0
  161. {annet-0.16.13 → annet-0.16.14}/annet.egg-info/top_level.txt +0 -0
  162. {annet-0.16.13 → annet-0.16.14}/annet_generators/__init__.py +0 -0
  163. {annet-0.16.13 → annet-0.16.14}/annet_generators/example/__init__.py +0 -0
  164. {annet-0.16.13 → annet-0.16.14}/annet_generators/example/lldp.py +0 -0
  165. {annet-0.16.13 → annet-0.16.14}/annet_generators/mesh_example/__init__.py +0 -0
  166. {annet-0.16.13 → annet-0.16.14}/requirements.txt +0 -0
  167. {annet-0.16.13 → annet-0.16.14}/setup.cfg +0 -0
  168. {annet-0.16.13 → annet-0.16.14}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: annet
3
- Version: 0.16.13
3
+ Version: 0.16.14
4
4
  Summary: annet
5
5
  Home-page: https://github.com/annetutil/annet
6
6
  License: MIT
@@ -59,4 +59,4 @@ pip install -r requirements-doc.txt
59
59
  sphinx-build -M html docs docs-build
60
60
  ```
61
61
 
62
- 3. Open rendered html in browser [docs-build/html/index.html](docs-build/html/index.html)
62
+ 3. Open rendered html in browser docs-build/html/index.html
@@ -5,10 +5,11 @@ DEFAULT_URL = "http://localhost"
5
5
 
6
6
 
7
7
  class NetboxStorageOpts:
8
- def __init__(self, url: str, token: str, insecure: bool = False):
8
+ def __init__(self, url: str, token: str, insecure: bool = False, exact_host_filter: bool = False):
9
9
  self.url = url
10
10
  self.token = token
11
11
  self.insecure = insecure
12
+ self.exact_host_filter = exact_host_filter
12
13
 
13
14
  @classmethod
14
15
  def parse_params(cls, conf_params: Optional[dict[str, str]], cli_opts: Any):
@@ -19,4 +20,8 @@ class NetboxStorageOpts:
19
20
  insecure = insecure_env in ("true", "1", "t")
20
21
  else:
21
22
  insecure = bool(conf_params.get("insecure") or False)
22
- return cls(url=url, token=token, insecure=insecure)
23
+ if exact_host_filter_env := os.getenv("NETBOX_EXACT_HOST_FILTER", "").lower():
24
+ exact_host_filter = exact_host_filter_env in ("true", "1", "t")
25
+ else:
26
+ exact_host_filter = bool(conf_params.get("exact_host_filter") or False)
27
+ return cls(url=url, token=token, insecure=insecure, exact_host_filter=exact_host_filter)
@@ -27,10 +27,12 @@ def storage_factory(opts: NetboxStorageOpts) -> Storage:
27
27
 
28
28
 
29
29
  class NetboxProvider(StorageProvider, AdapterWithName, AdapterWithConfig):
30
- def __init__(self, url: Optional[str] = None, token: Optional[str] = None, insecure: bool = False):
30
+ def __init__(self, url: Optional[str] = None, token: Optional[str] = None, insecure: bool = False,
31
+ exact_host_filter: bool = False):
31
32
  self.url = url
32
33
  self.token = token
33
34
  self.insecure = insecure
35
+ self.exact_host_filter = exact_host_filter
34
36
 
35
37
  @classmethod
36
38
  def with_config(cls, **kwargs: Dict[str, Any]) -> T:
@@ -87,16 +87,19 @@ def extend_ip_address(
87
87
 
88
88
  class NetboxStorageV37(Storage):
89
89
  def __init__(self, opts: Optional[NetboxStorageOpts] = None):
90
- ctx: ssl.con | ssl.SSLContext = None
91
- if opts.insecure:
92
- ctx = ssl.create_default_context()
93
- ctx.check_hostname = False
94
- ctx.verify_mode = ssl.CERT_NONE
95
- self.netbox = NetboxV37(
96
- url=opts.url,
97
- token=opts.token,
98
- ssl_context=ctx,
99
- )
90
+ ctx: Optional[ssl.SSLContext] = None
91
+ url = ""
92
+ token = ""
93
+ self.exact_host_filter = False
94
+ if opts:
95
+ if opts.insecure:
96
+ ctx = ssl.create_default_context()
97
+ ctx.check_hostname = False
98
+ ctx.verify_mode = ssl.CERT_NONE
99
+ url = opts.url
100
+ token = opts.token
101
+ self.exact_host_filter = opts.exact_host_filter
102
+ self.netbox = NetboxV37(url=url, token=token, ssl_context=ctx)
100
103
  self._all_fqdns: Optional[list[str]] = None
101
104
 
102
105
  def __enter__(self):
@@ -147,7 +150,7 @@ class NetboxStorageV37(Storage):
147
150
 
148
151
  interfaces = self._load_interfaces(list(device_ids))
149
152
  neighbours = {x.id: x for x in self._load_neighbours(interfaces)}
150
- neighbours_seen = defaultdict(set)
153
+ neighbours_seen: dict[str, set] = defaultdict(set)
151
154
 
152
155
  for interface in interfaces:
153
156
  device_ids[interface.device.id].interfaces.append(interface)
@@ -162,14 +165,18 @@ class NetboxStorageV37(Storage):
162
165
  def _load_devices(self, query: NetboxQuery) -> List[api_models.Device]:
163
166
  if not query.globs:
164
167
  return []
165
- query = _hostname_dot_hack(query)
166
- return [
167
- device
168
- for device in self.netbox.dcim_all_devices(
169
- name__ic=query.globs,
170
- ).results
171
- if _match_query(query, device)
172
- ]
168
+ if self.exact_host_filter:
169
+ devices = self.netbox.dcim_all_devices(name__ie=query.globs).results
170
+ else:
171
+ query = _hostname_dot_hack(query)
172
+ devices = [
173
+ device
174
+ for device in self.netbox.dcim_all_devices(
175
+ name__ic=query.globs,
176
+ ).results
177
+ if _match_query(query, device)
178
+ ]
179
+ return devices
173
180
 
174
181
  def _extend_interfaces(self, interfaces: List[models.Interface]) -> List[models.Interface]:
175
182
  extended_ifaces = {
@@ -103,7 +103,7 @@ class PeerOptions:
103
103
  af_rib_group: Optional[str] = None
104
104
  af_loops: Optional[int] = None
105
105
  hold_time: Optional[int] = None
106
- listen_network: Optional[bool] = None
106
+ listen_network: Optional[list[str]] = None
107
107
  remove_private: Optional[bool] = None
108
108
  as_override: Optional[bool] = None
109
109
  aigp: Optional[bool] = None
@@ -158,7 +158,7 @@ class Redistribute:
158
158
  @dataclass
159
159
  class FamilyOptions:
160
160
  family: Family
161
- vrf_name: str
161
+ vrf_name: str = ""
162
162
  multipath: int = 0
163
163
  global_multipath: int = 0
164
164
  aggregate: Aggregate = field(default_factory=Aggregate)
@@ -213,7 +213,7 @@ class PeerGroup:
213
213
  af_rib_group: Optional[str] = None
214
214
  af_loops: int = 0
215
215
  hold_time: int = 0
216
- listen_network: bool = False
216
+ listen_network: list[str] = field(default_factory=list)
217
217
  remove_private: bool = False
218
218
  as_override: bool = False
219
219
  aigp: bool = False
@@ -234,6 +234,12 @@ class PeerGroup:
234
234
  @dataclass
235
235
  class VrfOptions:
236
236
  vrf_name: str
237
+
238
+ ipv4_unicast: FamilyOptions
239
+ ipv6_unicast: FamilyOptions
240
+ ipv4_labeled_unicast: FamilyOptions
241
+ ipv6_labeled_unicast: FamilyOptions
242
+
237
243
  vrf_name_global: Optional[str] = None
238
244
  import_policy: Optional[str] = None
239
245
  export_policy: Optional[str] = None
@@ -244,23 +250,20 @@ class VrfOptions:
244
250
  route_distinguisher: Optional[str] = None
245
251
  static_label: Optional[int] = None # FIXME: str?
246
252
 
247
- ipv4_unicast: Optional[FamilyOptions] = None
248
- ipv6_unicast: Optional[FamilyOptions] = None
249
- ipv4_labeled_unicast: Optional[FamilyOptions] = None
250
- ipv6_labeled_unicast: Optional[FamilyOptions] = None
251
253
  groups: list[PeerGroup] = field(default_factory=list)
252
254
 
253
255
 
254
256
  @dataclass
255
257
  class GlobalOptions:
258
+ ipv4_unicast: FamilyOptions
259
+ ipv6_unicast: FamilyOptions
260
+ ipv4_labeled_unicast: FamilyOptions
261
+ ipv6_labeled_unicast: FamilyOptions
262
+
256
263
  local_as: ASN = ASN(None)
257
264
  loops: int = 0
258
265
  multipath: int = 0
259
266
  router_id: str = ""
260
267
  vrf: dict[str, VrfOptions] = field(default_factory=dict)
261
268
 
262
- ipv4_unicast: Optional[FamilyOptions] = None
263
- ipv6_unicast: Optional[FamilyOptions] = None
264
- ipv4_labeled_unicast: Optional[FamilyOptions] = None
265
- ipv6_labeled_unicast: Optional[FamilyOptions] = None
266
269
  groups: list[PeerGroup] = field(default_factory=list)
@@ -135,8 +135,11 @@ def _implicit_tree(device):
135
135
  """
136
136
  elif device.hw.Cisco:
137
137
  text += r"""
138
- !interface
138
+ !interface *Ethernet*
139
+ mtu 1500
139
140
  no shutdown
141
+
142
+
140
143
  """
141
144
  if device.hw.Cisco.Catalyst:
142
145
  # this configuration is not visible in running-config when enabled
@@ -8,9 +8,12 @@ __all__ = [
8
8
  "Left",
9
9
  "Right",
10
10
  "Match",
11
+ "VirtualLocal",
12
+ "VirtualPeer",
11
13
  ]
12
14
 
13
15
  from .executor import MeshExecutor
14
16
  from .match_args import Left, Right, Match
15
- from .registry import DirectPeer, IndirectPeer, MeshSession, GlobalOptions
16
- from .registry import MeshRulesRegistry
17
+ from .registry import (
18
+ DirectPeer, IndirectPeer, MeshSession, GlobalOptions, MeshRulesRegistry, VirtualLocal, VirtualPeer,
19
+ )
@@ -1,16 +1,30 @@
1
1
  from typing import Annotated, Optional, Union
2
2
 
3
- from annet.bgp_models import Family, Aggregate, Redistribute
3
+ from annet.bgp_models import Family, Redistribute
4
4
  from .basemodel import BaseMeshModel, Concat, DictMerge, Merge, KeyDefaultDict
5
5
  from .peer_models import MeshPeerGroup
6
6
 
7
7
 
8
+ class Aggregate(BaseMeshModel):
9
+ policy: str
10
+ routes: Annotated[tuple[str, ...], Concat()]
11
+ export_policy: str
12
+ as_path: str
13
+ reference: str
14
+ suppress: bool
15
+ discard: bool
16
+ as_set: bool
17
+
18
+
8
19
  class FamilyOptions(BaseMeshModel):
20
+ def __init__(self, **kwargs):
21
+ kwargs.setdefault("aggregate", Aggregate())
22
+ super().__init__(**kwargs)
9
23
  family: Family
10
24
  vrf_name: str
11
25
  multipath: int = 0
12
26
  global_multipath: int
13
- aggregate: Aggregate
27
+ aggregate: Annotated[Aggregate, Merge()]
14
28
  redistributes: Annotated[tuple[Redistribute, ...], Concat()]
15
29
  allow_default: bool
16
30
  aspath_relax: bool
@@ -24,16 +38,26 @@ class FamilyOptions(BaseMeshModel):
24
38
 
25
39
 
26
40
  class _FamiliesMixin:
27
- ipv4_unicast: Optional[FamilyOptions]
28
- ipv6_unicast: Optional[FamilyOptions]
29
- ipv4_labeled_unicast: Optional[FamilyOptions]
30
- ipv6_labeled_unicast: Optional[FamilyOptions]
41
+ def __init__(self, **kwargs):
42
+ kwargs.setdefault("ipv4_unicast", FamilyOptions(family="ipv4_unicast"))
43
+ kwargs.setdefault("ipv6_unicast", FamilyOptions(family="ipv6_unicast"))
44
+ kwargs.setdefault("ipv4_labeled_unicast", FamilyOptions(family="ipv4_labeled"))
45
+ kwargs.setdefault("ipv6_labeled_unicast", FamilyOptions(family="ipv6_labeled"))
46
+ super().__init__(**kwargs)
47
+ ipv4_unicast: Annotated[FamilyOptions, Merge()]
48
+ ipv6_unicast: Annotated[FamilyOptions, Merge()]
49
+ ipv4_labeled_unicast: Annotated[FamilyOptions, Merge()]
50
+ ipv6_labeled_unicast: Annotated[FamilyOptions, Merge()]
31
51
 
32
52
 
33
- class VrfOptions(BaseMeshModel, _FamiliesMixin):
34
- def __init__(self, **kwargs):
53
+ class VrfOptions(_FamiliesMixin, BaseMeshModel):
54
+ def __init__(self, vrf_name: str, **kwargs):
55
+ kwargs.setdefault("ipv4_unicast", FamilyOptions(family="ipv4_unicast", vrf_name=vrf_name))
56
+ kwargs.setdefault("ipv6_unicast", FamilyOptions(family="ipv6_unicast", vrf_name=vrf_name))
57
+ kwargs.setdefault("ipv4_labeled_unicast", FamilyOptions(family="ipv4_labeled", vrf_name=vrf_name))
58
+ kwargs.setdefault("ipv6_labeled_unicast", FamilyOptions(family="ipv6_labeled", vrf_name=vrf_name))
35
59
  kwargs.setdefault("groups", KeyDefaultDict(lambda x: MeshPeerGroup(name=x)))
36
- super().__init__(**kwargs)
60
+ super().__init__(vrf_name=vrf_name, **kwargs)
37
61
 
38
62
  vrf_name: str
39
63
  vrf_name_global: Optional[str]
@@ -48,7 +72,7 @@ class VrfOptions(BaseMeshModel, _FamiliesMixin):
48
72
  groups: Annotated[dict[str, MeshPeerGroup], DictMerge(Merge())]
49
73
 
50
74
 
51
- class GlobalOptionsDTO(BaseMeshModel, _FamiliesMixin):
75
+ class GlobalOptionsDTO(_FamiliesMixin, BaseMeshModel):
52
76
  def __init__(self, **kwargs):
53
77
  kwargs.setdefault("groups", KeyDefaultDict(lambda x: MeshPeerGroup(name=x)))
54
78
  kwargs.setdefault("vrf", KeyDefaultDict(lambda x: VrfOptions(vrf_name=x)))
@@ -1,15 +1,22 @@
1
1
  from dataclasses import dataclass
2
2
  from logging import getLogger
3
- from typing import Annotated, Callable, Optional
3
+ from typing import Annotated, Callable, Optional, Union
4
4
 
5
5
  from annet.bgp_models import Peer, GlobalOptions
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
9
9
  from .models_converter import to_bgp_global_options, to_bgp_peer, InterfaceChanges, to_interface_changes
10
- from .peer_models import PeerDTO
11
- from .registry import MeshRulesRegistry, GlobalOptions as MeshGlobalOptions, DirectPeer, MeshSession, IndirectPeer
12
-
10
+ from .peer_models import DirectPeerDTO, IndirectPeerDTO, VirtualLocalDTO, VirtualPeerDTO
11
+ from .registry import (
12
+ DirectPeer,
13
+ GlobalOptions as MeshGlobalOptions,
14
+ IndirectPeer,
15
+ MeshRulesRegistry,
16
+ MeshSession,
17
+ VirtualLocal,
18
+ VirtualPeer,
19
+ )
13
20
 
14
21
  logger = getLogger(__name__)
15
22
 
@@ -28,11 +35,16 @@ class PeerKey:
28
35
 
29
36
 
30
37
  class Pair(BaseMeshModel):
31
- local: Annotated[PeerDTO, Merge()]
32
- connected: Annotated[PeerDTO, Merge()]
38
+ local: Annotated[Union[DirectPeerDTO, IndirectPeerDTO], Merge()]
39
+ connected: Annotated[Union[DirectPeerDTO, IndirectPeerDTO], Merge()]
33
40
  device: Annotated[Device, UseLast()]
34
41
 
35
42
 
43
+ class VirtualPair(BaseMeshModel):
44
+ local: Annotated[VirtualLocalDTO, Merge()]
45
+ connected: Annotated[VirtualPeerDTO, Merge()]
46
+
47
+
36
48
  class MeshExecutor:
37
49
  def __init__(
38
50
  self,
@@ -93,14 +105,14 @@ class MeshExecutor:
93
105
  rule.handler(peer_neighbor, peer_device, session)
94
106
 
95
107
  try:
96
- neighbor_dto = merge(PeerDTO(), peer_neighbor, session)
108
+ neighbor_dto = merge(DirectPeerDTO(), peer_neighbor, session)
97
109
  except MergeForbiddenError as e:
98
110
  raise ValueError(
99
111
  f"Handler `{handler_name}` provided session data conflicting with "
100
112
  f"peer data for device `{neighbor_device.fqdn}`:\n" + str(e)
101
113
  ) from e
102
114
  try:
103
- device_dto = merge(PeerDTO(), peer_device, session)
115
+ device_dto = merge(DirectPeerDTO(), peer_device, session)
104
116
  except MergeForbiddenError as e:
105
117
  raise ValueError(
106
118
  f"Handler `{handler_name}` provided session data conflicting with "
@@ -132,6 +144,42 @@ class MeshExecutor:
132
144
  neighbor_peers[peer_key] = pair
133
145
  return list(neighbor_peers.values())
134
146
 
147
+ def _execute_virtual(self, device: Device) -> list[VirtualPair]:
148
+ virtual_peers: list[VirtualPair] = []
149
+ for rule in self._registry.lookup_virtual(device.fqdn):
150
+ for order_number in rule.num:
151
+ handler_name = self._handler_name(rule.handler)
152
+ logger.debug("Running direct handler: %s", handler_name)
153
+ session = MeshSession()
154
+ peer_device = VirtualLocal(rule.match, device)
155
+ peer_virtual = VirtualPeer(num=order_number)
156
+
157
+ rule.handler(peer_device, peer_virtual, session)
158
+
159
+ try:
160
+ virtual_dto = merge(VirtualPeerDTO(), peer_virtual, session)
161
+ except MergeForbiddenError as e:
162
+ raise ValueError(
163
+ f"Handler `{handler_name}` provided session data conflicting with "
164
+ f"virtual peer data for device `{device.fqdn}` and num={order_number}:\n" + str(e)
165
+ ) from e
166
+ try:
167
+ device_dto = merge(VirtualLocalDTO(), peer_device, session)
168
+ except MergeForbiddenError as e:
169
+ raise ValueError(
170
+ f"Handler `{handler_name}` provided session data conflicting with "
171
+ f"peer data for device `{device.fqdn}`:\n" + str(e)
172
+ ) from e
173
+
174
+ if not hasattr(device_dto, "svi"):
175
+ raise ValueError(
176
+ f"Handler `{handler_name}` did not provide `svi` number. "
177
+ "Virtual peer must be connected to SVI interface."
178
+ )
179
+ pair = VirtualPair(local=device_dto, connected=virtual_dto)
180
+ virtual_peers.append(pair)
181
+ return virtual_peers
182
+
135
183
  def _execute_indirect(self, device: Device, all_fqdns: list[str]) -> list[Pair]:
136
184
  # we can have multiple rules for the same pair
137
185
  # we merge them according to remote fqdn
@@ -152,14 +200,14 @@ class MeshExecutor:
152
200
  rule.handler(peer_connected, peer_device, session)
153
201
 
154
202
  try:
155
- connected_dto = merge(PeerDTO(), peer_connected, session)
203
+ connected_dto = merge(IndirectPeerDTO(), peer_connected, session)
156
204
  except MergeForbiddenError as e:
157
205
  raise ValueError(
158
206
  f"Handler `{handler_name}` provided session data conflicting with "
159
207
  f"peer data for device `{connected_device.fqdn}`:\n" + str(e)
160
208
  ) from e
161
209
  try:
162
- device_dto = merge(PeerDTO(), peer_device, session)
210
+ device_dto = merge(IndirectPeerDTO(), peer_device, session)
163
211
  except MergeForbiddenError as e:
164
212
  raise ValueError(
165
213
  f"Handler `{handler_name}` provided session data conflicting with "
@@ -193,7 +241,10 @@ class MeshExecutor:
193
241
  return list(connected_peers.values()) # FIXME
194
242
 
195
243
  def _to_bgp_peer(self, pair: Pair, interface: Optional[str]) -> Peer:
196
- return to_bgp_peer(pair.local, pair.connected, pair.device, interface)
244
+ return to_bgp_peer(pair.local, pair.connected, pair.device.hostname, interface)
245
+
246
+ def _virtual_to_bgp_peer(self, pair: VirtualPair, interface: Optional[str]) -> Peer:
247
+ return to_bgp_peer(pair.local, pair.connected, "", interface)
197
248
 
198
249
  def _to_bgp_global(self, global_options: GlobalOptionsDTO) -> GlobalOptions:
199
250
  return to_bgp_global_options(global_options)
@@ -225,6 +276,9 @@ class MeshExecutor:
225
276
  target_interface.add_addr(changes.addr, changes.vrf)
226
277
  return target_interface.name
227
278
 
279
+ def _apply_virtual_interface_changes(self, device: Device, local: VirtualLocalDTO) -> str:
280
+ return device.add_svi(local.svi).name # we check if SVI configured in execute method
281
+
228
282
  def execute_for(self, device: Device) -> MeshExecutionResult:
229
283
  all_fqdns = self._storage.resolve_all_fdnds()
230
284
 
@@ -239,6 +293,13 @@ class MeshExecutor:
239
293
  )
240
294
  peers.append(self._to_bgp_peer(direct_pair, target_interface))
241
295
 
296
+ for virtual_pair in self._execute_virtual(device):
297
+ target_interface = self._apply_virtual_interface_changes(
298
+ device,
299
+ virtual_pair.local,
300
+ )
301
+ peers.append(self._virtual_to_bgp_peer(virtual_pair, target_interface))
302
+
242
303
  for connected_pair in self._execute_indirect(device, all_fqdns):
243
304
  peers.append(self._to_bgp_peer(connected_pair, None))
244
305
 
@@ -1,14 +1,18 @@
1
1
  from dataclasses import dataclass
2
2
  from ipaddress import ip_interface
3
- from typing import Optional
3
+ from typing import Optional, Union
4
4
 
5
5
  from adaptix import Retort, loader, Chain, name_mapping
6
6
 
7
- from .peer_models import PeerDTO
8
- from ..bgp_models import GlobalOptions, VrfOptions, FamilyOptions, Peer, PeerGroup, ASN, PeerOptions
7
+ from .peer_models import DirectPeerDTO, IndirectPeerDTO, VirtualPeerDTO, VirtualLocalDTO
8
+ from ..bgp_models import Aggregate, GlobalOptions, VrfOptions, FamilyOptions, Peer, PeerGroup, ASN, PeerOptions
9
9
  from ..storage import Device
10
10
 
11
11
 
12
+ PeerDTO = Union[DirectPeerDTO, IndirectPeerDTO, VirtualPeerDTO]
13
+ LocalDTO = Union[DirectPeerDTO, IndirectPeerDTO, VirtualLocalDTO]
14
+
15
+
12
16
  @dataclass
13
17
  class InterfaceChanges:
14
18
  addr: str
@@ -46,6 +50,7 @@ retort = Retort(
46
50
  loader(GlobalOptions, ObjMapping, Chain.FIRST),
47
51
  loader(VrfOptions, ObjMapping, Chain.FIRST),
48
52
  loader(FamilyOptions, ObjMapping, Chain.FIRST),
53
+ loader(Aggregate, ObjMapping, Chain.FIRST),
49
54
  loader(PeerOptions, ObjMapping, Chain.FIRST),
50
55
  name_mapping(PeerOptions, map={
51
56
  "local_as": "asnum",
@@ -62,14 +67,13 @@ to_bgp_global_options = retort.get_loader(GlobalOptions)
62
67
  to_interface_changes = retort.get_loader(InterfaceChanges)
63
68
 
64
69
 
65
- def to_bgp_peer(local: PeerDTO, connected: PeerDTO, connected_device: Device, interface: Optional[str]) -> Peer:
70
+ def to_bgp_peer(local: LocalDTO, connected: PeerDTO, connected_hostname: str, interface: Optional[str]) -> Peer:
66
71
  options = retort.load(local, PeerOptions)
67
- # TODO validate `lagg_links` before conversion
68
72
  result = Peer(
69
73
  addr=str(ip_interface(connected.addr).ip),
70
74
  interface=interface,
71
75
  remote_as=ASN(connected.asnum),
72
- hostname=connected_device.hostname,
76
+ hostname=connected_hostname,
73
77
  options=options,
74
78
  )
75
79
  # connected
@@ -61,7 +61,7 @@ class _OptionsDTO(_SharedOptionsDTO):
61
61
  af_rib_group: Optional[str]
62
62
  af_loops: int
63
63
  hold_time: int
64
- listen_network: bool
64
+ listen_network: list[str]
65
65
  remove_private: bool
66
66
  as_override: bool
67
67
  aigp: bool
@@ -77,17 +77,41 @@ class _OptionsDTO(_SharedOptionsDTO):
77
77
  mtu: int
78
78
 
79
79
 
80
- class PeerDTO(MeshSession, _OptionsDTO):
80
+ class DirectPeerDTO(MeshSession, _OptionsDTO):
81
81
  pod: int
82
82
  addr: str
83
83
  description: str
84
+ update_source: str
84
85
 
85
86
  subif: int
86
87
  lag: Optional[int]
87
88
  lag_links_min: Optional[int]
88
89
  svi: Optional[int]
89
90
 
90
- group_name: str
91
+
92
+ class IndirectPeerDTO(MeshSession, _OptionsDTO):
93
+ pod: int
94
+ addr: str
95
+ description: str
96
+ update_source: str
97
+
98
+
99
+ class VirtualLocalDTO(BaseMeshModel):
100
+ asnum: int
101
+ pod: int
102
+ addr: str
103
+ description: str
104
+
105
+ import_policy: str
106
+ export_policy: str
107
+ update_source: str
108
+
109
+ svi: int
110
+
111
+
112
+ class VirtualPeerDTO(MeshSession, _OptionsDTO):
113
+ addr: str
114
+ description: str
91
115
 
92
116
 
93
117
  class MeshPeerGroup(_OptionsDTO):
@@ -1,13 +1,13 @@
1
1
  from dataclasses import dataclass
2
- from typing import Callable, Any
2
+ from typing import Callable, Any, Sequence
3
3
 
4
4
  from .match_args import MatchExpr, PairMatcher, SingleMatcher
5
5
  from .match_args import MatchedArgs
6
6
  from .device_models import GlobalOptionsDTO
7
- from .peer_models import PeerDTO, MeshSession
7
+ from .peer_models import MeshSession, IndirectPeerDTO, VirtualLocalDTO, VirtualPeerDTO, DirectPeerDTO
8
8
 
9
9
 
10
- class DirectPeer(PeerDTO):
10
+ class DirectPeer(DirectPeerDTO):
11
11
  match: MatchedArgs
12
12
  device: Any
13
13
  ports: list[str]
@@ -19,7 +19,7 @@ class DirectPeer(PeerDTO):
19
19
  self.ports = ports
20
20
 
21
21
 
22
- class IndirectPeer(PeerDTO):
22
+ class IndirectPeer(IndirectPeerDTO):
23
23
  match: MatchedArgs
24
24
  device: Any
25
25
 
@@ -29,6 +29,20 @@ class IndirectPeer(PeerDTO):
29
29
  self.device = device
30
30
 
31
31
 
32
+ class VirtualLocal(VirtualLocalDTO):
33
+ match: MatchedArgs
34
+ device: Any
35
+
36
+ def __init__(self, match: MatchedArgs, device: Any) -> None:
37
+ super().__init__()
38
+ self.match = match
39
+ self.device = device
40
+
41
+
42
+ class VirtualPeer(VirtualPeerDTO):
43
+ num: int
44
+
45
+
32
46
  class GlobalOptions(GlobalOptionsDTO):
33
47
  match: MatchedArgs
34
48
  device: Any
@@ -50,6 +64,7 @@ class GlobalRule:
50
64
 
51
65
  DirectHandler = Callable[[DirectPeer, DirectPeer, MeshSession], None]
52
66
  IndirectHandler = Callable[[IndirectPeer, IndirectPeer, MeshSession], None]
67
+ VirtualHandler = Callable[[VirtualLocal, VirtualPeer, MeshSession], None]
53
68
 
54
69
 
55
70
  @dataclass
@@ -66,6 +81,14 @@ class IndirectRule:
66
81
  handler: IndirectHandler
67
82
 
68
83
 
84
+ @dataclass
85
+ class VirtualRule:
86
+ __slots__ = ("matcher", "num", "handler")
87
+ matcher: SingleMatcher
88
+ num: Sequence[int]
89
+ handler: VirtualHandler
90
+
91
+
69
92
  @dataclass
70
93
  class MatchedGlobal:
71
94
  __slots__ = ("match", "handler")
@@ -95,11 +118,20 @@ class MatchedIndirectPair:
95
118
  match_right: MatchedArgs
96
119
 
97
120
 
121
+ @dataclass
122
+ class MatchedVirtualPair:
123
+ __slots__ = ("match", "num", "handler")
124
+ match: MatchedArgs
125
+ num: Sequence[int]
126
+ handler: VirtualHandler
127
+
128
+
98
129
  class MeshRulesRegistry:
99
130
  def __init__(self, match_short_name: bool = False):
100
131
  self.direct_rules: list[DirectRule] = []
101
132
  self.indirect_rules: list[IndirectRule] = []
102
133
  self.global_rules: list[GlobalRule] = []
134
+ self.virtual_rules: list[VirtualRule] = []
103
135
  self.nested: list[MeshRulesRegistry] = []
104
136
  self.match_short_name = match_short_name
105
137
 
@@ -142,6 +174,17 @@ class MeshRulesRegistry:
142
174
 
143
175
  return register
144
176
 
177
+ def virtual(
178
+ self, peer_mask: str, num: Sequence[int], *match: MatchExpr,
179
+ ) -> Callable[[VirtualHandler], VirtualHandler]:
180
+ matcher = SingleMatcher(peer_mask, match)
181
+
182
+ def register(handler: VirtualHandler) -> VirtualHandler:
183
+ self.virtual_rules.append(VirtualRule(matcher, num, handler))
184
+ return handler
185
+
186
+ return register
187
+
145
188
  def lookup_direct(self, device: str, neighbors: list[str]) -> list[MatchedDirectPair]:
146
189
  found = []
147
190
  device = self._normalize_host(device)
@@ -198,6 +241,20 @@ class MeshRulesRegistry:
198
241
  found.extend(registry.lookup_indirect(device, devices))
199
242
  return found
200
243
 
244
+ def lookup_virtual(self, device: str) -> list[MatchedVirtualPair]:
245
+ found = []
246
+ device = self._normalize_host(device)
247
+ for rule in self.virtual_rules:
248
+ if args := rule.matcher.match_one(device):
249
+ found.append(MatchedVirtualPair(
250
+ handler=rule.handler,
251
+ match=args,
252
+ num=rule.num,
253
+ ))
254
+ for registry in self.nested:
255
+ found.extend(registry.lookup_virtual(device))
256
+ return found
257
+
201
258
  def lookup_global(self, device: str) -> list[MatchedGlobal]:
202
259
  found = []
203
260
  device = self._normalize_host(device)