annet 0.0__tar.gz

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

Potentially problematic release.


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

Files changed (144) hide show
  1. annet-0.0/AUTHORS +21 -0
  2. annet-0.0/LICENSE +21 -0
  3. annet-0.0/MANIFEST.in +1 -0
  4. annet-0.0/PKG-INFO +25 -0
  5. annet-0.0/README.md +111 -0
  6. annet-0.0/annet/__init__.py +61 -0
  7. annet-0.0/annet/adapters/__init__.py +0 -0
  8. annet-0.0/annet/adapters/netbox/__init__.py +0 -0
  9. annet-0.0/annet/adapters/netbox/common/__init__.py +0 -0
  10. annet-0.0/annet/adapters/netbox/common/client.py +87 -0
  11. annet-0.0/annet/adapters/netbox/common/manufacturer.py +62 -0
  12. annet-0.0/annet/adapters/netbox/common/models.py +105 -0
  13. annet-0.0/annet/adapters/netbox/common/query.py +23 -0
  14. annet-0.0/annet/adapters/netbox/common/status_client.py +25 -0
  15. annet-0.0/annet/adapters/netbox/common/storage_opts.py +14 -0
  16. annet-0.0/annet/adapters/netbox/provider.py +34 -0
  17. annet-0.0/annet/adapters/netbox/v24/__init__.py +0 -0
  18. annet-0.0/annet/adapters/netbox/v24/api_models.py +73 -0
  19. annet-0.0/annet/adapters/netbox/v24/client.py +59 -0
  20. annet-0.0/annet/adapters/netbox/v24/storage.py +196 -0
  21. annet-0.0/annet/adapters/netbox/v37/__init__.py +0 -0
  22. annet-0.0/annet/adapters/netbox/v37/api_models.py +38 -0
  23. annet-0.0/annet/adapters/netbox/v37/client.py +62 -0
  24. annet-0.0/annet/adapters/netbox/v37/storage.py +149 -0
  25. annet-0.0/annet/annet.py +25 -0
  26. annet-0.0/annet/annlib/__init__.py +7 -0
  27. annet-0.0/annet/annlib/command.py +49 -0
  28. annet-0.0/annet/annlib/diff.py +158 -0
  29. annet-0.0/annet/annlib/errors.py +8 -0
  30. annet-0.0/annet/annlib/filter_acl.py +196 -0
  31. annet-0.0/annet/annlib/jsontools.py +116 -0
  32. annet-0.0/annet/annlib/lib.py +495 -0
  33. annet-0.0/annet/annlib/netdev/__init__.py +0 -0
  34. annet-0.0/annet/annlib/netdev/db.py +62 -0
  35. annet-0.0/annet/annlib/netdev/devdb/__init__.py +28 -0
  36. annet-0.0/annet/annlib/netdev/devdb/data/devdb.json +137 -0
  37. annet-0.0/annet/annlib/netdev/views/__init__.py +0 -0
  38. annet-0.0/annet/annlib/netdev/views/dump.py +121 -0
  39. annet-0.0/annet/annlib/netdev/views/hardware.py +112 -0
  40. annet-0.0/annet/annlib/output.py +246 -0
  41. annet-0.0/annet/annlib/patching.py +533 -0
  42. annet-0.0/annet/annlib/rbparser/__init__.py +0 -0
  43. annet-0.0/annet/annlib/rbparser/acl.py +120 -0
  44. annet-0.0/annet/annlib/rbparser/deploying.py +55 -0
  45. annet-0.0/annet/annlib/rbparser/ordering.py +52 -0
  46. annet-0.0/annet/annlib/rbparser/platform.py +51 -0
  47. annet-0.0/annet/annlib/rbparser/syntax.py +115 -0
  48. annet-0.0/annet/annlib/rulebook/__init__.py +0 -0
  49. annet-0.0/annet/annlib/rulebook/common.py +350 -0
  50. annet-0.0/annet/annlib/tabparser.py +648 -0
  51. annet-0.0/annet/annlib/types.py +35 -0
  52. annet-0.0/annet/api/__init__.py +826 -0
  53. annet-0.0/annet/argparse.py +415 -0
  54. annet-0.0/annet/cli.py +237 -0
  55. annet-0.0/annet/cli_args.py +503 -0
  56. annet-0.0/annet/configs/context.yml +18 -0
  57. annet-0.0/annet/configs/logging.yaml +39 -0
  58. annet-0.0/annet/connectors.py +77 -0
  59. annet-0.0/annet/deploy.py +536 -0
  60. annet-0.0/annet/diff.py +84 -0
  61. annet-0.0/annet/executor.py +551 -0
  62. annet-0.0/annet/filtering.py +40 -0
  63. annet-0.0/annet/gen.py +865 -0
  64. annet-0.0/annet/generators/__init__.py +435 -0
  65. annet-0.0/annet/generators/base.py +136 -0
  66. annet-0.0/annet/generators/common/__init__.py +0 -0
  67. annet-0.0/annet/generators/common/initial.py +33 -0
  68. annet-0.0/annet/generators/entire.py +97 -0
  69. annet-0.0/annet/generators/exceptions.py +10 -0
  70. annet-0.0/annet/generators/jsonfragment.py +125 -0
  71. annet-0.0/annet/generators/partial.py +119 -0
  72. annet-0.0/annet/generators/perf.py +79 -0
  73. annet-0.0/annet/generators/ref.py +15 -0
  74. annet-0.0/annet/generators/result.py +127 -0
  75. annet-0.0/annet/hardware.py +45 -0
  76. annet-0.0/annet/implicit.py +139 -0
  77. annet-0.0/annet/lib.py +128 -0
  78. annet-0.0/annet/output.py +167 -0
  79. annet-0.0/annet/parallel.py +448 -0
  80. annet-0.0/annet/patching.py +25 -0
  81. annet-0.0/annet/reference.py +148 -0
  82. annet-0.0/annet/rulebook/__init__.py +114 -0
  83. annet-0.0/annet/rulebook/arista/__init__.py +0 -0
  84. annet-0.0/annet/rulebook/arista/iface.py +16 -0
  85. annet-0.0/annet/rulebook/aruba/__init__.py +16 -0
  86. annet-0.0/annet/rulebook/aruba/ap_env.py +146 -0
  87. annet-0.0/annet/rulebook/aruba/misc.py +8 -0
  88. annet-0.0/annet/rulebook/cisco/__init__.py +0 -0
  89. annet-0.0/annet/rulebook/cisco/iface.py +68 -0
  90. annet-0.0/annet/rulebook/cisco/misc.py +57 -0
  91. annet-0.0/annet/rulebook/cisco/vlandb.py +90 -0
  92. annet-0.0/annet/rulebook/common.py +19 -0
  93. annet-0.0/annet/rulebook/deploying.py +87 -0
  94. annet-0.0/annet/rulebook/huawei/__init__.py +0 -0
  95. annet-0.0/annet/rulebook/huawei/aaa.py +75 -0
  96. annet-0.0/annet/rulebook/huawei/bgp.py +97 -0
  97. annet-0.0/annet/rulebook/huawei/iface.py +33 -0
  98. annet-0.0/annet/rulebook/huawei/misc.py +337 -0
  99. annet-0.0/annet/rulebook/huawei/vlandb.py +115 -0
  100. annet-0.0/annet/rulebook/juniper/__init__.py +107 -0
  101. annet-0.0/annet/rulebook/nexus/__init__.py +0 -0
  102. annet-0.0/annet/rulebook/nexus/iface.py +92 -0
  103. annet-0.0/annet/rulebook/patching.py +143 -0
  104. annet-0.0/annet/rulebook/ribbon/__init__.py +12 -0
  105. annet-0.0/annet/rulebook/texts/arista.deploy +20 -0
  106. annet-0.0/annet/rulebook/texts/arista.order +125 -0
  107. annet-0.0/annet/rulebook/texts/arista.rul +59 -0
  108. annet-0.0/annet/rulebook/texts/aruba.deploy +20 -0
  109. annet-0.0/annet/rulebook/texts/aruba.order +83 -0
  110. annet-0.0/annet/rulebook/texts/aruba.rul +87 -0
  111. annet-0.0/annet/rulebook/texts/cisco.deploy +27 -0
  112. annet-0.0/annet/rulebook/texts/cisco.order +82 -0
  113. annet-0.0/annet/rulebook/texts/cisco.rul +105 -0
  114. annet-0.0/annet/rulebook/texts/huawei.deploy +188 -0
  115. annet-0.0/annet/rulebook/texts/huawei.order +388 -0
  116. annet-0.0/annet/rulebook/texts/huawei.rul +471 -0
  117. annet-0.0/annet/rulebook/texts/juniper.rul +120 -0
  118. annet-0.0/annet/rulebook/texts/nexus.deploy +24 -0
  119. annet-0.0/annet/rulebook/texts/nexus.order +85 -0
  120. annet-0.0/annet/rulebook/texts/nexus.rul +83 -0
  121. annet-0.0/annet/rulebook/texts/nokia.rul +31 -0
  122. annet-0.0/annet/rulebook/texts/pc.order +5 -0
  123. annet-0.0/annet/rulebook/texts/pc.rul +9 -0
  124. annet-0.0/annet/rulebook/texts/ribbon.deploy +22 -0
  125. annet-0.0/annet/rulebook/texts/ribbon.rul +77 -0
  126. annet-0.0/annet/rulebook/texts/routeros.order +38 -0
  127. annet-0.0/annet/rulebook/texts/routeros.rul +45 -0
  128. annet-0.0/annet/storage.py +125 -0
  129. annet-0.0/annet/tabparser.py +36 -0
  130. annet-0.0/annet/text_term_format.py +95 -0
  131. annet-0.0/annet/tracing.py +170 -0
  132. annet-0.0/annet/types.py +227 -0
  133. annet-0.0/annet.egg-info/PKG-INFO +25 -0
  134. annet-0.0/annet.egg-info/SOURCES.txt +142 -0
  135. annet-0.0/annet.egg-info/dependency_links.txt +1 -0
  136. annet-0.0/annet.egg-info/entry_points.txt +5 -0
  137. annet-0.0/annet.egg-info/requires.txt +16 -0
  138. annet-0.0/annet.egg-info/top_level.txt +2 -0
  139. annet-0.0/annet_generators/__init__.py +0 -0
  140. annet-0.0/annet_generators/example/__init__.py +12 -0
  141. annet-0.0/annet_generators/example/lldp.py +53 -0
  142. annet-0.0/requirements.txt +16 -0
  143. annet-0.0/setup.cfg +4 -0
  144. annet-0.0/setup.py +46 -0
annet-0.0/AUTHORS ADDED
@@ -0,0 +1,21 @@
1
+ The following authors have created the source code of "annet" published and distributed by YANDEX LLC as the owner:
2
+
3
+ Aleksandr Balezin <gescheit@yandex-team.ru>
4
+ Aleksandr Klimenko <v0lk@yandex-team.ru>
5
+ Aleksei Lymar <alymar@nebius.com>
6
+ Alexey Andriyanov <alan@yandex-team.ru>
7
+ Alexey Esin <adess@yandex-team.ru>
8
+ Anton Egorov <eg0rov@yandex-team.ru>
9
+ Artem Denisov <artemdenisov@yandex-team.ru>
10
+ Azat Kurbanov <azatkurbanov@yandex-team.ru>
11
+ Devaev Maxim <mdevaev@yandex-team.ru>
12
+ Fedor Zhukov <azryve@nebius.com>
13
+ Grigorii Ozhegov <ozhegov@yandex-team.ru>
14
+ Grigorii Solovev <gslv@yandex-team.ru>
15
+ Konstantin Sazonov <moonug@yandex-team.ru>
16
+ Roman Glebov <kitaro@yandex-team.ru>
17
+ Roman Karaulanov <anteron92@yandex-team.ru>
18
+ Sergey Mishchenko <smishche@yandex-team.ru>
19
+ Vlad Starostin <vladstar@yandex-team.ru>
20
+ Vladimir Sukhonosov <xornet@yandex-team.ru>
21
+ Vladislav Daniliuk <jayt@nebius.com>
annet-0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) [2023] YANDEX LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
annet-0.0/MANIFEST.in ADDED
@@ -0,0 +1 @@
1
+ include requirements.txt
annet-0.0/PKG-INFO ADDED
@@ -0,0 +1,25 @@
1
+ Metadata-Version: 2.1
2
+ Name: annet
3
+ Version: 0.0
4
+ Summary: annet
5
+ Home-page: https://github.com/annetutil/annet
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ License-File: LICENSE
9
+ License-File: AUTHORS
10
+ Requires-Dist: colorama>=0.4.6
11
+ Requires-Dist: tabulate>=0.9.0
12
+ Requires-Dist: jsonpatch>=1.33
13
+ Requires-Dist: jsonpointer>=2.4
14
+ Requires-Dist: PyYAML>=6.0.1
15
+ Requires-Dist: Pygments>=2.14.0
16
+ Requires-Dist: Mako>=1.2.4
17
+ Requires-Dist: Jinja2>=3.1.2
18
+ Requires-Dist: psutil>=5.8.0
19
+ Requires-Dist: packaging>=23.2
20
+ Requires-Dist: contextlog>=1.1
21
+ Requires-Dist: valkit>=0.1.4
22
+ Requires-Dist: aiohttp>=3.8.4
23
+ Requires-Dist: yarl>=1.8.2
24
+ Requires-Dist: adaptix==3.0.0b2
25
+ Requires-Dist: dataclass-rest==0.4
annet-0.0/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # Annet - network configuration utility
2
+
3
+ The system contains network appliance config generators written in Python with optional use of text preprocessors (Jinja2, Mako).
4
+ Huawei, Cisco IOS, Cisco NX-OS, Cisco IOS-XR, Juniper, as well as devices configured via separate config files (Linux, FreeBSD, Cumulus) are supported.
5
+
6
+ annet has a number of modes (subcommands):
7
+
8
+ - ```annet gen``` - generates the entire config for the specified devices or specified parts of it
9
+ - ```annet diff``` - first does gen and then builds diff with current config version
10
+ - ```annet patch``` - first does diff and then generates a list of commands to apply diff on the device
11
+
12
+ Usage help can be obtained by calling ```annet -h``` or for a specific command, such as ```annet gen -h```.
13
+
14
+ ## Overview
15
+
16
+ ## Configuration
17
+
18
+ Provide `NETBOX_URL` and `NETBOX_TOKEN` environment variable to setup data source.
19
+
20
+ ```shell
21
+ export NETBOX_URL="https://demo.netbox.dev"
22
+ export NETBOX_TOKEN="1234567890abcdef01234567890abcdef0123456"
23
+ ```
24
+
25
+ ### annet gen
26
+
27
+ The annet_generators directory contains many files called generators.
28
+ A generator takes information about the switch as input and returns the configuration.
29
+ The part of the config that the generator is responsible for is specified in the generator's acl function. If a generator returns a configuration that does not fall under acl, an exception will be thrown.
30
+
31
+ Example generator:
32
+
33
+ ```python
34
+ from annet.generators import PartialGenerator
35
+
36
+ class Mtu(PartialGenerator):
37
+ TAGS = ["mtu"]
38
+ def acl_cisco(self, _):
39
+ return "system mtu jumbo"
40
+
41
+ def run_cisco(self, device):
42
+ yield "system mtu jumbo %d" % 9000
43
+ ```
44
+
45
+
46
+ And an example of calling annet:
47
+ ```bash
48
+ annet gen -g mtu sw6-i1
49
+ # -------------------- sw6-i1.cfg --------------------
50
+ system mtu jumbo 9000
51
+ ```
52
+
53
+ Method `acl_cisco` defines scope of the generator, which commands and block it controls.
54
+ The option `-g mtu` means that only generators with the mtu element in the TAGS variable should be called. If no tag is specified, all generators will be executed.
55
+
56
+
57
+ ### annet diff
58
+
59
+ If we were configuring the switch from scratch, these options would be enough, but in our reality we need to be able not only to generate the desired configuration, but also to be able to bring the current configuration to the desired one.
60
+ To do this, you need to be able to delete an outdated configuration and correctly add a new one. The **diff** module, which implements some tricky logic, is responsible for this work.
61
+ This logic is defined in the rulebook/texts/VENDOR folder.
62
+
63
+ Example diff:
64
+ ```diff
65
+ # -------------------- sw1-i38.cfg --------------------
66
+ acl number 2610
67
+ - rule 40 permit source 10.11.170.150 0
68
+ + rule 12 permit source 10.11.133.81 0
69
+ ```
70
+
71
+ ### annet patch
72
+
73
+ Next, you need to create a list of commands from the resulting diff. The **patch** module is responsible for this. It receives the diff, runs the logic specified in rulebook/texts/VENDOR and returns the list of commands.
74
+ Let's take the above diff. It says to remove the command ``rule 40 permit source 10.11.170.150 0`` and add ``rule 12 permit source 10.11.133.81 0``.
75
+ Basic command delete logic for huawei is adding undo to the command. So the undo command will look like this: ``undo rule 40 permit source 10.11.170.150 0```, but this is an invalid command. In case of canceling acl rules, you need to execute ``undo rule N```.
76
+ So you need to write the undo logic for the ```rule ```` command in the ``acl ``` block.
77
+ Here is the part of rulebook/texts/huawei.rul responsible for this:
78
+ ```
79
+ acl name *
80
+ rule * %logic=huawei.misc.undo_redo
81
+ ```
82
+ The asterisk here means that the key argument of the undo_redo function will contain the first word after rule, namely the rule number.
83
+
84
+ Here, the undo_redo function from the file in rulebook/huawei/misc.py is used to generate the command to remove rules in acl.
85
+ ```python
86
+ def undo_redo(rule, key, diff, **_):
87
+ ...
88
+ ```
89
+ Now calling ```annet patch -g snmp sw1-i38```` returns the correct set of commands.
90
+ ```
91
+ acl number 2610
92
+ undo rule 40
93
+ rule 12 permit source 10.11.133.81 0
94
+ quit
95
+ ```
96
+
97
+
98
+ <!-- ### annet deploy
99
+
100
+ To apply these commands on a switch there is a **deploy** module.
101
+ annet can apply changes (roll out) to multiple devices at the same time.
102
+
103
+ By default, the edits that annet proposes to roll out will be shown before the rollout.
104
+ The user must confirm that they agree to roll out the proposed diff to a given list of devices.
105
+ During the rollout, annet will display the overall progress of the task and the log of one of the devices.
106
+
107
+ Normal layout. The screen with patches will be shown and the process of laying out will be displayed.
108
+ ```bash
109
+ annet deploy -g snmp $HOST
110
+ ```
111
+ Credentials will be used from the current user (username, ssh key, ssh agent, encrypted password in $HOME). -->
@@ -0,0 +1,61 @@
1
+ import logging
2
+ import logging.config
3
+ import os
4
+ import pkgutil
5
+ import sys
6
+ from argparse import SUPPRESS, Namespace
7
+
8
+ import colorama
9
+ import yaml
10
+ from annet.annlib.errors import ( # pylint: disable=wrong-import-position
11
+ DeployCancelled,
12
+ ExecError,
13
+ )
14
+ from contextlog import patch_logging, patch_threading
15
+ from valkit.python import valid_logging_level
16
+
17
+ import annet.argparse
18
+
19
+
20
+ __all__ = ("DeployCancelled", "ExecError")
21
+
22
+ DEBUG2_LEVELV_NUM = 9
23
+
24
+
25
+ def fill_base_args(parser: annet.argparse.ArgParser, pkg_name: str, logging_config: str):
26
+ parser.add_argument("--log-level", default="WARN", type=valid_logging_level,
27
+ help="Уровень детализации логов (DEBUG, DEBUG2 (with comocutor debug), INFO, WARN, CRITICAL)")
28
+ parser.add_argument("--pkg_name", default=pkg_name, help=SUPPRESS)
29
+ parser.add_argument("--logging_config", default=logging_config, help=SUPPRESS)
30
+
31
+
32
+ def init_logging(options: Namespace):
33
+ patch_logging()
34
+ patch_threading()
35
+ logging.captureWarnings(True)
36
+ logging_config = yaml.safe_load(pkgutil.get_data(options.pkg_name, options.logging_config))
37
+ if options.log_level is not None:
38
+ logging_config.setdefault("root", {})
39
+ logging_config["root"]["level"] = options.log_level
40
+ logging.addLevelName(DEBUG2_LEVELV_NUM, "DEBUG2")
41
+ logging.config.dictConfig(logging_config)
42
+
43
+
44
+ def init(options: Namespace):
45
+ init_logging(options)
46
+
47
+ # Отключить colorama.init, если стоит env-переменная. Нужно в тестах
48
+ if os.environ.get("ANN_FORCE_COLOR", None) not in [None, "", "0", "no"]:
49
+ colorama.init = lambda *_, **__: None
50
+ colorama.init()
51
+
52
+ # Workaround for Python 3.8.0: https://bugs.python.org/issue38529
53
+ import asyncio.streams
54
+ if hasattr(asyncio.streams.StreamReaderProtocol, "_on_reader_gc"):
55
+ asyncio.streams.StreamReaderProtocol._on_reader_gc = lambda *args, **kwargs: None # pylint: disable=protected-access
56
+
57
+
58
+ def assert_python_version():
59
+ if sys.version_info < (3, 8, 0):
60
+ sys.stderr.write("Error: you need python 3.8.0 or higher\n")
61
+ sys.exit(1)
File without changes
File without changes
File without changes
@@ -0,0 +1,87 @@
1
+ from dataclasses import dataclass
2
+ from functools import wraps
3
+ from typing import Generic, Optional, List, TypeVar, Callable
4
+
5
+ from dataclass_rest.http.requests import RequestsClient
6
+ from requests import Session
7
+
8
+ Model = TypeVar("Model")
9
+
10
+
11
+ @dataclass
12
+ class PagingResponse(Generic[Model]):
13
+ next: Optional[str]
14
+ previous: Optional[str]
15
+ count: int
16
+ results: List[Model]
17
+
18
+
19
+ Func = TypeVar("Func", bound=Callable)
20
+
21
+
22
+ def _collect_by_pages(func: Func) -> Func:
23
+ """Collect all results using only pagination."""
24
+ @wraps(func)
25
+ def wrapper(self, *args, **kwargs):
26
+ kwargs.setdefault("offset", 0)
27
+ limit = kwargs.setdefault("limit", 100)
28
+ results = []
29
+ method = func.__get__(self, self.__class__)
30
+ has_next = True
31
+ while has_next:
32
+ page = method(*args, **kwargs)
33
+ kwargs["offset"] += limit
34
+ results.extend(page.results)
35
+ has_next = bool(page.next)
36
+ return PagingResponse(
37
+ previous=None,
38
+ next=None,
39
+ count=len(results),
40
+ results=results,
41
+ )
42
+
43
+ return wrapper
44
+
45
+
46
+ # default batch size 100 is calculated to fit list of UUIDs in 4k URL length
47
+ def collect(func: Func, field: str = "", batch_size: int = 100) -> Func:
48
+ """
49
+ Collect data from method iterating over pages and filter batches.
50
+
51
+ :param func: Method to call
52
+ :param field: Field which defines a filter split into batches
53
+ :param batch_size: Limit of values in `field` filter requested at a time
54
+ """
55
+ func = _collect_by_pages(func)
56
+ if not field:
57
+ return func
58
+
59
+ @wraps(func)
60
+ def wrapper(self, *args, **kwargs):
61
+ value = kwargs.get(field)
62
+ if not value:
63
+ return func(*args, **kwargs)
64
+
65
+ method = func.__get__(self, self.__class__)
66
+ results = []
67
+ for offset in range(0, len(value), batch_size):
68
+ kwargs[field] = value[offset:offset + batch_size]
69
+ page = method(*args, **kwargs)
70
+ results.extend(page.results)
71
+ return PagingResponse(
72
+ previous=None,
73
+ next=None,
74
+ count=len(results),
75
+ results=results,
76
+ )
77
+
78
+ return wrapper
79
+
80
+
81
+ class BaseNetboxClient(RequestsClient):
82
+ def __init__(self, url: str, token: str):
83
+ url = url.rstrip("/") + "/api/"
84
+ session = Session()
85
+ if token:
86
+ session.headers["Authorization"] = f"Token {token}"
87
+ super().__init__(url, session)
@@ -0,0 +1,62 @@
1
+ from logging import getLogger
2
+
3
+ from annet.annlib.netdev.views.hardware import HardwareView
4
+
5
+ logger = getLogger(__name__)
6
+
7
+ _VENDORS = {
8
+ "cisco": "Cisco",
9
+ "catalyst": "Cisco Catalyst",
10
+ "nexus": "Cisco Nexus",
11
+ "huawei": "Huawei",
12
+ "juniper": "Juniper",
13
+ "arista": "Arista",
14
+ "pc": "PC",
15
+ "nokia": "Nokia",
16
+ "aruba": "Aruba",
17
+ "routeros": "RouterOS",
18
+ "ribbon": "Ribbon",
19
+ }
20
+
21
+
22
+ def _vendor_to_hw(vendor):
23
+ return HardwareView(_VENDORS.get(vendor.lower(), vendor), None)
24
+
25
+
26
+ def get_hw(manufacturer: str, model: str):
27
+ # by some reason Netbox calls Mellanox SN as MSN, so we fix them here
28
+ if manufacturer == "Mellanox" and model.startswith("MSN"):
29
+ model = model.replace("MSN", "SN", 1)
30
+ hw = _vendor_to_hw(manufacturer + " " + model)
31
+ if not hw:
32
+ raise ValueError(f"unsupported manufacturer {manufacturer}")
33
+ return hw
34
+
35
+
36
+ def get_breed(manufacturer: str, model: str):
37
+ if manufacturer == "Huawei" and model.startswith("CE"):
38
+ return "vrp85"
39
+ elif manufacturer == "Huawei" and model.startswith("NE"):
40
+ return "vrp85"
41
+ elif manufacturer == "Huawei":
42
+ return "vrp55"
43
+ elif manufacturer == "Mellanox":
44
+ return "cuml2"
45
+ elif manufacturer == "Juniper":
46
+ return "jun10"
47
+ elif manufacturer == "Cisco":
48
+ return "ios12"
49
+ elif manufacturer == "Adva":
50
+ return "adva8"
51
+ elif manufacturer == "Arista":
52
+ return "eos4"
53
+ raise ValueError(f"unsupported manufacturer {manufacturer}")
54
+
55
+
56
+ def is_supported(manufacturer: str) -> bool:
57
+ if manufacturer not in (
58
+ "Huawei", "Mellanox", "Juniper", "Cisco", "Adva", "Arista",
59
+ ):
60
+ logger.warning("Unsupported manufacturer `%s`", manufacturer)
61
+ return False
62
+ return True
@@ -0,0 +1,105 @@
1
+ from dataclasses import dataclass, field
2
+ from datetime import datetime
3
+ from typing import List, Optional, Any, Dict
4
+
5
+ from annet.annlib.netdev.views.hardware import HardwareView
6
+ from annet.storage import Storage
7
+
8
+
9
+ @dataclass
10
+ class Entity:
11
+ id: int
12
+ name: str
13
+
14
+
15
+ @dataclass
16
+ class Label:
17
+ value: str
18
+ label: str
19
+
20
+
21
+ @dataclass
22
+ class IpFamily:
23
+ value: int
24
+ label: str
25
+
26
+
27
+ @dataclass
28
+ class DeviceType:
29
+ id: int
30
+ manufacturer: Entity
31
+ model: str
32
+
33
+
34
+ @dataclass
35
+ class DeviceIp:
36
+ id: int
37
+ display: str
38
+ address: str
39
+ family: int
40
+
41
+
42
+ @dataclass
43
+ class IpAddress:
44
+ id: int
45
+ assigned_object_id: int
46
+ display: str
47
+ family: IpFamily
48
+ address: str
49
+ status: Label
50
+ tags: List[Entity]
51
+ created: datetime
52
+ last_updated: datetime
53
+
54
+
55
+ @dataclass
56
+ class Interface(Entity):
57
+ device: Entity
58
+ enabled: bool
59
+ display: str = ""
60
+ ip_addresses: List[IpAddress] = field(default_factory=list)
61
+
62
+
63
+ @dataclass
64
+ class NetboxDevice(Entity):
65
+ url: str
66
+ storage: Storage
67
+ neighbours_ids: List[int]
68
+
69
+ display: str
70
+ device_type: DeviceType
71
+ device_role: Entity
72
+ tenant: Optional[Entity]
73
+ platform: Optional[Entity]
74
+ serial: str
75
+ asset_tag: Optional[str]
76
+ site: Entity
77
+ rack: Optional[Entity]
78
+ position: Optional[float]
79
+ face: Optional[Label]
80
+ status: Label
81
+ primary_ip: Optional[DeviceIp]
82
+ primary_ip4: Optional[DeviceIp]
83
+ primary_ip6: Optional[DeviceIp]
84
+ tags: List[Entity]
85
+ custom_fields: Dict[str, Any]
86
+ created: datetime
87
+ last_updated: datetime
88
+
89
+ fqdn: str
90
+ hostname: str
91
+ hw: Optional[HardwareView]
92
+ breed: str
93
+
94
+ interfaces: List[Interface]
95
+
96
+ # compat
97
+
98
+ def __hash__(self):
99
+ return hash((self.id, type(self)))
100
+
101
+ def __eq__(self, other):
102
+ return type(self) is type(other) and self.url == other.url
103
+
104
+ def is_pc(self):
105
+ return self.device_type.manufacturer.name == "Mellanox"
@@ -0,0 +1,23 @@
1
+ from dataclasses import dataclass
2
+ from typing import List, Union, Iterable, Optional
3
+
4
+ from annet.storage import Query
5
+
6
+
7
+ @dataclass
8
+ class NetboxQuery(Query):
9
+ query: List[str]
10
+
11
+ @classmethod
12
+ def new(
13
+ cls, query: Union[str, Iterable[str]],
14
+ hosts_range: Optional[slice] = None,
15
+ ) -> "NetboxQuery":
16
+ if hosts_range is not None:
17
+ raise ValueError("host_range is not supported")
18
+ return cls(query=list(query))
19
+
20
+ @property
21
+ def globs(self):
22
+ # We process every query host as a glob
23
+ return self.query
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass
2
+ from typing import Dict
3
+
4
+ from adaptix import Retort, name_mapping, NameStyle
5
+ from dataclass_rest import get
6
+ from dataclass_rest.client_protocol import FactoryProtocol
7
+
8
+ from .client import BaseNetboxClient
9
+
10
+
11
+ @dataclass
12
+ class Status:
13
+ netbox_version: str
14
+ plugins: Dict[str, str]
15
+
16
+
17
+ class NetboxStatusClient(BaseNetboxClient):
18
+ def _init_response_body_factory(self) -> FactoryProtocol:
19
+ return Retort(recipe=[
20
+ name_mapping(name_style=NameStyle.LOWER_KEBAB)
21
+ ])
22
+
23
+ @get("status")
24
+ def status(self) -> Status:
25
+ ...
@@ -0,0 +1,14 @@
1
+ import os
2
+
3
+
4
+ class NetboxStorageOpts:
5
+ def __init__(self, url: str, token: str):
6
+ self.url = url
7
+ self.token = token
8
+
9
+ @classmethod
10
+ def from_cli_opts(cls, cli_opts):
11
+ return cls(
12
+ url=os.getenv("NETBOX_URL", "http://localhost"),
13
+ token=os.getenv("NETBOX_TOKEN", "").strip(),
14
+ )
@@ -0,0 +1,34 @@
1
+ from dataclass_rest.exceptions import ClientError
2
+
3
+ from annet.storage import StorageProvider, Storage
4
+ from .common.status_client import NetboxStatusClient
5
+ from .common.storage_opts import NetboxStorageOpts
6
+ from .common.query import NetboxQuery
7
+ from .v24.storage import NetboxStorageV24
8
+ from .v37.storage import NetboxStorageV37
9
+
10
+
11
+ def storage_factory(opts: NetboxStorageOpts) -> Storage:
12
+ client = NetboxStatusClient(opts.url, opts.token)
13
+ try:
14
+ status = client.status()
15
+ except ClientError as e:
16
+ if e.status_code == 404:
17
+ # old version do not support status reqeust
18
+ return NetboxStorageV24(opts)
19
+ raise
20
+ if status.netbox_version.startswith("3."):
21
+ return NetboxStorageV37(opts)
22
+ else:
23
+ raise ValueError(f"Unsupported version: {status.netbox_version}")
24
+
25
+
26
+ class NetboxProvider(StorageProvider):
27
+ def storage(self):
28
+ return storage_factory
29
+
30
+ def opts(self):
31
+ return NetboxStorageOpts
32
+
33
+ def query(self):
34
+ return NetboxQuery
File without changes
@@ -0,0 +1,73 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime
3
+ from typing import List, Optional, Any, Dict
4
+
5
+ from annet.adapters.netbox.common.models import Entity, DeviceType
6
+
7
+
8
+ @dataclass
9
+ class Label:
10
+ value: int
11
+ label: str
12
+
13
+
14
+ @dataclass
15
+ class DeviceIp:
16
+ id: int
17
+ address: str
18
+ family: int
19
+
20
+
21
+ @dataclass
22
+ class Device(Entity):
23
+ url: str
24
+ display_name: str
25
+ device_type: DeviceType
26
+ device_role: Entity
27
+ tenant: Optional[Entity]
28
+ platform: Optional[Entity]
29
+ serial: str
30
+ asset_tag: Optional[str]
31
+ site: Entity
32
+ rack: Optional[Entity]
33
+ position: Optional[float]
34
+ face: Optional[Label]
35
+ status: Label
36
+ primary_ip: Optional[DeviceIp]
37
+ primary_ip4: Optional[DeviceIp]
38
+ primary_ip6: Optional[DeviceIp]
39
+ tags: List[str]
40
+ custom_fields: Dict[str, Any]
41
+ created: datetime
42
+ last_updated: datetime
43
+
44
+
45
+ @dataclass
46
+ class Interface(Entity):
47
+ device: Entity
48
+ enabled: bool
49
+
50
+
51
+ @dataclass
52
+ class Vrf(Entity):
53
+ rd: str
54
+
55
+
56
+ @dataclass
57
+ class IpAddress:
58
+ id: int
59
+ family: int
60
+ address: str
61
+ vrf: Optional[Vrf]
62
+ tenant: Any # ???
63
+ status: Label
64
+ description: Optional[str]
65
+ custom_fields: Dict[str, Any]
66
+ tags: List[str]
67
+ created: datetime
68
+ last_updated: datetime
69
+
70
+ interface: Entity
71
+
72
+ nat_inside: Any # ???
73
+ nat_outside: Any # ???