gnetcli-adapter 0.0.2__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.
|
@@ -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,309 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["flit_core >=2,<4"]
|
|
3
|
+
build-backend = "flit_core.buildapi"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gnetcli_adapter"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"annet",
|
|
9
|
+
"gnetclisdk>=1.0.2",
|
|
10
|
+
"pydantic_settings",
|
|
11
|
+
"pydantic"
|
|
12
|
+
]
|
|
13
|
+
authors = [
|
|
14
|
+
{name = "Aleksandr Balezin", email = "gescheit12@gmail.com"},
|
|
15
|
+
]
|
|
16
|
+
description = "Gnetcli-server adapter for Annet"
|
|
17
|
+
readme = "README.md"
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.8",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11"
|
|
27
|
+
]
|
|
28
|
+
requires-python = ">=3.8.1"
|
|
29
|
+
dynamic = ["version"]
|
|
30
|
+
|
|
31
|
+
[project.entry-points."annet.adapters"]
|
|
32
|
+
deploy_fetcher = "gnetcli_adapter.gnetcli_adapter:GnetcliFetcher"
|
|
33
|
+
deploy_driver = "gnetcli_adapter.gnetcli_adapter:GnetcliDeployer"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
test = [
|
|
37
|
+
"bandit[toml]==1.7.5",
|
|
38
|
+
"black==23.3.0",
|
|
39
|
+
"check-manifest==0.49",
|
|
40
|
+
"flake8-bugbear==23.5.9",
|
|
41
|
+
"flake8-docstrings",
|
|
42
|
+
"flake8-formatter_junit_xml",
|
|
43
|
+
"flake8",
|
|
44
|
+
"flake8-pyproject",
|
|
45
|
+
"pre-commit==3.3.1",
|
|
46
|
+
"pylint==2.17.4",
|
|
47
|
+
"pylint_junit",
|
|
48
|
+
"pytest-cov==4.0.0",
|
|
49
|
+
"pytest-mock<3.10.1",
|
|
50
|
+
"pytest-runner",
|
|
51
|
+
"pytest==7.3.1",
|
|
52
|
+
"pytest-github-actions-annotate-failures",
|
|
53
|
+
"shellcheck-py==0.9.0.2"
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[project.urls]
|
|
57
|
+
Documentation = "https://github.com/annetutil/gnetcli_adapter"
|
|
58
|
+
Source = "https://github.com/annetutil/gnetcli_adapter"
|
|
59
|
+
Tracker = "https://github.com/annetutil/gnetcli_adapter/issues"
|
|
60
|
+
|
|
61
|
+
[tool.flit.module]
|
|
62
|
+
name = "gnetcli_adapter"
|
|
63
|
+
|
|
64
|
+
[tool.bandit]
|
|
65
|
+
exclude_dirs = ["build","dist","tests","scripts"]
|
|
66
|
+
number = 4
|
|
67
|
+
recursive = true
|
|
68
|
+
targets = "src"
|
|
69
|
+
|
|
70
|
+
[tool.black]
|
|
71
|
+
line-length = 120
|
|
72
|
+
fast = true
|
|
73
|
+
|
|
74
|
+
[tool.coverage.run]
|
|
75
|
+
branch = true
|
|
76
|
+
|
|
77
|
+
[tool.coverage.report]
|
|
78
|
+
fail_under = 100
|
|
79
|
+
|
|
80
|
+
[tool.flake8]
|
|
81
|
+
max-line-length = 120
|
|
82
|
+
select = "F,E,W,B,B901,B902,B903"
|
|
83
|
+
exclude = [
|
|
84
|
+
".eggs",
|
|
85
|
+
".git",
|
|
86
|
+
".tox",
|
|
87
|
+
"nssm",
|
|
88
|
+
"obj",
|
|
89
|
+
"out",
|
|
90
|
+
"packages",
|
|
91
|
+
"pywin32",
|
|
92
|
+
"tests",
|
|
93
|
+
"swagger_client"
|
|
94
|
+
]
|
|
95
|
+
ignore = [
|
|
96
|
+
"E722",
|
|
97
|
+
"B001",
|
|
98
|
+
"W503",
|
|
99
|
+
"E203"
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
[tool.pyright]
|
|
103
|
+
include = ["src"]
|
|
104
|
+
exclude = [
|
|
105
|
+
"**/__pycache__",
|
|
106
|
+
]
|
|
107
|
+
venv = "env37"
|
|
108
|
+
|
|
109
|
+
reportMissingImports = true
|
|
110
|
+
reportMissingTypeStubs = false
|
|
111
|
+
|
|
112
|
+
pythonVersion = "3.7"
|
|
113
|
+
pythonPlatform = "Linux"
|
|
114
|
+
|
|
115
|
+
executionEnvironments = [
|
|
116
|
+
{ root = "src" }
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
[tool.pytest.ini_options]
|
|
120
|
+
addopts = "--cov-report xml:coverage.xml --cov src --cov-fail-under 0 --cov-append -m 'not integration'"
|
|
121
|
+
pythonpath = [
|
|
122
|
+
"src"
|
|
123
|
+
]
|
|
124
|
+
testpaths = "tests"
|
|
125
|
+
junit_family = "xunit2"
|
|
126
|
+
markers = [
|
|
127
|
+
"integration: marks as integration test",
|
|
128
|
+
"notebooks: marks as notebook test",
|
|
129
|
+
"slow: marks tests as slow",
|
|
130
|
+
"unit: fast offline tests",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
[tool.tox]
|
|
134
|
+
legacy_tox_ini = """
|
|
135
|
+
[tox]
|
|
136
|
+
envlist = py, integration, all
|
|
137
|
+
|
|
138
|
+
[testenv]
|
|
139
|
+
commands =
|
|
140
|
+
pytest -m "not integration" {posargs}
|
|
141
|
+
|
|
142
|
+
[testenv:integration]
|
|
143
|
+
commands =
|
|
144
|
+
pytest -m "integration" {posargs}
|
|
145
|
+
|
|
146
|
+
[testenv:all]
|
|
147
|
+
extras = all
|
|
148
|
+
commands =
|
|
149
|
+
pytest {posargs}
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
[tool.pylint]
|
|
153
|
+
extension-pkg-whitelist= [
|
|
154
|
+
"numpy",
|
|
155
|
+
"torch",
|
|
156
|
+
"cv2",
|
|
157
|
+
"pyodbc",
|
|
158
|
+
"pydantic",
|
|
159
|
+
"ciso8601",
|
|
160
|
+
"netcdf4",
|
|
161
|
+
"scipy"
|
|
162
|
+
]
|
|
163
|
+
ignore="CVS"
|
|
164
|
+
ignore-patterns="test.*?py,conftest.py"
|
|
165
|
+
init-hook='import sys; sys.setrecursionlimit(8 * sys.getrecursionlimit())'
|
|
166
|
+
jobs=0
|
|
167
|
+
limit-inference-results=100
|
|
168
|
+
persistent="yes"
|
|
169
|
+
suggestion-mode="yes"
|
|
170
|
+
unsafe-load-any-extension="no"
|
|
171
|
+
|
|
172
|
+
[tool.pylint.'MESSAGES CONTROL']
|
|
173
|
+
enable="c-extension-no-member"
|
|
174
|
+
disable="missing-function-docstring,global-statement,wrong-import-order,missing-class-docstring,too-many-arguments,too-many-positional-arguments"
|
|
175
|
+
|
|
176
|
+
[tool.pylint.'REPORTS']
|
|
177
|
+
evaluation="10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)"
|
|
178
|
+
output-format="text"
|
|
179
|
+
reports="no"
|
|
180
|
+
score="yes"
|
|
181
|
+
|
|
182
|
+
[tool.pylint.'REFACTORING']
|
|
183
|
+
max-nested-blocks=5
|
|
184
|
+
never-returning-functions="sys.exit"
|
|
185
|
+
|
|
186
|
+
[tool.pylint.'BASIC']
|
|
187
|
+
argument-naming-style="snake_case"
|
|
188
|
+
attr-naming-style="snake_case"
|
|
189
|
+
bad-names= [
|
|
190
|
+
"foo",
|
|
191
|
+
"bar"
|
|
192
|
+
]
|
|
193
|
+
class-attribute-naming-style="any"
|
|
194
|
+
class-naming-style="PascalCase"
|
|
195
|
+
const-naming-style="UPPER_CASE"
|
|
196
|
+
docstring-min-length=-1
|
|
197
|
+
function-naming-style="snake_case"
|
|
198
|
+
good-names= [
|
|
199
|
+
"i",
|
|
200
|
+
"j",
|
|
201
|
+
"k",
|
|
202
|
+
"ex",
|
|
203
|
+
"Run",
|
|
204
|
+
"_"
|
|
205
|
+
]
|
|
206
|
+
include-naming-hint="yes"
|
|
207
|
+
inlinevar-naming-style="any"
|
|
208
|
+
method-naming-style="snake_case"
|
|
209
|
+
module-naming-style="any"
|
|
210
|
+
no-docstring-rgx="^_"
|
|
211
|
+
property-classes="abc.abstractproperty"
|
|
212
|
+
variable-naming-style="snake_case"
|
|
213
|
+
|
|
214
|
+
[tool.pylint.'FORMAT']
|
|
215
|
+
ignore-long-lines="^\\s*(# )?.*['\"]?<?https?://\\S+>?"
|
|
216
|
+
indent-after-paren=4
|
|
217
|
+
indent-string=' '
|
|
218
|
+
max-line-length=120
|
|
219
|
+
max-module-lines=1000
|
|
220
|
+
single-line-class-stmt="no"
|
|
221
|
+
single-line-if-stmt="no"
|
|
222
|
+
|
|
223
|
+
[tool.pylint.'LOGGING']
|
|
224
|
+
logging-format-style="old"
|
|
225
|
+
logging-modules="logging"
|
|
226
|
+
|
|
227
|
+
[tool.pylint.'MISCELLANEOUS']
|
|
228
|
+
notes= [
|
|
229
|
+
"FIXME",
|
|
230
|
+
"XXX",
|
|
231
|
+
"TODO"
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
[tool.pylint.'SIMILARITIES']
|
|
235
|
+
ignore-comments="yes"
|
|
236
|
+
ignore-docstrings="yes"
|
|
237
|
+
ignore-imports="yes"
|
|
238
|
+
min-similarity-lines=7
|
|
239
|
+
|
|
240
|
+
[tool.pylint.'SPELLING']
|
|
241
|
+
max-spelling-suggestions=4
|
|
242
|
+
spelling-store-unknown-words="no"
|
|
243
|
+
|
|
244
|
+
[tool.pylint.'STRING']
|
|
245
|
+
check-str-concat-over-line-jumps="no"
|
|
246
|
+
|
|
247
|
+
[tool.pylint.'TYPECHECK']
|
|
248
|
+
contextmanager-decorators="contextlib.contextmanager"
|
|
249
|
+
generated-members="numpy.*,np.*.sql.functions,collect_list"
|
|
250
|
+
ignore-mixin-members="yes"
|
|
251
|
+
ignore-none="yes"
|
|
252
|
+
ignore-on-opaque-inference="yes"
|
|
253
|
+
ignored-classes="optparse.Values,thread._local,_thread._local,numpy,torch,swagger_client"
|
|
254
|
+
ignored-modules="numpy,torch,swagger_client,netCDF4,scipy"
|
|
255
|
+
missing-member-hint="yes"
|
|
256
|
+
missing-member-hint-distance=1
|
|
257
|
+
missing-member-max-choices=1
|
|
258
|
+
|
|
259
|
+
[tool.pylint.'VARIABLES']
|
|
260
|
+
additional-builtins="dbutils"
|
|
261
|
+
allow-global-unused-variables="yes"
|
|
262
|
+
callbacks= [
|
|
263
|
+
"cb_",
|
|
264
|
+
"_cb"
|
|
265
|
+
]
|
|
266
|
+
dummy-variables-rgx="_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_"
|
|
267
|
+
ignored-argument-names="_.*|^ignored_|^unused_"
|
|
268
|
+
init-import="no"
|
|
269
|
+
redefining-builtins-modules="six.moves,past.builtins,future.builtins,builtins,io"
|
|
270
|
+
|
|
271
|
+
[tool.pylint.'CLASSES']
|
|
272
|
+
defining-attr-methods= [
|
|
273
|
+
"__init__",
|
|
274
|
+
"__new__",
|
|
275
|
+
"setUp",
|
|
276
|
+
"__post_init__"
|
|
277
|
+
]
|
|
278
|
+
exclude-protected= [
|
|
279
|
+
"_asdict",
|
|
280
|
+
"_fields",
|
|
281
|
+
"_replace",
|
|
282
|
+
"_source",
|
|
283
|
+
"_make"
|
|
284
|
+
]
|
|
285
|
+
valid-classmethod-first-arg="cls"
|
|
286
|
+
valid-metaclass-classmethod-first-arg="cls"
|
|
287
|
+
|
|
288
|
+
[tool.pylint.'DESIGN']
|
|
289
|
+
max-args=5
|
|
290
|
+
max-attributes=7
|
|
291
|
+
max-bool-expr=5
|
|
292
|
+
max-branches=12
|
|
293
|
+
max-locals=15
|
|
294
|
+
max-parents=7
|
|
295
|
+
max-public-methods=20
|
|
296
|
+
max-returns=6
|
|
297
|
+
max-statements=50
|
|
298
|
+
min-public-methods=2
|
|
299
|
+
|
|
300
|
+
[tool.pylint.'IMPORTS']
|
|
301
|
+
allow-wildcard-with-all="no"
|
|
302
|
+
analyse-fallback-blocks="no"
|
|
303
|
+
deprecated-modules="optparse,tkinter.tix"
|
|
304
|
+
|
|
305
|
+
[tool.pylint.'EXCEPTIONS']
|
|
306
|
+
overgeneral-exceptions= [
|
|
307
|
+
"BaseException",
|
|
308
|
+
"Exception"
|
|
309
|
+
]
|
|
@@ -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
|