gnetcli-adapter 0.0.2__py3-none-any.whl

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.
@@ -0,0 +1 @@
1
+ __version__ = "0.0.2"
@@ -0,0 +1,293 @@
1
+ import asyncio
2
+ import json
3
+ import subprocess
4
+ import time
5
+
6
+ from annet.deploy import DeployDriver, DeployOptions, DeployResult, apply_deploy_rulebook
7
+ from annet.annlib.command import Command, CommandList
8
+ from annet.annlib.netdev.views.hardware import HardwareView
9
+ from annet.rulebook import common
10
+
11
+ from annet.deploy import Fetcher, AdapterWithConfig, AdapterWithName
12
+ from typing import Dict, List, Any, Optional, Tuple
13
+ from annet.storage import Device
14
+ from gnetclisdk.client import Credentials, Gnetcli, HostParams
15
+ from pydantic import Field, field_validator, FieldValidationInfo
16
+ from pydantic_core import PydanticUndefined
17
+ from pydantic_settings import BaseSettings, SettingsConfigDict
18
+ import base64
19
+ import logging
20
+ import threading
21
+ import atexit
22
+
23
+ breed_to_device = {
24
+ "routeros": "ros",
25
+ }
26
+ _local_gnetcli: Optional[threading.Thread] = None
27
+ _local_gnetcli_p: Optional[subprocess.Popen] = None
28
+ _local_gnetcli_url: Optional[str] = None
29
+ LOG_FORMAT = "%(asctime)s - l:%(lineno)d - %(funcName)s() - %(levelname)s - %(message)s"
30
+ DATE_FMT = "%Y-%m-%d %H:%M:%S"
31
+ _logger = logging.getLogger(__name__)
32
+ GNETCLI_SERVER = "/usr/bin/server"
33
+
34
+
35
+ class AppSettings(BaseSettings):
36
+ model_config = SettingsConfigDict(env_prefix="gnetcli_", validate_assignment=True)
37
+
38
+ server: str = Field(default="localhost:50051")
39
+ insecure_grpc: bool = Field(default=True)
40
+ login: str = Field(default="")
41
+ password: str = Field(default="")
42
+ dev_login: str = Field(default="")
43
+ dev_password: str = Field(default="")
44
+
45
+ def make_credentials(self) -> Credentials:
46
+ return Credentials(self.dev_login, self.dev_password)
47
+
48
+ @field_validator("*", mode="before")
49
+ @classmethod
50
+ def not_none(cls, value: Any, info: FieldValidationInfo):
51
+ # NOTE: All fields that are optional for values, will assume the value in
52
+ # "default" (if defined in "Field") if "None" is informed as "value". That
53
+ # is, "None" is never assumed if passed as a "value".
54
+ if (
55
+ cls.model_fields[info.field_name].get_default() is not PydanticUndefined
56
+ and not cls.model_fields[info.field_name].is_required()
57
+ and value is None
58
+ ):
59
+ return cls.model_fields[info.field_name].get_default()
60
+ return value
61
+
62
+
63
+ async def get_config(breed: str) -> List[str]:
64
+ if breed == "routeros":
65
+ return ["/export"]
66
+ raise Exception("unknown breed")
67
+
68
+
69
+ def check_gnetcli_server():
70
+ global _local_gnetcli
71
+ if not _local_gnetcli:
72
+ t = threading.Thread(target=run_gnetcli_server, args=())
73
+ t.daemon = True
74
+ t.start()
75
+ time.sleep(1)
76
+ _local_gnetcli = t
77
+ if _local_gnetcli_p is None:
78
+ raise Exception("server failed")
79
+
80
+
81
+ def cleanup():
82
+ if _local_gnetcli_p is not None:
83
+ _local_gnetcli_p.kill()
84
+
85
+
86
+ atexit.register(cleanup)
87
+
88
+
89
+ def run_gnetcli_server():
90
+ global _local_gnetcli_p
91
+ global _local_gnetcli_url
92
+ _logger.info("starting gnetcli server")
93
+ try:
94
+ proc = subprocess.Popen(
95
+ [GNETCLI_SERVER, "--conf-file", "-"],
96
+ stdout=subprocess.PIPE,
97
+ stderr=subprocess.PIPE,
98
+ stdin=subprocess.PIPE,
99
+ bufsize=1,
100
+ universal_newlines=True,
101
+ )
102
+ except Exception as e:
103
+ logging.exception("server exec error %s", e)
104
+ raise
105
+ proc.stdin.write(
106
+ """
107
+ logging:
108
+ level: debug
109
+ json: true
110
+ port: 0
111
+ """
112
+ )
113
+ proc.stdin.close()
114
+ _local_gnetcli_p = proc
115
+ while True:
116
+ output = proc.stderr.readline()
117
+ if output == "" and proc.poll() is not None:
118
+ break
119
+ if output:
120
+ _logger.debug("gnetcli output: %s", output.strip())
121
+ if _local_gnetcli_url is None:
122
+ try:
123
+ data = json.loads(output)
124
+ except Exception:
125
+ pass
126
+ else:
127
+ if data.get("msg") == "init tcp socket":
128
+ _local_gnetcli_url = data.get("address")
129
+ if data.get("level") == "panic":
130
+ _logger.error("gnetcli error %s", data)
131
+ _logger.debug("set gnetcli server exit code %s", proc.returncode)
132
+ rc = proc.poll()
133
+ return rc
134
+
135
+
136
+ class GnetcliFetcher(Fetcher, AdapterWithConfig, AdapterWithName):
137
+ def __init__(
138
+ self,
139
+ url: Optional[str] = None,
140
+ login: Optional[str] = None,
141
+ password: Optional[str] = None,
142
+ dev_login: Optional[str] = None,
143
+ dev_password: Optional[str] = None,
144
+ ):
145
+ if not url:
146
+ check_gnetcli_server()
147
+ if _local_gnetcli_url is None:
148
+ _logger.info("waiting for _local_gnetcli_url appears")
149
+ start = time.monotonic()
150
+ while time.monotonic() - start < 5:
151
+ if _local_gnetcli_p is not None and _local_gnetcli_p.returncode is not None:
152
+ raise Exception("gnetcli server died with code %s" % _local_gnetcli_p.returncode)
153
+ if _local_gnetcli_url is not None:
154
+ break
155
+
156
+ self.conf = AppSettings(
157
+ login=login, password=password, dev_login=dev_login, dev_password=dev_password, server=_local_gnetcli_url
158
+ )
159
+ auth_token = (
160
+ base64.b64encode(b"%s:%s" % (self.conf.login.encode(), self.conf.password.encode())).strip().decode()
161
+ )
162
+ auth_token = f"Basic {auth_token}"
163
+ self.api = Gnetcli(
164
+ server=self.conf.server,
165
+ auth_token=auth_token,
166
+ insecure_grpc=self.conf.insecure_grpc,
167
+ user_agent="annet",
168
+ )
169
+
170
+ def name(self) -> str:
171
+ return "gnetcli"
172
+
173
+ def with_config(self, **kwargs: Dict[str, Any]) -> Fetcher:
174
+ return GnetcliFetcher(**kwargs)
175
+
176
+ def fetch_packages(self, devices: List[Device], processes: int = 1, max_slots: int = 0):
177
+ if not devices:
178
+ return {}, {}
179
+ raise NotImplementedError()
180
+
181
+ def fetch(
182
+ self,
183
+ devices: List[Device],
184
+ files_to_download: Dict[str, List[str]] = None,
185
+ processes: int = 1,
186
+ max_slots: int = 0,
187
+ ) -> Tuple[Dict[Device, str], Dict[Device, Any]]:
188
+ return asyncio.run(self.afetch(devices=devices))
189
+
190
+ async def afetch(self, devices: List[Device]):
191
+ running = {}
192
+ failed_running = {}
193
+ for device in devices:
194
+ try:
195
+ dev_res = await self.afetch_dev(device=device)
196
+ except Exception as e:
197
+ failed_running[device] = e
198
+ else:
199
+ running[device] = dev_res
200
+ return running, failed_running
201
+
202
+ async def afetch_dev(self, device: Device) -> str:
203
+ if device.breed not in breed_to_device:
204
+ raise Exception("unknown breed for gnetcli")
205
+ device_cls = breed_to_device[device.breed]
206
+
207
+ cmds = await get_config(breed=device.breed)
208
+ dev_result = []
209
+ for cmd in cmds:
210
+ res = await self.api.cmd(
211
+ hostname=device.fqdn,
212
+ cmd=cmd,
213
+ host_params=HostParams(
214
+ credentials=self.conf.make_credentials(),
215
+ device=device_cls,
216
+ ),
217
+ )
218
+ if res.status != 0:
219
+ raise Exception("cmd error %s" % res)
220
+ dev_result.append(res.out)
221
+ return b"\n".join(dev_result).decode()
222
+
223
+
224
+ class GnetcliDeployer(DeployDriver, AdapterWithConfig, AdapterWithName):
225
+ def __init__(
226
+ self,
227
+ login: Optional[str] = None,
228
+ password: Optional[str] = None,
229
+ dev_login: Optional[str] = None,
230
+ dev_password: Optional[str] = None,
231
+ ):
232
+ self.conf = AppSettings(login=login, password=password, dev_login=dev_login, dev_password=dev_password)
233
+ auth_token = (
234
+ base64.b64encode(b"%s:%s" % (self.conf.login.encode(), self.conf.password.encode())).strip().decode()
235
+ )
236
+ auth_token = f"Basic {auth_token}"
237
+ self.api = Gnetcli(
238
+ server=self.conf.server,
239
+ auth_token=auth_token,
240
+ insecure_grpc=self.conf.insecure_grpc,
241
+ user_agent="annet",
242
+ )
243
+
244
+ def name(self) -> str:
245
+ return "gnetcli"
246
+
247
+ def with_config(self, **kwargs: Dict[str, Any]) -> DeployDriver:
248
+ return GnetcliDeployer(**kwargs)
249
+
250
+ async def bulk_deploy(self, deploy_cmds: Dict[Device, CommandList], args: DeployOptions) -> DeployResult:
251
+ deploy_items = deploy_cmds.items()
252
+ result = await asyncio.gather(*[asyncio.Task(self.deploy(device, cmds, args)) for device, cmds in deploy_items])
253
+ res = DeployResult(hostnames=[], results={}, durations={}, original_states={})
254
+ res.add_results(results={dev.fqdn: dev_res for (dev, _), dev_res in zip(deploy_items, result)})
255
+ return res
256
+
257
+ async def deploy(self, device: Device, cmds: CommandList, args: DeployOptions) -> str:
258
+ device_cls = breed_to_device[device.breed]
259
+ async with self.api.cmd_session(hostname=device.fqdn) as sess:
260
+ result = []
261
+ for cmd in cmds:
262
+ res = await sess.cmd(
263
+ cmd=cmd.cmd,
264
+ cmd_timeout=cmd.timeout,
265
+ host_params=HostParams(
266
+ credentials=self.conf.make_credentials(),
267
+ device=device_cls,
268
+ ),
269
+ )
270
+ if res.status != 0:
271
+ raise Exception("cmd %s error %s status %s", cmd, res.err, res.status)
272
+ result.append(res)
273
+ return result
274
+
275
+ def apply_deploy_rulebook(
276
+ self,
277
+ hw: HardwareView,
278
+ cmd_paths: Dict[Tuple[str, ...], Dict[str, Any]],
279
+ do_finalize: bool = True,
280
+ do_commit: bool = True,
281
+ ):
282
+ res = apply_deploy_rulebook(hw=hw, cmd_paths=cmd_paths, do_finalize=do_finalize, do_commit=do_commit)
283
+ return res
284
+
285
+ def build_configuration_cmdlist(self, hw: HardwareView, do_finalize: bool = True, do_commit: bool = True):
286
+ res = common.apply(hw, do_commit=do_commit, do_finalize=do_finalize)
287
+ return res
288
+
289
+ def build_exit_cmdlist(self, hw: HardwareView) -> CommandList:
290
+ ret = CommandList()
291
+ if hw.Huawei:
292
+ ret.add_cmd(Command("quit", suppress_eof=True))
293
+ return ret
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.1
2
+ Name: gnetcli_adapter
3
+ Version: 0.0.2
4
+ Summary: Gnetcli-server adapter for Annet
5
+ Author-email: Aleksandr Balezin <gescheit12@gmail.com>
6
+ Requires-Python: >=3.8.1
7
+ Description-Content-Type: text/markdown
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Requires-Dist: annet
17
+ Requires-Dist: gnetclisdk>=1.0.2
18
+ Requires-Dist: pydantic_settings
19
+ Requires-Dist: pydantic
20
+ Requires-Dist: bandit[toml]==1.7.5 ; extra == "test"
21
+ Requires-Dist: black==23.3.0 ; extra == "test"
22
+ Requires-Dist: check-manifest==0.49 ; extra == "test"
23
+ Requires-Dist: flake8-bugbear==23.5.9 ; extra == "test"
24
+ Requires-Dist: flake8-docstrings ; extra == "test"
25
+ Requires-Dist: flake8-formatter_junit_xml ; extra == "test"
26
+ Requires-Dist: flake8 ; extra == "test"
27
+ Requires-Dist: flake8-pyproject ; extra == "test"
28
+ Requires-Dist: pre-commit==3.3.1 ; extra == "test"
29
+ Requires-Dist: pylint==2.17.4 ; extra == "test"
30
+ Requires-Dist: pylint_junit ; extra == "test"
31
+ Requires-Dist: pytest-cov==4.0.0 ; extra == "test"
32
+ Requires-Dist: pytest-mock<3.10.1 ; extra == "test"
33
+ Requires-Dist: pytest-runner ; extra == "test"
34
+ Requires-Dist: pytest==7.3.1 ; extra == "test"
35
+ Requires-Dist: pytest-github-actions-annotate-failures ; extra == "test"
36
+ Requires-Dist: shellcheck-py==0.9.0.2 ; extra == "test"
37
+ Project-URL: Documentation, https://github.com/annetutil/gnetcli_adapter
38
+ Project-URL: Source, https://github.com/annetutil/gnetcli_adapter
39
+ Project-URL: Tracker, https://github.com/annetutil/gnetcli_adapter/issues
40
+ Provides-Extra: test
41
+
42
+ # gnetcli_adapter
43
+ This package provides deployer and fetcher adapters for Annet
44
+
@@ -0,0 +1,6 @@
1
+ gnetcli_adapter/__init__.py,sha256=QvlVh4JTl3JL7jQAja76yKtT-IvF4631ASjWY1wS6AQ,22
2
+ gnetcli_adapter/gnetcli_adapter.py,sha256=TrqGcflvG8FD6tjFOnPr959827e8ukKCQPCCZVBrtJI,10302
3
+ gnetcli_adapter-0.0.2.dist-info/entry_points.txt,sha256=jjqSLHm62HJpsTDccbTN7Zh8dDUeppyyyYANGVZ3PIA,142
4
+ gnetcli_adapter-0.0.2.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
5
+ gnetcli_adapter-0.0.2.dist-info/METADATA,sha256=n6ilBu-341y4uX62VbCDem-4N056yEjKoxwsSnfZVhM,1927
6
+ gnetcli_adapter-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,4 @@
1
+ [annet.adapters]
2
+ deploy_driver=gnetcli_adapter.gnetcli_adapter:GnetcliDeployer
3
+ deploy_fetcher=gnetcli_adapter.gnetcli_adapter:GnetcliFetcher
4
+