gnetclisdk 1.0.100__py3-none-any.whl → 1.1.0__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.
Potentially problematic release.
This version of gnetclisdk might be problematic. Click here for more details.
- gnetclisdk/client.py +3 -1
- gnetclisdk/config.py +54 -0
- gnetclisdk/exceptions.py +14 -3
- gnetclisdk/starter.py +102 -0
- {gnetclisdk-1.0.100.dist-info → gnetclisdk-1.1.0.dist-info}/METADATA +2 -1
- gnetclisdk-1.1.0.dist-info/RECORD +13 -0
- gnetclisdk-1.0.100.dist-info/RECORD +0 -11
- {gnetclisdk-1.0.100.dist-info → gnetclisdk-1.1.0.dist-info}/WHEEL +0 -0
- {gnetclisdk-1.0.100.dist-info → gnetclisdk-1.1.0.dist-info}/top_level.txt +0 -0
gnetclisdk/client.py
CHANGED
|
@@ -14,7 +14,7 @@ from google.protobuf.message import Message
|
|
|
14
14
|
|
|
15
15
|
from .proto import server_pb2, server_pb2_grpc
|
|
16
16
|
from .auth import BasicClientAuthentication, ClientAuthentication, OAuthClientAuthentication
|
|
17
|
-
from .exceptions import parse_grpc_error
|
|
17
|
+
from .exceptions import parse_grpc_error, DeviceConnectError
|
|
18
18
|
from .interceptors import get_auth_client_interceptors
|
|
19
19
|
|
|
20
20
|
_logger = logging.getLogger(__name__)
|
|
@@ -470,6 +470,8 @@ async def grpc_call_wrapper(stub: grpc.UnaryUnaryMultiCallable, request: Any) ->
|
|
|
470
470
|
verbose=verbose,
|
|
471
471
|
)
|
|
472
472
|
last_exc.__cause__ = e
|
|
473
|
+
if gn_exc == DeviceConnectError:
|
|
474
|
+
continue
|
|
473
475
|
raise last_exc from None
|
|
474
476
|
else:
|
|
475
477
|
last_exc = None
|
gnetclisdk/config.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from dataclasses import dataclass, asdict, field
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class LogConfig:
|
|
9
|
+
level: str = "info"
|
|
10
|
+
json: bool = False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class AuthAppConfig:
|
|
15
|
+
login: str = ""
|
|
16
|
+
password: str = ""
|
|
17
|
+
private_key: str = "" # path to private key file
|
|
18
|
+
proxy_jump: str = ""
|
|
19
|
+
use_agent: bool = False
|
|
20
|
+
ssh_config: bool = False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Config:
|
|
25
|
+
logging: LogConfig = field(default_factory=LogConfig)
|
|
26
|
+
port: str = "" # Listen address
|
|
27
|
+
http_port: str = "" # Http listen address
|
|
28
|
+
dev_auth: AuthAppConfig = field(default_factory=AuthAppConfig)
|
|
29
|
+
dev_conf: str = "" # Path to yaml with device types
|
|
30
|
+
tls: bool = False
|
|
31
|
+
cert_file: str = ""
|
|
32
|
+
key_file: str = ""
|
|
33
|
+
basic_auth: str = ""
|
|
34
|
+
disable_tcp: bool = False
|
|
35
|
+
unix_socket: str = "" # Unix socket pat
|
|
36
|
+
default_read_timeout: timedelta = timedelta(seconds=0)
|
|
37
|
+
default_cmd_timeout: timedelta = timedelta(seconds=0)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def dict_factory(data):
|
|
41
|
+
return {
|
|
42
|
+
key: (
|
|
43
|
+
f"{value.total_seconds()}s"
|
|
44
|
+
if isinstance(value, timedelta)
|
|
45
|
+
else value
|
|
46
|
+
)
|
|
47
|
+
for key, value in data
|
|
48
|
+
if value != "" # skip empty strings
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def config_to_yaml(cfg: Config) -> str:
|
|
53
|
+
cfg_dict = asdict(cfg, dict_factory=dict_factory)
|
|
54
|
+
return yaml.safe_dump(cfg_dict, sort_keys=False)
|
gnetclisdk/exceptions.py
CHANGED
|
@@ -33,7 +33,7 @@ class GnetcliException(Exception):
|
|
|
33
33
|
super().__init__(self.message)
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
class
|
|
36
|
+
class DeviceConnectionError(GnetcliException):
|
|
37
37
|
"""
|
|
38
38
|
Problem with connection to a device.
|
|
39
39
|
"""
|
|
@@ -41,6 +41,14 @@ class DeviceConnectError(GnetcliException):
|
|
|
41
41
|
pass
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
class DeviceConnectError(GnetcliException):
|
|
45
|
+
"""
|
|
46
|
+
Problem with connecting to a device.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
44
52
|
class UnknownDevice(GnetcliException):
|
|
45
53
|
"""
|
|
46
54
|
Host is not found in inventory
|
|
@@ -49,7 +57,7 @@ class UnknownDevice(GnetcliException):
|
|
|
49
57
|
pass
|
|
50
58
|
|
|
51
59
|
|
|
52
|
-
class DeviceAuthError(
|
|
60
|
+
class DeviceAuthError(DeviceConnectionError):
|
|
53
61
|
"""
|
|
54
62
|
Unable to authenticate on a device.
|
|
55
63
|
"""
|
|
@@ -140,9 +148,12 @@ def parse_grpc_error(grpc_error: grpc.aio.AioRpcError) -> Tuple[Type[GnetcliExce
|
|
|
140
148
|
return DeviceAuthError, verbose
|
|
141
149
|
elif detail in {"connection_error", "busy_error"}:
|
|
142
150
|
verbose = ""
|
|
143
|
-
return
|
|
151
|
+
return DeviceConnectionError, verbose
|
|
144
152
|
elif detail in {"exec_error", "generic_error"}:
|
|
145
153
|
verbose = ""
|
|
146
154
|
return ExecError, verbose
|
|
155
|
+
elif detail == "connect_error":
|
|
156
|
+
verbose = str(error_info)
|
|
157
|
+
return DeviceConnectError, verbose
|
|
147
158
|
|
|
148
159
|
return GnetcliException, ""
|
gnetclisdk/starter.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from asyncio.subprocess import Process
|
|
5
|
+
from json import JSONDecodeError
|
|
6
|
+
from subprocess import DEVNULL, PIPE, TimeoutExpired
|
|
7
|
+
|
|
8
|
+
from gnetclisdk.config import Config, LogConfig, AuthAppConfig, config_to_yaml
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
DEFAULT_GNETCLI_SERVER_CONF = Config(
|
|
13
|
+
logging=LogConfig(level="debug", json=True),
|
|
14
|
+
dev_auth=AuthAppConfig(use_agent=True, ssh_config=True),
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GnetcliStarter:
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
server_path: str,
|
|
22
|
+
server_conf: Config = DEFAULT_GNETCLI_SERVER_CONF,
|
|
23
|
+
start_timeout: int = 5,
|
|
24
|
+
stop_timeout: int = 10,
|
|
25
|
+
):
|
|
26
|
+
self._server_path = server_path
|
|
27
|
+
self._server_conf = server_conf
|
|
28
|
+
self._running = False
|
|
29
|
+
self._proc: Process | None = None
|
|
30
|
+
self._start_timeout = start_timeout
|
|
31
|
+
self._stop_timeout = stop_timeout
|
|
32
|
+
|
|
33
|
+
async def _start(self) -> Process:
|
|
34
|
+
logger.debug("Starting Gnetcli server: %s", self._server_path)
|
|
35
|
+
proc = await asyncio.create_subprocess_exec(
|
|
36
|
+
self._server_path,
|
|
37
|
+
"--conf-file",
|
|
38
|
+
"-",
|
|
39
|
+
stdout=DEVNULL, # we do not read stdout
|
|
40
|
+
stderr=PIPE,
|
|
41
|
+
stdin=PIPE,
|
|
42
|
+
)
|
|
43
|
+
self._running = True
|
|
44
|
+
conf_yaml = config_to_yaml(self._server_conf)
|
|
45
|
+
proc.stdin.write(conf_yaml.encode())
|
|
46
|
+
await proc.stdin.drain()
|
|
47
|
+
proc.stdin.close()
|
|
48
|
+
return proc
|
|
49
|
+
|
|
50
|
+
async def _wait_url(self) -> str:
|
|
51
|
+
while proc := self._proc:
|
|
52
|
+
output = await proc.stderr.readline()
|
|
53
|
+
if output == "" and proc.returncode is not None:
|
|
54
|
+
break
|
|
55
|
+
if not output:
|
|
56
|
+
continue
|
|
57
|
+
logger.debug("gnetcli output: %s", output.strip())
|
|
58
|
+
try:
|
|
59
|
+
data = json.loads(output)
|
|
60
|
+
except JSONDecodeError:
|
|
61
|
+
logger.error("cannot decode data")
|
|
62
|
+
continue
|
|
63
|
+
if data.get("msg") == "init tcp socket":
|
|
64
|
+
logger.debug("Tcp socket found")
|
|
65
|
+
return data.get("address")
|
|
66
|
+
if data.get("msg") == "init unix socket":
|
|
67
|
+
logger.debug("Unix socket found")
|
|
68
|
+
return "unix:" + data.get("path")
|
|
69
|
+
if data.get("level") == "panic":
|
|
70
|
+
logger.error("gnetcli error %s", data)
|
|
71
|
+
return "" # stopped
|
|
72
|
+
|
|
73
|
+
async def __aenter__(self) -> str:
|
|
74
|
+
self._proc = await self._start()
|
|
75
|
+
try:
|
|
76
|
+
return await asyncio.wait_for(
|
|
77
|
+
self._wait_url(), timeout=self._start_timeout
|
|
78
|
+
)
|
|
79
|
+
except asyncio.TimeoutError:
|
|
80
|
+
logger.error("gnetcli _wait_url timeout, terminating")
|
|
81
|
+
await self._terminate()
|
|
82
|
+
raise RuntimeError("gnetcli start failed")
|
|
83
|
+
|
|
84
|
+
async def _terminate(self) -> None:
|
|
85
|
+
if (proc := self._proc) is None:
|
|
86
|
+
return
|
|
87
|
+
if proc.returncode is not None:
|
|
88
|
+
logger.error(
|
|
89
|
+
"gnetcli already terminated with code: %s", proc.returncode
|
|
90
|
+
)
|
|
91
|
+
return
|
|
92
|
+
proc.terminate()
|
|
93
|
+
try:
|
|
94
|
+
await asyncio.wait_for(proc.wait(), timeout=self._stop_timeout)
|
|
95
|
+
except TimeoutExpired:
|
|
96
|
+
logger.debug("gnetcli terminate failed, killing")
|
|
97
|
+
self._proc.kill()
|
|
98
|
+
self._proc = None
|
|
99
|
+
|
|
100
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
101
|
+
self.running = False
|
|
102
|
+
await self._terminate()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gnetclisdk
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: Client for Gnetcli GRPC-server
|
|
5
5
|
Home-page: https://github.com/annetutil/gnetcli
|
|
6
6
|
Author: Alexander Balezin
|
|
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
Requires-Dist: protobuf>=4.24.4
|
|
17
17
|
Requires-Dist: grpcio>=1.59.2
|
|
18
18
|
Requires-Dist: googleapis-common-protos>=1.61.0
|
|
19
|
+
Requires-Dist: pyyaml
|
|
19
20
|
Dynamic: author
|
|
20
21
|
Dynamic: author-email
|
|
21
22
|
Dynamic: classifier
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
gnetclisdk/auth.py,sha256=GwM7H7Ecb-gwqUTkQorifNB_mtnZfgeS46gOW2Vx1U4,1246
|
|
2
|
+
gnetclisdk/client.py,sha256=5LEIvHqWk2EVSw4xsaVWPOFahPAiuMObMe2bb9YStu8,19806
|
|
3
|
+
gnetclisdk/config.py,sha256=8Zj9oJdZXE27fMHQy2DBdC6KXdL8flDQ2w3fgc7xXNs,1364
|
|
4
|
+
gnetclisdk/exceptions.py,sha256=Cg94IdDAzQ9Q56Tdif6ladV1IcdBUg0TPa54OBVhJD0,4038
|
|
5
|
+
gnetclisdk/interceptors.py,sha256=apj3l4lnR2ZcsA49odptrBC0kTDmP6Mp0EzYkeEJz9Y,7010
|
|
6
|
+
gnetclisdk/starter.py,sha256=h3Qd6STKGfoobmxSUf2R8DdOqqd7HA4HDcTyEj0eygM,3432
|
|
7
|
+
gnetclisdk/proto/server_pb2.py,sha256=v_A3rRjuWKa7rPOrsAPOl_oLmfD4sqT5ELpAyM3cUI0,8126
|
|
8
|
+
gnetclisdk/proto/server_pb2.pyi,sha256=66Oq_YbkGgEQt9ea-P64H58hiUZoMhqptlMQdl2IAaw,8402
|
|
9
|
+
gnetclisdk/proto/server_pb2_grpc.py,sha256=H_CmevWcjvGTvp4TmzEV-TJRPq2SsQ6UgoCDc-hosyo,12805
|
|
10
|
+
gnetclisdk-1.1.0.dist-info/METADATA,sha256=QPnFLuTdBzkK3SfYS6LbxVTQ_3bXHQYh6RdehItUUlY,1814
|
|
11
|
+
gnetclisdk-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
gnetclisdk-1.1.0.dist-info/top_level.txt,sha256=MNjS8LEt6d2rZ-dUbV2cnqkuTMu3EqEL2eiSvUZuUlA,11
|
|
13
|
+
gnetclisdk-1.1.0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
gnetclisdk/auth.py,sha256=GwM7H7Ecb-gwqUTkQorifNB_mtnZfgeS46gOW2Vx1U4,1246
|
|
2
|
-
gnetclisdk/client.py,sha256=BGLamlnThZkx1NdojblX_iTTRtmhyIcuGpFUe_2bo-4,19716
|
|
3
|
-
gnetclisdk/exceptions.py,sha256=FVaj3X5163yPFsglfsw3MaZ9r3OE_Cih9N7EL1NTGgo,3791
|
|
4
|
-
gnetclisdk/interceptors.py,sha256=apj3l4lnR2ZcsA49odptrBC0kTDmP6Mp0EzYkeEJz9Y,7010
|
|
5
|
-
gnetclisdk/proto/server_pb2.py,sha256=v_A3rRjuWKa7rPOrsAPOl_oLmfD4sqT5ELpAyM3cUI0,8126
|
|
6
|
-
gnetclisdk/proto/server_pb2.pyi,sha256=66Oq_YbkGgEQt9ea-P64H58hiUZoMhqptlMQdl2IAaw,8402
|
|
7
|
-
gnetclisdk/proto/server_pb2_grpc.py,sha256=H_CmevWcjvGTvp4TmzEV-TJRPq2SsQ6UgoCDc-hosyo,12805
|
|
8
|
-
gnetclisdk-1.0.100.dist-info/METADATA,sha256=eQdSPuAcYAWUtC3Cwrum7gNen7X5j2GiWOF9xyIrkDE,1794
|
|
9
|
-
gnetclisdk-1.0.100.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
gnetclisdk-1.0.100.dist-info/top_level.txt,sha256=MNjS8LEt6d2rZ-dUbV2cnqkuTMu3EqEL2eiSvUZuUlA,11
|
|
11
|
-
gnetclisdk-1.0.100.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|