Merge branch 'api-doc-improvements'
[pyi2ncommon] / src / arnied_api.py
CommitLineData
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 22Wrappers around the arnied varlink API.
4f460481 23
fcec8a63
CH
24Featuring:
25 - Arnied: stateless class with methods as exposed in the varlink API.
4f460481 26
d5d2c1d7
CH
27For documentation of Exceptions, methods and their arguments, refer to arnied
28source code (arnied/client/arnieclient.hxx). For compatibility, argument names
29are the same here as they are there (defeating python naming rules).
30
31See package `cnfvar` for a higher-level interface to this functionality.
32
4f460481
SA
33.. codeauthor:: Intra2net
34"""
35
36from contextlib import contextmanager
37from dataclasses import dataclass
38from types import SimpleNamespace
39from enum import Enum, auto
40import typing
41import json
42import sys
43
44
45#: socket where the varlink interface is exposed
46ARNIED_SOCKET = "/var/intranator/arnied-varlink.sock"
47
48
49# Interface types
50
51
52@dataclass
d5d2c1d7 53class 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 62class 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 72class GetCnfQuery:
4f460481
SA
73 name: str
74 instance: typing.Optional[int] = None
75
d5d2c1d7 76
4f460481
SA
77class ProgramStatus(Enum):
78 Running = auto()
79 Scheduled = auto()
80 NotScheduled = auto()
81
82
83# Method return types (not present in the interface)
84
85class IsQueueActiveRet(SimpleNamespace):
86 active: bool
87
d5d2c1d7 88
4f460481
SA
89class IsScheduledOrRunningRet(SimpleNamespace):
90 status: ProgramStatus
91 timestamp: typing.Optional[int]
92
d5d2c1d7 93
4f460481
SA
94class GetCnfRet(SimpleNamespace):
95 vars: typing.List[CnfVar]
96
d5d2c1d7 97
4f460481
SA
98class SetCnfRet(SimpleNamespace):
99 change_numbers: typing.List[int]
100
d5d2c1d7 101
4f460481
SA
102class SetCommitCnf(SimpleNamespace):
103 results: typing.List[ChangeCnfVar]
104
d5d2c1d7 105
4f460481
SA
106class CommitCnfRet(SimpleNamespace):
107 results: typing.List[ChangeCnfVar]
108
109
110# Exceptions
111
112
113class InternalBridgeError(Exception):
114 def __init__(self):
115 super().__init__("An error occurred (InternalBridgeError)")
116
d5d2c1d7 117
4f460481
SA
118class EmptyInputError(Exception):
119 def __init__(self):
120 super().__init__("An error occurred (EmptyInputError)")
121
d5d2c1d7 122
4f460481
SA
123class 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
129class 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
135class 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
148class 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
154class SchedulerProgError(Exception):
155 def __init__(self):
156 super().__init__("An error occurred (SchedulerProgError)")
157
d5d2c1d7 158
4f460481
SA
159class 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
165class 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
172class 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
182class 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)