gnetclisdk 0.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.
gnetclisdk/auth.py ADDED
@@ -0,0 +1,41 @@
1
+ import abc
2
+
3
+
4
+ class ClientAuthentication(abc.ABC):
5
+ @abc.abstractmethod
6
+ def get_authentication_header_key(self) -> str:
7
+ """
8
+ Name of the header used for authentication.
9
+ :return: Header name.
10
+ """
11
+ raise NotImplementedError("abstract method get_authentication_header_key() not implemented")
12
+
13
+ @abc.abstractmethod
14
+ def create_authentication_header_value(self) -> str:
15
+ """
16
+ Creates value for authentication header.
17
+ :return: Authentication header value.
18
+ """
19
+ raise NotImplementedError("abstract method create_authentication_header_value() not implemented")
20
+
21
+
22
+ class OAuthClientAuthentication(ClientAuthentication):
23
+ def __init__(self, token: str):
24
+ self.__token = token
25
+
26
+ def get_authentication_header_key(self) -> str:
27
+ return "authorization"
28
+
29
+ def create_authentication_header_value(self) -> str:
30
+ return f"OAuth {self.__token}"
31
+
32
+
33
+ class BasicClientAuthentication(ClientAuthentication):
34
+ def __init__(self, token: str):
35
+ self.__token = token
36
+
37
+ def get_authentication_header_key(self) -> str:
38
+ return "authorization"
39
+
40
+ def create_authentication_header_value(self) -> str:
41
+ return f"Basic {self.__token}"
gnetclisdk/client.py ADDED
@@ -0,0 +1,481 @@
1
+ import asyncio
2
+ import logging
3
+ import os.path
4
+ import uuid
5
+ from abc import ABC, abstractmethod
6
+ from contextlib import asynccontextmanager
7
+ from dataclasses import dataclass, field
8
+ from functools import partial
9
+ from typing import Any, AsyncIterator, List, Optional, Tuple, Dict
10
+
11
+ import grpc
12
+ from google.protobuf.message import Message
13
+
14
+ from .proto import server_pb2, server_pb2_grpc
15
+ from .auth import BasicClientAuthentication, ClientAuthentication, OAuthClientAuthentication
16
+ from .exceptions import parse_grpc_error
17
+ from .interceptors import get_auth_client_interceptors
18
+
19
+ _logger = logging.getLogger(__name__)
20
+ HEADER_REQUEST_ID = "x-request-id"
21
+ HEADER_USER_AGENT = "user-agent"
22
+ DEFAULT_USER_AGENT = "Gnetcli SDK"
23
+ DEFAULT_SERVER = "localhost:50051"
24
+ SERVER_ENV = "GNETCLI_SERVER"
25
+ GRPC_MAX_MESSAGE_LENGTH = 130 * 1024**2
26
+
27
+ default_grpc_options: List[Tuple[str, Any]] = [
28
+ ("grpc.max_concurrent_streams", 900),
29
+ ("grpc.max_send_message_length", GRPC_MAX_MESSAGE_LENGTH),
30
+ ("grpc.max_receive_message_length", GRPC_MAX_MESSAGE_LENGTH),
31
+ ]
32
+
33
+
34
+ @dataclass
35
+ class QA:
36
+ question: str
37
+ answer: str
38
+
39
+
40
+ @dataclass
41
+ class Credentials:
42
+ login: str
43
+ password: str
44
+
45
+ def make_pb(self) -> Message:
46
+ pb = server_pb2.Credentials()
47
+ pb.login = self.login
48
+ pb.password = self.password
49
+ return pb
50
+
51
+
52
+ @dataclass
53
+ class File:
54
+ content: bytes
55
+ status: server_pb2.FileStatus
56
+
57
+
58
+ @dataclass
59
+ class HostParams:
60
+ device: str
61
+ port: Optional[int] = None
62
+ hostname: Optional[str] = None
63
+ credentials: Optional[Credentials] = None
64
+ ip: Optional[str] = None
65
+
66
+ def make_pb(self) -> Message:
67
+ pbcmd = server_pb2.HostParams(
68
+ host=self.hostname,
69
+ port=self.port,
70
+ credentials=self.credentials.make_pb(),
71
+ device=self.device,
72
+ ip=self.ip,
73
+ )
74
+ return pbcmd
75
+
76
+
77
+ def make_auth(auth_token: str) -> ClientAuthentication:
78
+ if auth_token.lower().startswith("oauth"):
79
+ authentication = OAuthClientAuthentication(auth_token.split(" ")[1])
80
+ elif auth_token.lower().startswith("basic"):
81
+ authentication = BasicClientAuthentication(auth_token.split(" ")[1])
82
+ else:
83
+ raise Exception("unknown token type")
84
+ return authentication
85
+
86
+
87
+ class Gnetcli:
88
+ def __init__(
89
+ self,
90
+ auth_token: Optional[str] = None, # like 'Basic ...'
91
+ server: Optional[str] = None,
92
+ target_name_override: Optional[str] = None,
93
+ cert_file: Optional[str] = None,
94
+ user_agent: str = DEFAULT_USER_AGENT,
95
+ insecure_grpc: bool = False,
96
+ ):
97
+ if server is None:
98
+ self._server = os.getenv(SERVER_ENV, DEFAULT_SERVER)
99
+ else:
100
+ self._server = server
101
+ self._user_agent = user_agent
102
+
103
+ options: List[Tuple[str, Any]] = [
104
+ *default_grpc_options,
105
+ ("grpc.primary_user_agent", user_agent),
106
+ ]
107
+ if target_name_override:
108
+ _logger.warning("set target_name_override %s", target_name_override)
109
+ options.append(("grpc.ssl_target_name_override", target_name_override))
110
+ self._target_name_override = target_name_override
111
+ cert = get_cert(cert_file=cert_file)
112
+ channel_credentials = grpc.ssl_channel_credentials(root_certificates=cert)
113
+ interceptors = []
114
+ if auth_token:
115
+ authentication: ClientAuthentication
116
+ authentication = make_auth(auth_token)
117
+ interceptors = get_auth_client_interceptors(authentication)
118
+ grpc_channel_fn = partial(grpc.aio.secure_channel, credentials=channel_credentials, interceptors=interceptors)
119
+ if insecure_grpc:
120
+ grpc_channel_fn = partial(grpc.aio.insecure_channel, interceptors=interceptors)
121
+ self._grpc_channel_fn = grpc_channel_fn
122
+ self._options = options
123
+ self._channel: Optional[grpc.aio.Channel] = None
124
+ self._insecure_grpc: bool = insecure_grpc
125
+
126
+ async def cmd(
127
+ self,
128
+ hostname: str,
129
+ cmd: str,
130
+ trace: bool = False,
131
+ qa: Optional[List[QA]] = None,
132
+ read_timeout: float = 0.0,
133
+ cmd_timeout: float = 0.0,
134
+ host_params: Optional[HostParams] = None,
135
+ ) -> Message:
136
+ pbcmd = make_cmd(
137
+ hostname=hostname,
138
+ cmd=cmd,
139
+ trace=trace,
140
+ qa=qa,
141
+ read_timeout=read_timeout,
142
+ cmd_timeout=cmd_timeout,
143
+ host_params=host_params,
144
+ )
145
+ if self._channel is None:
146
+ _logger.debug("connect to %s", self._server)
147
+ self._channel = self._grpc_channel_fn(self._server, options=self._options)
148
+ stub = server_pb2_grpc.GnetcliStub(self._channel)
149
+ response = await grpc_call_wrapper(stub.Exec, pbcmd)
150
+ return response
151
+
152
+ async def add_device(
153
+ self,
154
+ name: str,
155
+ prompt_expression: str,
156
+ error_expression: Optional[str] = None,
157
+ pager_expression: Optional[str] = None,
158
+ ) -> Message:
159
+ pbdev = server_pb2.Device
160
+ pbdev.name = name
161
+ pbdev.prompt_expression = prompt_expression
162
+ if error_expression:
163
+ pbdev.error_expression = error_expression
164
+ if pager_expression:
165
+ pbdev.pager_expression = pager_expression
166
+ if self._channel is None:
167
+ _logger.debug("connect to %s", self._server)
168
+ self._channel = self._grpc_channel_fn(self._server, options=self._options)
169
+ stub = server_pb2_grpc.GnetcliStub(self._channel)
170
+ response = await grpc_call_wrapper(stub.AddDevice, pbdev)
171
+ return response
172
+
173
+ def connect(self) -> None:
174
+ # make connection here will pass it to session
175
+ if not self._channel:
176
+ _logger.debug("real connect to %s", self._server)
177
+ self._channel = self._grpc_channel_fn(self._server, options=self._options)
178
+
179
+ async def cmd_netconf(self, hostname: str, cmd: str, json: bool = False, trace: bool = False) -> Message:
180
+ pbcmd = server_pb2.CMDNetconf(host=hostname, cmd=cmd, json=json, trace=trace)
181
+ _logger.debug("connect to %s", self._server)
182
+ async with self._grpc_channel_fn(self._server, options=self._options) as channel:
183
+ stub = server_pb2_grpc.GnetcliStub(channel)
184
+ _logger.debug("executing netconf cmd: %r", pbcmd)
185
+ try:
186
+ response = await grpc_call_wrapper(stub.ExecNetconf, pbcmd)
187
+ except Exception as e:
188
+ _logger.error("error hostname=%s cmd=%r error=%s", hostname, repr(pbcmd), e)
189
+ raise
190
+ return response
191
+
192
+ @asynccontextmanager
193
+ async def cmd_session(self, hostname: str) -> AsyncIterator["GnetcliSessionCmd"]:
194
+ sess = GnetcliSessionCmd(
195
+ hostname,
196
+ server=self._server,
197
+ channel=self._channel,
198
+ target_name_override=self._target_name_override,
199
+ user_agent=self._user_agent,
200
+ insecure_grpc=self._insecure_grpc,
201
+ )
202
+ await sess.connect()
203
+ try:
204
+ yield sess
205
+ finally:
206
+ await sess.close()
207
+
208
+ @asynccontextmanager
209
+ async def netconf_session(self, hostname: str) -> AsyncIterator["GnetcliSessionNetconf"]:
210
+ sess = GnetcliSessionNetconf(
211
+ hostname,
212
+ # self._token,
213
+ server=self._server,
214
+ target_name_override=self._target_name_override,
215
+ user_agent=self._user_agent,
216
+ insecure_grpc=self._insecure_grpc,
217
+ )
218
+ await sess.connect()
219
+ try:
220
+ yield sess
221
+ finally:
222
+ await sess.close()
223
+
224
+ async def set_host_params(self, hostname: str, params: HostParams) -> None:
225
+ pbcmd = server_pb2.HostParams(
226
+ host=hostname,
227
+ port=params.port,
228
+ credentials=params.credentials.make_pb(),
229
+ device=params.device)
230
+ _logger.debug("connect to %s", self._server)
231
+ async with self._grpc_channel_fn(self._server, options=self._options) as channel:
232
+ _logger.debug("set params for %s", hostname)
233
+ stub = server_pb2_grpc.GnetcliStub(channel)
234
+ await grpc_call_wrapper(stub.SetupHostParams, pbcmd)
235
+ return
236
+
237
+ async def upload(self, hostname: str, files: Dict[str, File]) -> None:
238
+ pbcmd = server_pb2.FileUploadRequest(host=hostname, files=make_files_request(files))
239
+ _logger.debug("connect to %s", self._server)
240
+ async with self._grpc_channel_fn(self._server, options=self._options) as channel:
241
+ _logger.debug("upload %s to %s", files.keys(), hostname)
242
+ stub = server_pb2_grpc.GnetcliStub(channel)
243
+ response: Message = await grpc_call_wrapper(stub.Upload, pbcmd)
244
+ _logger.debug("upload res %s", response)
245
+ return
246
+
247
+ async def download(self, hostname: str, paths: List[str]) -> Dict[str, File]:
248
+ pbcmd = server_pb2.FileDownloadRequest(host=hostname, paths=paths)
249
+ _logger.debug("connect to %s", self._server)
250
+ async with self._grpc_channel_fn(self._server, options=self._options) as channel:
251
+ _logger.debug("download %s from %s", paths, hostname)
252
+ stub = server_pb2_grpc.GnetcliStub(channel)
253
+ response: server_pb2.FilesResult = await grpc_call_wrapper(stub.Download, pbcmd)
254
+ res: Dict[str, File] = {}
255
+ for file in response.files:
256
+ res[file.path] = File(content=file.data, status=file.status)
257
+ return res
258
+
259
+
260
+ class GnetcliSession(ABC):
261
+ def __init__(
262
+ self,
263
+ hostname: str,
264
+ token: str,
265
+ server: str = DEFAULT_SERVER,
266
+ target_name_override: Optional[str] = None,
267
+ cert_file: Optional[str] = None,
268
+ user_agent: str = DEFAULT_USER_AGENT,
269
+ insecure_grpc: bool = False,
270
+ channel: Optional[grpc.aio.Channel] = None,
271
+ credentials: Optional[Credentials] = None,
272
+ ):
273
+ self._hostname = hostname
274
+ self._credentials = credentials
275
+ self._server = server
276
+ self._channel: Optional[grpc.aio.Channel] = channel
277
+ self._stub: Optional[server_pb2_grpc.GnetcliStub] = None
278
+ self._stream: Optional[grpc.aio.StreamStreamCall] = None
279
+ self._user_agent = user_agent
280
+
281
+ options: List[Tuple[str, Any]] = [
282
+ ("grpc.max_concurrent_streams", 900),
283
+ ("grpc.max_send_message_length", GRPC_MAX_MESSAGE_LENGTH),
284
+ ("grpc.max_receive_message_length", GRPC_MAX_MESSAGE_LENGTH),
285
+ ]
286
+ if target_name_override:
287
+ options.append(("grpc.ssl_target_name_override", target_name_override))
288
+ cert = get_cert(cert_file=cert_file)
289
+ channel_credentials = grpc.ssl_channel_credentials(root_certificates=cert)
290
+ authentication: ClientAuthentication
291
+ if token.startswith("OAuth"):
292
+ authentication = OAuthClientAuthentication(token.split(" ")[1])
293
+ elif token.startswith("Basic"):
294
+ authentication = BasicClientAuthentication(token.split(" ")[1])
295
+ else:
296
+ raise Exception("unknown token type")
297
+ interceptors = get_auth_client_interceptors(authentication)
298
+ grpc_channel_fn = partial(grpc.aio.secure_channel, credentials=channel_credentials, interceptors=interceptors)
299
+ if insecure_grpc:
300
+ grpc_channel_fn = partial(grpc.aio.insecure_channel, interceptors=interceptors)
301
+ self._grpc_channel_fn = grpc_channel_fn
302
+ self._options = options
303
+ self._req_id: Optional[Any] = None
304
+
305
+ def _get_metadata(self) -> List[Tuple[str, str]]:
306
+ req_id = make_req_id()
307
+ metadata = [
308
+ (HEADER_REQUEST_ID, req_id),
309
+ (HEADER_USER_AGENT, self._user_agent),
310
+ ]
311
+ return metadata
312
+
313
+ @abstractmethod
314
+ async def connect(self) -> None:
315
+ if self._channel is None:
316
+ _logger.debug("connect to %s self._channel=%s", self._server, self._channel)
317
+ self._channel = self._grpc_channel_fn(self._server, options=self._options)
318
+ self._stub = server_pb2_grpc.GnetcliStub(self._channel)
319
+ if self._stub is None:
320
+ raise Exception("empty stub")
321
+
322
+ async def _cmd(self, cmdpb: Any) -> Message:
323
+ # TODO: add connect retry on first cmd
324
+ if not self._stream:
325
+ raise Exception("empty self._stream")
326
+ try:
327
+ _logger.debug("cmd %r on %r", str(cmdpb).replace("\n", ""), self._stream)
328
+ await self._stream.write(cmdpb)
329
+ response: Message = await self._stream.read()
330
+ except grpc.aio.AioRpcError as e:
331
+ _logger.debug("caught exception %s %s", e, parse_grpc_error(e))
332
+ gn_exc, verbose = parse_grpc_error(e)
333
+ last_exc = gn_exc(
334
+ message=f"{e.__class__.__name__} {e.details()}",
335
+ imetadata=e.initial_metadata(), # type: ignore
336
+ verbose=verbose,
337
+ )
338
+ last_exc.__cause__ = e
339
+ raise last_exc from None
340
+ _logger.debug("response %s", format_long_msg(str(response), 100))
341
+ return response
342
+
343
+ async def close(self) -> None:
344
+ _logger.debug("close stream %s", self._stream)
345
+ if self._stream:
346
+ await self._stream.done_writing()
347
+ self._stream.done()
348
+ self._stream = None
349
+
350
+
351
+ class GnetcliSessionCmd(GnetcliSession):
352
+ async def cmd(
353
+ self,
354
+ cmd: str,
355
+ trace: bool = False,
356
+ qa: Optional[List[QA]] = None,
357
+ cmd_timeout: float = 0.0,
358
+ read_timeout: float = 0.0,
359
+ host_params: Optional[HostParams] = None,
360
+ ) -> Message:
361
+ _logger.debug("session cmd %r", cmd)
362
+ pbcmd = make_cmd(
363
+ hostname=self._hostname,
364
+ cmd=cmd,
365
+ trace=trace,
366
+ qa=qa,
367
+ read_timeout=read_timeout,
368
+ cmd_timeout=cmd_timeout,
369
+ host_params=host_params,
370
+ )
371
+ return await self._cmd(pbcmd)
372
+
373
+ async def connect(self) -> None:
374
+ await super(GnetcliSessionCmd, self).connect()
375
+ if self._stub:
376
+ self._stream = self._stub.ExecChat(metadata=self._get_metadata())
377
+ else:
378
+ raise Exception()
379
+
380
+
381
+ class GnetcliSessionNetconf(GnetcliSession):
382
+ async def cmd(self, cmd: str, trace: bool = False, json: bool = False) -> Message:
383
+ _logger.debug("netconf session cmd %r", cmd)
384
+ cmdpb = server_pb2.CMDNetconf(host=self._hostname, cmd=cmd, json=json)
385
+ return await self._cmd(cmdpb)
386
+
387
+ async def connect(self) -> None:
388
+ await super(GnetcliSessionNetconf, self).connect()
389
+ if self._stub:
390
+ self._stream = self._stub.ExecNetconfChat(metadata=self._get_metadata())
391
+ else:
392
+ raise Exception()
393
+
394
+
395
+ async def grpc_call_wrapper(stub: grpc.UnaryUnaryMultiCallable, request: Any) -> Message:
396
+ last_exc: Optional[Exception] = None
397
+ response: Optional[Message] = None
398
+ for i in range(5):
399
+ req_id = make_req_id()
400
+ metadata = [
401
+ (HEADER_REQUEST_ID, req_id),
402
+ ]
403
+ _logger.debug("executing %s: %r, req_id=%s", type(request), repr(request), req_id)
404
+ await asyncio.sleep(i * 2)
405
+ try:
406
+ response = await stub(request=request, metadata=metadata)
407
+ except grpc.aio.AioRpcError as e:
408
+ _logger.debug("caught exception %s req_id=%s %s", e, req_id, parse_grpc_error(e))
409
+ gn_exc, verbose = parse_grpc_error(e)
410
+ last_exc = gn_exc(
411
+ message=f"{e.__class__.__name__} {e.details()}",
412
+ imetadata=e.initial_metadata(), # type: ignore
413
+ request_id=req_id,
414
+ verbose=verbose,
415
+ )
416
+ last_exc.__cause__ = e
417
+ raise last_exc from None
418
+ else:
419
+ last_exc = None
420
+ break
421
+
422
+ if last_exc is not None:
423
+ raise last_exc
424
+ if response is None:
425
+ raise Exception()
426
+ else:
427
+ return response
428
+
429
+
430
+ def make_req_id() -> str:
431
+ return str(uuid.uuid4())
432
+
433
+
434
+ def get_cert(cert_file: Optional[str]) -> Optional[bytes]:
435
+ cert: Optional[bytes] = None
436
+ if cert_file:
437
+ _logger.debug("open cert_file %s", cert_file)
438
+ with open(cert_file, "rb") as f:
439
+ cert = f.read()
440
+ return cert
441
+
442
+
443
+ def format_long_msg(msg: str, max_len: int) -> str:
444
+ if len(msg) <= max_len:
445
+ return msg
446
+ return "%s... and %s more" % (msg[:max_len], len(msg) - max_len)
447
+
448
+
449
+ def make_cmd(
450
+ hostname: str,
451
+ cmd: str,
452
+ trace: bool = False,
453
+ qa: Optional[List[QA]] = None,
454
+ read_timeout: float = 0.0,
455
+ cmd_timeout: float = 0.0,
456
+ host_params: Optional[HostParams] = None,
457
+ ) -> Message:
458
+ qa_cmd: List[Message] = []
459
+ if qa:
460
+ for item in qa:
461
+ qaitem = server_pb2.QA()
462
+ qaitem.question = item.question
463
+ qaitem.answer = item.answer
464
+ qa_cmd.append(qaitem)
465
+ res = server_pb2.CMD(
466
+ host=hostname,
467
+ cmd=cmd,
468
+ trace=trace,
469
+ qa=qa_cmd,
470
+ read_timeout=read_timeout,
471
+ cmd_timeout=cmd_timeout,
472
+ host_params=host_params.make_pb(),
473
+ )
474
+ return res # type: ignore
475
+
476
+
477
+ def make_files_request(files: Dict[str, File]) -> List[server_pb2.FileData]:
478
+ res: List[server_pb2.FileData] = []
479
+ for path, file in files.items():
480
+ res.append(server_pb2.FileData(path=path, data=file.content))
481
+ return res
@@ -0,0 +1,116 @@
1
+ from typing import Optional, Sequence, Tuple, Type, Union
2
+
3
+ import grpc.aio
4
+
5
+ MetadataType = Sequence[Tuple[str, Union[str, bytes]]]
6
+
7
+
8
+ def extract_metadata(m: MetadataType) -> dict:
9
+ # calling get from metadataType throws KeyError
10
+ metadata = {}
11
+ for k, v in m:
12
+ metadata[k] = v
13
+ return metadata
14
+
15
+
16
+ class GnetcliException(Exception):
17
+ def __init__(
18
+ self,
19
+ message: str = "",
20
+ imetadata: Optional[MetadataType] = None,
21
+ request_id: Optional[str] = None,
22
+ verbose: Optional[str] = "",
23
+ ):
24
+ self.message = message
25
+ if imetadata:
26
+ rs = extract_metadata(imetadata).get("real-server")
27
+ if rs:
28
+ self.message = f"{self.message} RS:{rs}"
29
+ if request_id:
30
+ self.message = f"{self.message} req_id:{request_id}"
31
+ if verbose:
32
+ self.message = f"{self.message} verbose:{verbose}"
33
+ super().__init__(self.message)
34
+
35
+
36
+ class DeviceConnectError(GnetcliException):
37
+ """
38
+ Problem with connection to a device.
39
+ """
40
+
41
+ pass
42
+
43
+
44
+ class UnknownDevice(GnetcliException):
45
+ """
46
+ Host is not found in inventory
47
+ """
48
+
49
+ pass
50
+
51
+
52
+ class DeviceAuthError(DeviceConnectError):
53
+ """
54
+ Unable to authenticate on a device.
55
+ """
56
+
57
+ pass
58
+
59
+
60
+ class ExecError(GnetcliException):
61
+ """
62
+ Error happened during execution.
63
+ """
64
+
65
+ pass
66
+
67
+
68
+ class NotReady(GnetcliException):
69
+ """
70
+ Server is not ready.
71
+ """
72
+
73
+ pass
74
+
75
+
76
+ class Unauthenticated(GnetcliException):
77
+ """
78
+ Unable to authenticate on Gnetcli server.
79
+ """
80
+
81
+ pass
82
+
83
+
84
+ class PermissionDenied(GnetcliException):
85
+ """
86
+ Permission denied.
87
+ """
88
+
89
+ pass
90
+
91
+
92
+ def parse_grpc_error(grpc_error: grpc.aio.AioRpcError) -> Tuple[Type[GnetcliException], str]:
93
+ code = grpc_error.code()
94
+ detail = ""
95
+ if grpc_error.details():
96
+ detail = grpc_error.details() # type: ignore
97
+ if code == grpc.StatusCode.UNAVAILABLE and detail == "not ready":
98
+ return NotReady, ""
99
+ if code == grpc.StatusCode.UNAUTHENTICATED:
100
+ return Unauthenticated, detail
101
+ if code == grpc.StatusCode.PERMISSION_DENIED:
102
+ return PermissionDenied, detail
103
+ if code == grpc.StatusCode.OUT_OF_RANGE:
104
+ return UnknownDevice, detail
105
+ if code == grpc.StatusCode.INTERNAL:
106
+ if detail == "auth_device_error":
107
+ verbose = ""
108
+ return DeviceAuthError, verbose
109
+ if detail in {"connection_error", "busy_error"}:
110
+ verbose = ""
111
+ return DeviceConnectError, verbose
112
+ elif detail in {"exec_error", "generic_error"}:
113
+ verbose = ""
114
+ return ExecError, verbose
115
+
116
+ return GnetcliException, ""