annet 0.16.13__tar.gz → 0.16.15__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.15}/PKG-INFO +1 -1
  2. {annet-0.16.13 → annet-0.16.15}/README.md +1 -1
  3. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/common/models.py +7 -0
  4. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/common/storage_opts.py +7 -2
  5. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/provider.py +3 -1
  6. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/v37/storage.py +26 -19
  7. {annet-0.16.13 → annet-0.16.15}/annet/annlib/netdev/devdb/data/devdb.json +2 -0
  8. {annet-0.16.13 → annet-0.16.15}/annet/bgp_models.py +15 -11
  9. {annet-0.16.13 → annet-0.16.15}/annet/implicit.py +16 -10
  10. {annet-0.16.13 → annet-0.16.15}/annet/mesh/__init__.py +5 -2
  11. {annet-0.16.13 → annet-0.16.15}/annet/mesh/device_models.py +34 -10
  12. {annet-0.16.13 → annet-0.16.15}/annet/mesh/executor.py +72 -11
  13. {annet-0.16.13 → annet-0.16.15}/annet/mesh/models_converter.py +19 -11
  14. {annet-0.16.13 → annet-0.16.15}/annet/mesh/peer_models.py +28 -3
  15. {annet-0.16.13 → annet-0.16.15}/annet/mesh/registry.py +61 -4
  16. {annet-0.16.13 → annet-0.16.15/annet.egg-info}/PKG-INFO +1 -1
  17. {annet-0.16.13 → annet-0.16.15}/annet_generators/mesh_example/bgp.py +1 -0
  18. {annet-0.16.13 → annet-0.16.15}/annet_generators/mesh_example/mesh_logic.py +14 -1
  19. {annet-0.16.13 → annet-0.16.15}/AUTHORS +0 -0
  20. {annet-0.16.13 → annet-0.16.15}/LICENSE +0 -0
  21. {annet-0.16.13 → annet-0.16.15}/MANIFEST.in +0 -0
  22. {annet-0.16.13 → annet-0.16.15}/annet/__init__.py +0 -0
  23. {annet-0.16.13 → annet-0.16.15}/annet/adapters/__init__.py +0 -0
  24. {annet-0.16.13 → annet-0.16.15}/annet/adapters/fetchers/__init__.py +0 -0
  25. {annet-0.16.13 → annet-0.16.15}/annet/adapters/fetchers/stub/__init__.py +0 -0
  26. {annet-0.16.13 → annet-0.16.15}/annet/adapters/fetchers/stub/fetcher.py +0 -0
  27. {annet-0.16.13 → annet-0.16.15}/annet/adapters/file/__init__.py +0 -0
  28. {annet-0.16.13 → annet-0.16.15}/annet/adapters/file/provider.py +0 -0
  29. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/__init__.py +0 -0
  30. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/common/__init__.py +0 -0
  31. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/common/client.py +0 -0
  32. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/common/manufacturer.py +0 -0
  33. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/common/query.py +0 -0
  34. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/common/status_client.py +0 -0
  35. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/v24/__init__.py +0 -0
  36. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/v24/storage.py +0 -0
  37. {annet-0.16.13 → annet-0.16.15}/annet/adapters/netbox/v37/__init__.py +0 -0
  38. {annet-0.16.13 → annet-0.16.15}/annet/annet.py +0 -0
  39. {annet-0.16.13 → annet-0.16.15}/annet/annlib/__init__.py +0 -0
  40. {annet-0.16.13 → annet-0.16.15}/annet/annlib/command.py +0 -0
  41. {annet-0.16.13 → annet-0.16.15}/annet/annlib/diff.py +0 -0
  42. {annet-0.16.13 → annet-0.16.15}/annet/annlib/errors.py +0 -0
  43. {annet-0.16.13 → annet-0.16.15}/annet/annlib/filter_acl.py +0 -0
  44. {annet-0.16.13 → annet-0.16.15}/annet/annlib/jsontools.py +0 -0
  45. {annet-0.16.13 → annet-0.16.15}/annet/annlib/lib.py +0 -0
  46. {annet-0.16.13 → annet-0.16.15}/annet/annlib/netdev/__init__.py +0 -0
  47. {annet-0.16.13 → annet-0.16.15}/annet/annlib/netdev/db.py +0 -0
  48. {annet-0.16.13 → annet-0.16.15}/annet/annlib/netdev/devdb/__init__.py +0 -0
  49. {annet-0.16.13 → annet-0.16.15}/annet/annlib/netdev/views/__init__.py +0 -0
  50. {annet-0.16.13 → annet-0.16.15}/annet/annlib/netdev/views/dump.py +0 -0
  51. {annet-0.16.13 → annet-0.16.15}/annet/annlib/netdev/views/hardware.py +0 -0
  52. {annet-0.16.13 → annet-0.16.15}/annet/annlib/output.py +0 -0
  53. {annet-0.16.13 → annet-0.16.15}/annet/annlib/patching.py +0 -0
  54. {annet-0.16.13 → annet-0.16.15}/annet/annlib/rbparser/__init__.py +0 -0
  55. {annet-0.16.13 → annet-0.16.15}/annet/annlib/rbparser/acl.py +0 -0
  56. {annet-0.16.13 → annet-0.16.15}/annet/annlib/rbparser/deploying.py +0 -0
  57. {annet-0.16.13 → annet-0.16.15}/annet/annlib/rbparser/ordering.py +0 -0
  58. {annet-0.16.13 → annet-0.16.15}/annet/annlib/rbparser/platform.py +0 -0
  59. {annet-0.16.13 → annet-0.16.15}/annet/annlib/rbparser/syntax.py +0 -0
  60. {annet-0.16.13 → annet-0.16.15}/annet/annlib/rulebook/__init__.py +0 -0
  61. {annet-0.16.13 → annet-0.16.15}/annet/annlib/rulebook/common.py +0 -0
  62. {annet-0.16.13 → annet-0.16.15}/annet/annlib/tabparser.py +0 -0
  63. {annet-0.16.13 → annet-0.16.15}/annet/annlib/types.py +0 -0
  64. {annet-0.16.13 → annet-0.16.15}/annet/api/__init__.py +0 -0
  65. {annet-0.16.13 → annet-0.16.15}/annet/argparse.py +0 -0
  66. {annet-0.16.13 → annet-0.16.15}/annet/cli.py +0 -0
  67. {annet-0.16.13 → annet-0.16.15}/annet/cli_args.py +0 -0
  68. {annet-0.16.13 → annet-0.16.15}/annet/configs/context.yml +0 -0
  69. {annet-0.16.13 → annet-0.16.15}/annet/configs/logging.yaml +0 -0
  70. {annet-0.16.13 → annet-0.16.15}/annet/connectors.py +0 -0
  71. {annet-0.16.13 → annet-0.16.15}/annet/deploy.py +0 -0
  72. {annet-0.16.13 → annet-0.16.15}/annet/diff.py +0 -0
  73. {annet-0.16.13 → annet-0.16.15}/annet/executor.py +0 -0
  74. {annet-0.16.13 → annet-0.16.15}/annet/filtering.py +0 -0
  75. {annet-0.16.13 → annet-0.16.15}/annet/gen.py +0 -0
  76. {annet-0.16.13 → annet-0.16.15}/annet/generators/__init__.py +0 -0
  77. {annet-0.16.13 → annet-0.16.15}/annet/generators/base.py +0 -0
  78. {annet-0.16.13 → annet-0.16.15}/annet/generators/common/__init__.py +0 -0
  79. {annet-0.16.13 → annet-0.16.15}/annet/generators/common/initial.py +0 -0
  80. {annet-0.16.13 → annet-0.16.15}/annet/generators/entire.py +0 -0
  81. {annet-0.16.13 → annet-0.16.15}/annet/generators/exceptions.py +0 -0
  82. {annet-0.16.13 → annet-0.16.15}/annet/generators/jsonfragment.py +0 -0
  83. {annet-0.16.13 → annet-0.16.15}/annet/generators/partial.py +0 -0
  84. {annet-0.16.13 → annet-0.16.15}/annet/generators/perf.py +0 -0
  85. {annet-0.16.13 → annet-0.16.15}/annet/generators/ref.py +0 -0
  86. {annet-0.16.13 → annet-0.16.15}/annet/generators/result.py +0 -0
  87. {annet-0.16.13 → annet-0.16.15}/annet/hardware.py +0 -0
  88. {annet-0.16.13 → annet-0.16.15}/annet/lib.py +0 -0
  89. {annet-0.16.13 → annet-0.16.15}/annet/mesh/basemodel.py +0 -0
  90. {annet-0.16.13 → annet-0.16.15}/annet/mesh/match_args.py +0 -0
  91. {annet-0.16.13 → annet-0.16.15}/annet/output.py +0 -0
  92. {annet-0.16.13 → annet-0.16.15}/annet/parallel.py +0 -0
  93. {annet-0.16.13 → annet-0.16.15}/annet/patching.py +0 -0
  94. {annet-0.16.13 → annet-0.16.15}/annet/reference.py +0 -0
  95. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/__init__.py +0 -0
  96. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/arista/__init__.py +0 -0
  97. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/arista/iface.py +0 -0
  98. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/aruba/__init__.py +0 -0
  99. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/aruba/ap_env.py +0 -0
  100. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/aruba/misc.py +0 -0
  101. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/b4com/__init__.py +0 -0
  102. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/b4com/file.py +0 -0
  103. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/cisco/__init__.py +0 -0
  104. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/cisco/iface.py +0 -0
  105. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/cisco/misc.py +0 -0
  106. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/cisco/vlandb.py +0 -0
  107. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/common.py +0 -0
  108. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/deploying.py +0 -0
  109. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/huawei/__init__.py +0 -0
  110. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/huawei/aaa.py +0 -0
  111. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/huawei/bgp.py +0 -0
  112. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/huawei/iface.py +0 -0
  113. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/huawei/misc.py +0 -0
  114. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/huawei/vlandb.py +0 -0
  115. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/juniper/__init__.py +0 -0
  116. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/nexus/__init__.py +0 -0
  117. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/nexus/iface.py +0 -0
  118. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/patching.py +0 -0
  119. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/ribbon/__init__.py +0 -0
  120. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/routeros/__init__.py +0 -0
  121. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/routeros/file.py +0 -0
  122. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/arista.deploy +0 -0
  123. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/arista.order +0 -0
  124. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/arista.rul +0 -0
  125. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/aruba.deploy +0 -0
  126. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/aruba.order +0 -0
  127. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/aruba.rul +0 -0
  128. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/b4com.deploy +0 -0
  129. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/b4com.order +0 -0
  130. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/b4com.rul +0 -0
  131. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/cisco.deploy +0 -0
  132. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/cisco.order +0 -0
  133. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/cisco.rul +0 -0
  134. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/huawei.deploy +0 -0
  135. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/huawei.order +0 -0
  136. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/huawei.rul +0 -0
  137. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/juniper.rul +0 -0
  138. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/nexus.deploy +0 -0
  139. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/nexus.order +0 -0
  140. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/nexus.rul +0 -0
  141. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/nokia.rul +0 -0
  142. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/optixtrans.deploy +0 -0
  143. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/optixtrans.order +0 -0
  144. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/optixtrans.rul +0 -0
  145. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/pc.deploy +0 -0
  146. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/pc.order +0 -0
  147. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/pc.rul +0 -0
  148. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/ribbon.deploy +0 -0
  149. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/ribbon.rul +0 -0
  150. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/routeros.order +0 -0
  151. {annet-0.16.13 → annet-0.16.15}/annet/rulebook/texts/routeros.rul +0 -0
  152. {annet-0.16.13 → annet-0.16.15}/annet/storage.py +0 -0
  153. {annet-0.16.13 → annet-0.16.15}/annet/tabparser.py +0 -0
  154. {annet-0.16.13 → annet-0.16.15}/annet/text_term_format.py +0 -0
  155. {annet-0.16.13 → annet-0.16.15}/annet/tracing.py +0 -0
  156. {annet-0.16.13 → annet-0.16.15}/annet/types.py +0 -0
  157. {annet-0.16.13 → annet-0.16.15}/annet.egg-info/SOURCES.txt +0 -0
  158. {annet-0.16.13 → annet-0.16.15}/annet.egg-info/dependency_links.txt +0 -0
  159. {annet-0.16.13 → annet-0.16.15}/annet.egg-info/entry_points.txt +0 -0
  160. {annet-0.16.13 → annet-0.16.15}/annet.egg-info/requires.txt +0 -0
  161. {annet-0.16.13 → annet-0.16.15}/annet.egg-info/top_level.txt +0 -0
  162. {annet-0.16.13 → annet-0.16.15}/annet_generators/__init__.py +0 -0
  163. {annet-0.16.13 → annet-0.16.15}/annet_generators/example/__init__.py +0 -0
  164. {annet-0.16.13 → annet-0.16.15}/annet_generators/example/lldp.py +0 -0
  165. {annet-0.16.13 → annet-0.16.15}/annet_generators/mesh_example/__init__.py +0 -0
  166. {annet-0.16.13 → annet-0.16.15}/requirements.txt +0 -0
  167. {annet-0.16.13 → annet-0.16.15}/setup.cfg +0 -0
  168. {annet-0.16.13 → annet-0.16.15}/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.15
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
@@ -139,6 +139,13 @@ class Interface(Entity):
139
139
  family = IpFamily(value=6, label="IPv6")
140
140
  else:
141
141
  family = IpFamily(value=4, label="IPv4")
142
+
143
+ for existing_addr in self.ip_addresses:
144
+ if existing_addr.address == address_mask and (
145
+ (existing_addr.vrf is None and vrf is None) or
146
+ (existing_addr.vrf is not None and existing_addr.vrf.name == vrf)
147
+ ):
148
+ return
142
149
  self.ip_addresses.append(IpAddress(
143
150
  id=0,
144
151
  display=address_mask,
@@ -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 = {
@@ -54,6 +54,8 @@
54
54
  "Huawei.CE.CE8800.CE8851": " CE8851",
55
55
  "Huawei.CE.CE8800.CE8855": " CE8855",
56
56
  "Huawei.CE.CE9800": " CE98\\d\\d",
57
+ "Huawei.CE.CE9800.CE9855": " CE9855",
58
+ "Huawei.CE.CE9800.CE9860": " CE9860",
57
59
  "Huawei.Quidway": " (LS-)?S",
58
60
  "Huawei.Quidway.S2x": "2\\d{3}",
59
61
  "Huawei.Quidway.S2x.S2300": "23\\d\\d",
@@ -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)
@@ -178,6 +178,7 @@ class FamilyOptions:
178
178
  class PeerGroup:
179
179
  name: str
180
180
  remote_as: ASN = ASN(None)
181
+ families: set[Family] = field(default_factory=set)
181
182
  internal_name: str = ""
182
183
  description: str = ""
183
184
  update_source: str = ""
@@ -213,7 +214,7 @@ class PeerGroup:
213
214
  af_rib_group: Optional[str] = None
214
215
  af_loops: int = 0
215
216
  hold_time: int = 0
216
- listen_network: bool = False
217
+ listen_network: list[str] = field(default_factory=list)
217
218
  remove_private: bool = False
218
219
  as_override: bool = False
219
220
  aigp: bool = False
@@ -234,6 +235,12 @@ class PeerGroup:
234
235
  @dataclass
235
236
  class VrfOptions:
236
237
  vrf_name: str
238
+
239
+ ipv4_unicast: FamilyOptions
240
+ ipv6_unicast: FamilyOptions
241
+ ipv4_labeled_unicast: FamilyOptions
242
+ ipv6_labeled_unicast: FamilyOptions
243
+
237
244
  vrf_name_global: Optional[str] = None
238
245
  import_policy: Optional[str] = None
239
246
  export_policy: Optional[str] = None
@@ -244,23 +251,20 @@ class VrfOptions:
244
251
  route_distinguisher: Optional[str] = None
245
252
  static_label: Optional[int] = None # FIXME: str?
246
253
 
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
254
  groups: list[PeerGroup] = field(default_factory=list)
252
255
 
253
256
 
254
257
  @dataclass
255
258
  class GlobalOptions:
259
+ ipv4_unicast: FamilyOptions
260
+ ipv6_unicast: FamilyOptions
261
+ ipv4_labeled_unicast: FamilyOptions
262
+ ipv6_labeled_unicast: FamilyOptions
263
+
256
264
  local_as: ASN = ASN(None)
257
265
  loops: int = 0
258
266
  multipath: int = 0
259
267
  router_id: str = ""
260
268
  vrf: dict[str, VrfOptions] = field(default_factory=dict)
261
269
 
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
270
  groups: list[PeerGroup] = field(default_factory=list)
@@ -71,14 +71,14 @@ def _implicit_tree(device):
71
71
  netconf
72
72
  """
73
73
  elif device.hw.Arista:
74
- # эта часть конфигурации будет не видна в конфиге, если она включена с таким набором полей:
74
+ # This part of configuration will not be visible in configuration
75
75
  text = r"""
76
76
  ip load-sharing trident fields ipv6 destination-port source-ip ingress-interface destination-ip source-port flow-label
77
77
  ip load-sharing trident fields ip source-ip source-port destination-ip destination-port ingress-interface
78
78
  """
79
79
  elif device.hw.Nexus:
80
80
  text = r"""
81
- # часть конфигурации скрытая если включена
81
+ # This part of configuration will not be visible in configuration if enabled
82
82
  snmp-server enable traps link linkDown
83
83
  snmp-server enable traps link linkUp
84
84
  """
@@ -107,25 +107,24 @@ def _implicit_tree(device):
107
107
  """
108
108
 
109
109
  elif device.hw.Nexus.N3x:
110
- # У нексуса сложные взаимоотношения с
111
- # shutdown командой вездe
112
- # в данный момент поведение проверенно для Cisco Nexus 3132Q 6.0(2)U6(7)
110
+ # Cisco Nexus has some specific related to "shutdown" command
111
+ # Behavior is cheked on Cisco Nexus 3132Q 6.0(2)U6(7)
113
112
  text += r"""
114
113
  # SVI
115
114
  !interface Vlan*
116
115
  !shutdown
117
116
  !interface mgmt[0-9]*
118
117
  no shutdown
119
- # Физические НЕ сплитованные интерфейсы и subif'ы
118
+ # Physical and NOT splitted interfaces and subifs
120
119
  !interface */Ethernet1\/[0-9.]*/
121
120
  no shutdown
122
- # Физические НЕ сплитованные интерфейсы и subif'ы
121
+ # Physical and NOT splitted interfaces and subifs
123
122
  !interface */Ethernet1\/[0-9]+\/[0-9.]+/
124
123
  # только explicit
125
- # Лупбеки
124
+ # Loopbacks
126
125
  !interface */Loopback[0-9.]+/
127
126
  no shutdown
128
- # Агрегаты
127
+ # Port-Channels
129
128
  !interface */port-channel[0-9.]+/
130
129
  no shutdown
131
130
  # BGP
@@ -135,7 +134,14 @@ def _implicit_tree(device):
135
134
  """
136
135
  elif device.hw.Cisco:
137
136
  text += r"""
138
- !interface
137
+ !interface */\S*Ethernet\S+/
138
+ mtu 1500
139
+ no shutdown
140
+ !interface */Loopback[0-9.]+/
141
+ mtu 1500
142
+ no shutdown
143
+ !interface */port-channel[0-9.]+/
144
+ mtu 1500
139
145
  no shutdown
140
146
  """
141
147
  if device.hw.Cisco.Catalyst:
@@ -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,12 +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
- from adaptix import Retort, loader, Chain, name_mapping
5
+ from adaptix import Retort, loader, Chain, name_mapping, as_is_loader
6
6
 
7
- from .peer_models import PeerDTO
8
- from ..bgp_models import GlobalOptions, VrfOptions, FamilyOptions, Peer, PeerGroup, ASN, PeerOptions
9
- from ..storage import Device
7
+ from .peer_models import DirectPeerDTO, IndirectPeerDTO, VirtualPeerDTO, VirtualLocalDTO
8
+ from ..bgp_models import (
9
+ Aggregate, GlobalOptions, VrfOptions, FamilyOptions, Peer, PeerGroup, ASN, PeerOptions,
10
+ Redistribute, BFDTimers,
11
+ )
12
+
13
+
14
+ PeerDTO = Union[DirectPeerDTO, IndirectPeerDTO, VirtualPeerDTO]
15
+ LocalDTO = Union[DirectPeerDTO, IndirectPeerDTO, VirtualLocalDTO]
10
16
 
11
17
 
12
18
  @dataclass
@@ -46,7 +52,10 @@ retort = Retort(
46
52
  loader(GlobalOptions, ObjMapping, Chain.FIRST),
47
53
  loader(VrfOptions, ObjMapping, Chain.FIRST),
48
54
  loader(FamilyOptions, ObjMapping, Chain.FIRST),
55
+ loader(Aggregate, ObjMapping, Chain.FIRST),
49
56
  loader(PeerOptions, ObjMapping, Chain.FIRST),
57
+ as_is_loader(Redistribute),
58
+ as_is_loader(BFDTimers),
50
59
  name_mapping(PeerOptions, map={
51
60
  "local_as": "asnum",
52
61
  }),
@@ -62,14 +71,13 @@ to_bgp_global_options = retort.get_loader(GlobalOptions)
62
71
  to_interface_changes = retort.get_loader(InterfaceChanges)
63
72
 
64
73
 
65
- def to_bgp_peer(local: PeerDTO, connected: PeerDTO, connected_device: Device, interface: Optional[str]) -> Peer:
74
+ def to_bgp_peer(local: LocalDTO, connected: PeerDTO, connected_hostname: str, interface: Optional[str]) -> Peer:
66
75
  options = retort.load(local, PeerOptions)
67
- # TODO validate `lagg_links` before conversion
68
76
  result = Peer(
69
77
  addr=str(ip_interface(connected.addr).ip),
70
78
  interface=interface,
71
79
  remote_as=ASN(connected.asnum),
72
- hostname=connected_device.hostname,
80
+ hostname=connected_hostname,
73
81
  options=options,
74
82
  )
75
83
  # connected
@@ -78,7 +86,7 @@ def to_bgp_peer(local: PeerDTO, connected: PeerDTO, connected_device: Device, in
78
86
  result.description = getattr(connected, "description", result.description)
79
87
  result.families = getattr(connected, "families", result.families)
80
88
  # local
81
- result.import_policy = getattr(connected, "import_policy", result.import_policy)
82
- result.export_policy = getattr(connected, "export_policy", result.export_policy)
83
- result.update_source = getattr(connected, "update_source", result.update_source)
89
+ result.import_policy = getattr(local, "import_policy", result.import_policy)
90
+ result.export_policy = getattr(local, "export_policy", result.export_policy)
91
+ result.update_source = getattr(local, "update_source", result.update_source)
84
92
  return result
@@ -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,21 +77,46 @@ 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(_OptionsDTO):
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):
113
+ addr: str
114
+ description: str
91
115
 
92
116
 
93
117
  class MeshPeerGroup(_OptionsDTO):
94
118
  name: str
119
+ families: Annotated[set[FamilyName], Concat()]
95
120
  remote_as: Union[int, str]
96
121
  internal_name: str
97
122
  update_source: str