Commit | Line | Data |
---|---|---|
4f460481 SA |
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 | """ | |
fcec8a63 | 22 | Wrappers around the arnied varlink API. |
4f460481 | 23 | |
fcec8a63 CH |
24 | Featuring: |
25 | - Arnied: stateless class with methods as exposed in the varlink API. | |
4f460481 | 26 | |
d5d2c1d7 CH |
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 | ||
4f460481 SA |
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 | |
d5d2c1d7 | 53 | class CnfVar: |
4f460481 SA |
54 | name: str |
55 | instance: int | |
56 | data: str | |
57 | deleted: bool | |
58 | children: typing.List['CnfVar'] | |
59 | ||
d5d2c1d7 | 60 | |
4f460481 | 61 | @dataclass |
d5d2c1d7 | 62 | class ChangeCnfVar: |
4f460481 SA |
63 | chg_nr: int |
64 | name: str | |
65 | instance: int | |
66 | data: str | |
67 | result_type: int | |
68 | result_msg: str | |
69 | ||
d5d2c1d7 | 70 | |
4f460481 | 71 | @dataclass |
d5d2c1d7 | 72 | class GetCnfQuery: |
4f460481 SA |
73 | name: str |
74 | instance: typing.Optional[int] = None | |
75 | ||
d5d2c1d7 | 76 | |
4f460481 SA |
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 | ||
d5d2c1d7 | 88 | |
4f460481 SA |
89 | class IsScheduledOrRunningRet(SimpleNamespace): |
90 | status: ProgramStatus | |
91 | timestamp: typing.Optional[int] | |
92 | ||
d5d2c1d7 | 93 | |
4f460481 SA |
94 | class GetCnfRet(SimpleNamespace): |
95 | vars: typing.List[CnfVar] | |
96 | ||
d5d2c1d7 | 97 | |
4f460481 SA |
98 | class SetCnfRet(SimpleNamespace): |
99 | change_numbers: typing.List[int] | |
100 | ||
d5d2c1d7 | 101 | |
4f460481 SA |
102 | class SetCommitCnf(SimpleNamespace): |
103 | results: typing.List[ChangeCnfVar] | |
104 | ||
d5d2c1d7 | 105 | |
4f460481 SA |
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 | ||
d5d2c1d7 | 117 | |
4f460481 SA |
118 | class EmptyInputError(Exception): |
119 | def __init__(self): | |
120 | super().__init__("An error occurred (EmptyInputError)") | |
121 | ||
d5d2c1d7 | 122 | |
4f460481 SA |
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 | ||
d5d2c1d7 | 128 | |
4f460481 SA |
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 | ||
d5d2c1d7 | 134 | |
4f460481 SA |
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}") | |
d5d2c1d7 CH |
145 | super().__init__("Error committing cnfvars:\n" + "\n".join(msgs)) |
146 | ||
4f460481 SA |
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 | ||
d5d2c1d7 | 153 | |
4f460481 SA |
154 | class SchedulerProgError(Exception): |
155 | def __init__(self): | |
156 | super().__init__("An error occurred (SchedulerProgError)") | |
157 | ||
d5d2c1d7 | 158 | |
4f460481 SA |
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 | ||
d5d2c1d7 | 164 | |
4f460481 SA |
165 | class ShowerrVarMissing(Exception): |
166 | def __init__(self, params_count: int) -> None: | |
d5d2c1d7 CH |
167 | super().__init__( |
168 | f"[ShowerrVarMissing] Error in the arnied API (params_count={params_count})") | |
4f460481 SA |
169 | self.params_count = params_count |
170 | ||
d5d2c1d7 | 171 | |
4f460481 SA |
172 | class ProviderNotFound(Exception): |
173 | def __init__(self, provider_id: int) -> None: | |
d5d2c1d7 CH |
174 | super().__init__( |
175 | f"[ProviderNotFound] Could not find provider (provider_id={provider_id})") | |
4f460481 SA |
176 | self.provider_id = provider_id |
177 | ||
178 | ||
179 | # Arnied varlink proxy | |
180 | ||
181 | ||
d5d2c1d7 CH |
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 | """ | |
4f460481 SA |
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 | |
d5d2c1d7 CH |
272 | def set_commit_cnf(cls, vars: typing.List[CnfVar], username: typing.Optional[str], |
273 | nogenerate: bool, fix_commit: bool) -> SetCommitCnf: | |
4f460481 SA |
274 | with cls.new_connection() as conn: |
275 | return conn.SetCommitCnf(vars, username, nogenerate, fix_commit) | |
276 | ||
277 | @classmethod | |
d5d2c1d7 CH |
278 | def commit_cnf(cls, change_numbers: typing.List[int], username: typing.Optional[str], |
279 | nogenerate: bool, fix_commit: bool) -> CommitCnfRet: | |
4f460481 SA |
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) |