Review: a few fixes
[pyi2ncommon] / src / arnied_api.py
1 # The software in this package is distributed under the GNU General
2 # Public License version 2 (with a special exception described below).
3 #
4 # A copy of GNU General Public License (GPL) is included in this distribution,
5 # in the file COPYING.GPL.
6 #
7 # As a special exception, if other files instantiate templates or use macros
8 # or inline functions from this file, or you compile this file and link it
9 # with other works to produce a work based on this file, this file
10 # does not by itself cause the resulting work to be covered
11 # by the GNU General Public License.
12 #
13 # However the source code for this file must still be made available
14 # in accordance with section (3) of the GNU General Public License.
15 #
16 # This exception does not invalidate any other reasons why a work based
17 # on this file might be covered by the GNU General Public License.
18 #
19 # Copyright (c) 2016-2022 Intra2net AG <info@intra2net.com>
20
21 """
22 arnied_api: wrappers around the arnied varlink API.
23
24 Featuring
25 - Arnied: stateless class with methods as exposed in the varlink API.
26
27 For documentation of Exceptions, methods and their arguments, refer to arnied
28 source code (arnied/client/arnieclient.hxx). For compatibility, argument names
29 are the same here as they are there (defeating python naming rules).
30
31 See package `cnfvar` for a higher-level interface to this functionality.
32
33 .. codeauthor:: Intra2net
34 """
35
36 from contextlib import contextmanager
37 from dataclasses import dataclass
38 from types import SimpleNamespace
39 from enum import Enum, auto
40 import typing
41 import json
42 import sys
43
44
45 #: socket where the varlink interface is exposed
46 ARNIED_SOCKET = "/var/intranator/arnied-varlink.sock"
47
48
49 # Interface types
50
51
52 @dataclass
53 class CnfVar:
54     name: str
55     instance: int
56     data: str
57     deleted: bool
58     children: typing.List['CnfVar']
59
60
61 @dataclass
62 class ChangeCnfVar:
63     chg_nr: int
64     name: str
65     instance: int
66     data: str
67     result_type: int
68     result_msg: str
69
70
71 @dataclass
72 class GetCnfQuery:
73     name: str
74     instance: typing.Optional[int] = None
75
76
77 class ProgramStatus(Enum):
78     Running = auto()
79     Scheduled = auto()
80     NotScheduled = auto()
81
82
83 # Method return types (not present in the interface)
84
85 class IsQueueActiveRet(SimpleNamespace):
86     active: bool
87
88
89 class IsScheduledOrRunningRet(SimpleNamespace):
90     status: ProgramStatus
91     timestamp: typing.Optional[int]
92
93
94 class GetCnfRet(SimpleNamespace):
95     vars: typing.List[CnfVar]
96
97
98 class SetCnfRet(SimpleNamespace):
99     change_numbers: typing.List[int]
100
101
102 class SetCommitCnf(SimpleNamespace):
103     results: typing.List[ChangeCnfVar]
104
105
106 class CommitCnfRet(SimpleNamespace):
107     results: typing.List[ChangeCnfVar]
108
109
110 # Exceptions
111
112
113 class InternalBridgeError(Exception):
114     def __init__(self):
115         super().__init__("An error occurred (InternalBridgeError)")
116
117
118 class EmptyInputError(Exception):
119     def __init__(self):
120         super().__init__("An error occurred (EmptyInputError)")
121
122
123 class BadCharError(Exception):
124     def __init__(self, results: str) -> None:
125         super().__init__(f"[BadCharError] Error in the arnied API (results={results})")
126         self.results = results
127
128
129 class ChildError(Exception):
130     def __init__(self, results: str) -> None:
131         super().__init__(f"[ChildError] Error in the arnied API (results={results})")
132         self.results = results
133
134
135 class CnfCommitError(Exception):
136     def __init__(self, results: typing.List[ChangeCnfVar]) -> None:
137         self.results = []
138         msgs = []
139         for r in results:
140             # the type does not seem to be converted correctly
141             if isinstance(r, dict):
142                 r = ChangeCnfVar(**r)
143             self.results.append(r)
144             msgs.append(f"{r.name},{r.instance}: \"{r.data}\": {r.result_msg}")
145         super().__init__("Error committing cnfvars:\n" + "\n".join(msgs))
146
147
148 class NotFoundError(Exception):
149     def __init__(self, results: str) -> None:
150         super().__init__(f"[NotFoundError] Error in the arnied API (results={results})")
151         self.results = results
152
153
154 class SchedulerProgError(Exception):
155     def __init__(self):
156         super().__init__("An error occurred (SchedulerProgError)")
157
158
159 class ShowerrVarDataErr(Exception):
160     def __init__(self, msg_id: int) -> None:
161         super().__init__(f"[ShowerrVarDataErr] Error in the arnied API (msg_id={msg_id})")
162         self.msg_id = msg_id
163
164
165 class ShowerrVarMissing(Exception):
166     def __init__(self, params_count: int) -> None:
167         super().__init__(
168             f"[ShowerrVarMissing] Error in the arnied API (params_count={params_count})")
169         self.params_count = params_count
170
171
172 class ProviderNotFound(Exception):
173     def __init__(self, provider_id: int) -> None:
174         super().__init__(
175             f"[ProviderNotFound] Could not find provider (provider_id={provider_id})")
176         self.provider_id = provider_id
177
178
179 # Arnied varlink proxy
180
181
182 class Arnied:
183     """
184     Expose methods of the arnied varlink interface in python.
185
186     As described in module doc, documentation of exceptions, methods, and
187     their arguments can be found in arnied source code.
188     """
189     VARLINK_ADDRESS = f"unix:{ARNIED_SOCKET}"
190
191     @classmethod
192     def _send_msg_wrapper(cls, send_msg):
193         """
194         Wrap the _send_message method of the client to always send parameters.
195
196         HACK: this is as hackish as it comes, but at the moment we do not
197         have another workaround. The underlying varlink package will only
198         send a dictionary with a "parameters" key when we pass parameters,
199         however methods with nullable arguments require this key, in which
200         case the API errors out. One example is the `GetCnf` method: if
201         we want to get *all* cnfvars we should not provide it with any args,
202         but since the varlink module will not pass a dictionary with a
203         "parameters" key, it will fail. This has been reported upstream:
204         https://github.com/varlink/python/issues/10#issuecomment-1067232162
205         """
206         def _send_msg(msg):
207             data = json.loads(msg.decode("utf8"))
208             if not data.get("method", "").startswith("io.arnied"):
209                 return send_msg(msg)
210             if data.get("parameters", {}):
211                 return send_msg(msg)
212             data["parameters"] = {}
213             return send_msg(json.dumps(data).encode("utf8"))
214         return _send_msg
215
216     @classmethod
217     @contextmanager
218     def new_connection(cls):
219         # lazy import to reduce the scope of this dependency
220         import varlink
221         client = varlink.Client.new_with_address(cls.VARLINK_ADDRESS)
222         conn = client.open("io.arnied", namespaced=True)
223         try:
224             sm = conn._send_message
225             setattr(conn, "_send_message", Arnied._send_msg_wrapper(sm))
226             yield conn
227         except varlink.VarlinkError as ex:
228             # not an exception from this module
229             if not ex.error().startswith("io.arnied"):
230                 raise
231
232             # raise the corresponding exception
233             error_class_name = ex.error().split(".")[-1]
234             self_mod = sys.modules[__name__]
235             exception_class = self_mod.__dict__[error_class_name]
236             raise exception_class(**ex.parameters())
237         finally:
238             conn.close()
239             client.cleanup()
240
241     @classmethod
242     def is_queue_active(cls) -> IsQueueActiveRet:
243         with cls.new_connection() as conn:
244             return conn.IsQueueActive()
245
246     @classmethod
247     def is_scheduled_or_running(cls, prog: str) -> IsScheduledOrRunningRet:
248         with cls.new_connection() as conn:
249             return conn.IsScheduledOrRunning(prog)
250
251     @classmethod
252     def noop(cls) -> None:
253         with cls.new_connection() as conn:
254             return conn.NoOp()
255
256     @classmethod
257     def no_timeout(cls) -> None:
258         with cls.new_connection() as conn:
259             return conn.NoTimeout()
260
261     @classmethod
262     def get_cnf(cls, query: typing.Optional[GetCnfQuery]) -> GetCnfRet:
263         with cls.new_connection() as conn:
264             return conn.GetCnf(query)
265
266     @classmethod
267     def set_cnf(cls, vars: typing.List[CnfVar]) -> SetCnfRet:
268         with cls.new_connection() as conn:
269             return conn.SetCnf(vars)
270
271     @classmethod
272     def set_commit_cnf(cls, vars: typing.List[CnfVar], username: typing.Optional[str],
273                        nogenerate: bool, fix_commit: bool) -> SetCommitCnf:
274         with cls.new_connection() as conn:
275             return conn.SetCommitCnf(vars, username, nogenerate, fix_commit)
276
277     @classmethod
278     def commit_cnf(cls, change_numbers: typing.List[int], username: typing.Optional[str],
279                    nogenerate: bool, fix_commit: bool) -> CommitCnfRet:
280         with cls.new_connection() as conn:
281             return conn.CommitCnf(change_numbers, username, nogenerate, fix_commit)
282
283     @classmethod
284     def showerr_add(cls, msg_id: int, params: typing.List[str]) -> None:
285         with cls.new_connection() as conn:
286             return conn.ShowerrAdd(msg_id, params)
287
288     @classmethod
289     def now_online(cls, provider: int) -> None:
290         with cls.new_connection() as conn:
291             return conn.NowOnline(provider)
292
293     @classmethod
294     def now_offline(cls) -> None:
295         with cls.new_connection() as conn:
296             return conn.NowOffline()
297
298     @classmethod
299     def now_dialing(cls, provider: int) -> None:
300         with cls.new_connection() as conn:
301             return conn.NowDialing(provider)
302
303     @classmethod
304     def dial_taking_ages(cls) -> None:
305         with cls.new_connection() as conn:
306             return conn.DialTakingAges()
307
308     @classmethod
309     def barrier_executed(cls, barrier_nr: int) -> None:
310         with cls.new_connection() as conn:
311             return conn.BarrierExecuted(barrier_nr)