1 # The software in this package is distributed under the GNU General
2 # Public License version 2 (with a special exception described below).
4 # A copy of GNU General Public License (GPL) is included in this distribution,
5 # in the file COPYING.GPL.
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.
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.
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.
19 # Copyright (c) 2016-2022 Intra2net AG <info@intra2net.com>
22 store: implementations of CNF stores using varlink and *et_cnf binaries.
25 - CnfStore: the main store class that is implemented using the varlink API
27 - BinaryCnfStore: alternative store class implementation using the set_ and
30 .. codeauthor:: Intra2net
37 from .. import arnied_wrapper, arnied_api
38 from . import CnfBinary, Cnf, CnfList
40 log = logging.getLogger("pyi2ncommon.cnfvar.store")
44 """Main CNF store class that uses the varlink API."""
46 def __init__(self, backend_driver=arnied_api.Arnied):
48 Initialize this class.
50 :param backend_driver: driver to use to talk to the cnfvar backend
51 :type: :py:class:`arnied_api.Arnied`
53 self._driver = backend_driver
54 log.debug("Initialized CnfStore with driver `%s`", type(backend_driver))
56 def query(self, name=None, instance=None):
58 Query the CNF store and return a list of parsed CNFs.
60 :param str name: optional name of the CNFs to query
61 :param instance: optional CNF instance
62 :type instance: str or int
63 :returns: list of parsed CNF values
64 :rtype: :py:class:`CnfList`
68 user_cnfs = store.query("USER")
70 log.debug("Querying CnfStore with name=%s and instance=%s via arnied API",
73 # the API only expects an object if there is a name (and it's not case-insensitive)
74 query = arnied_api.GetCnfQuery(name.upper(), instance) if name else None
75 api_ret = self._driver.get_cnf(query)
77 # NOTE: logging all output here would result in huge lines when querying
78 # all variables via `store.query()`
79 log.debug("Arnied API returned %d cnfvars", len(api_ret.vars))
80 return CnfList.from_api_structure(api_ret.vars)
82 def commit(self, cnf, fix_problems=False):
84 Update or insert CNF variables from a list.
86 :param cnf: CNF instance or list of CNFs to update or insert
87 :type cnf: :py:class:`Cnf` or :py:class:`CnfList`
88 :raises: :py:class:`CommitException` if the arnied API complains
89 :param bool fix_problems: whether to automatically fix errors in the vars
91 .. note:: you can mix variables to insert and variables to update
92 in the same list as the system should handle it nicely
96 user_cnf = store.query("USER")\
97 .single_with_value("joe")
98 user_cnf.add_child("user_group_member_ref", "3")
99 store.commit(user_cnf)
101 cnf = self._cnf_or_list(cnf, operation="commit")
102 self._autofix_instances(cnf)
103 log.debug("Committing variables via arnied API:\n%s", cnf)
105 arnied_cnfs = cnf.to_api_structure()
106 self._do_commit(cnf, arnied_cnfs, fix_problems=fix_problems)
108 def delete(self, cnf, fix_problems=False):
110 Delete a list of top-level CNF variables.
112 :param cnf: a single CNF value or a list of values
113 :type cnf: :py:class:`CnfList` or :py:class:`Cnf`
114 :param bool fix_problems: whether to automatically fix errors in the vars
118 user_cnfs = store.query("USER")\
119 .where(lambda c: c.value in ["joe", "jane"])
120 store.delete(user_cnfs)
122 cnf = self._cnf_or_list(cnf, operation="delete")
123 if any((c.parent is not None for c in cnf)):
124 raise RuntimeError("Calling delete is only supported on top-level CNF variables")
125 log.debug("Deleting variables via arnied API:\n%s", cnf)
127 arnied_cnfs = cnf.to_api_structure()
128 for c in arnied_cnfs:
131 self._do_commit(cnf, arnied_cnfs, fix_problems=fix_problems)
133 def _autofix_instances(self, cnfs):
135 Auto-assign a valid instance value to all top-level vars in a list.
137 :param cnfs: list of cnfvars to fix
138 :type cnfs: :py:class`CnfList`
140 When the instance of a cnfvar is -1, the backend initializes it
141 automatically. However it starts on 1, whereas many variables are not
142 allowed to start on one (those that are meant to be unique, f.e.).
143 This method can be used in child classes to use an alternative scheme,
144 however for performance reasons the base API class uses the default and
145 relies on the cnfvar backend to do this job.
148 def _do_commit(self, original_cnfs, arnied_cnfs, fix_problems=False):
150 Set cnfvars and commit changes.
152 :param original_cnfs: list of CNFs to print in case of errors
153 :type original_cnfs: :py:class:`CnfList`
154 :param arnied_cnfs: list of cnfvars to pass to the arnied API
155 :type arnied_cnfs: [:py:class:`arnied_api.CnfVar`]
156 :param bool fix_problems: whether to automatically fix errors in the vars
157 :raises: :py:class:`CommitException` if the arnied API complains
160 ret = self._driver.set_commit_cnf(vars=arnied_cnfs, username=None,
162 fix_commit=fix_problems)
163 except arnied_api.CnfCommitError as ex:
164 # fatal errors, we will handle it in our custom exception
168 for r in ret.results:
169 # message can contain HTML escape codes
170 msg = html.unescape(r.result_msg)
172 # `result_type` is defined as int in the varlink API,
173 # but the meaning of the codes are:
174 # enum result_type { OK=0, WARN=1, FAIL_TEMP=2, FAIL=3 }
175 if r.result_type == 1:
176 log.debug("Warning in `` %s,%s: \"%s\" ``: {msg} (code=%s)",
177 r.name, r.instance, r.data, r.result_type)
178 elif r.result_type > 1:
179 errors.append(f"Error in `` {r.name},{r.instance}: \"{r.data}\" ``"
183 log.debug("Error sending variables:\n%s", arnied_cnfs)
184 raise CommitException(original_cnfs, "\n".join(errors))
186 self._wait_for_generate()
188 def _wait_for_generate(self, timeout=300):
190 Wait for the 'generate' program to end.
192 :param int timeout: program run timeout
193 :raises: :py:class:`TimeoutError` if the program did not finish on time
195 def scheduled_or_running(progname):
196 ret = self._driver.is_scheduled_or_running(progname)
197 return ret.status in [arnied_api.ProgramStatus.Scheduled,
198 arnied_api.ProgramStatus.Running]
200 def wait_for_program(progname):
201 log.debug("Waiting for `%s` to be running", progname)
203 if scheduled_or_running(progname):
204 # if is running or scheduled, break to wait for completion
208 # after trying and retrying, program is not scheduled nor
209 # running, so it is safe to assume it has already executed
212 # program running or scheduled, wait
213 log.debug("Waiting for `%s` to finish", progname)
214 for _ in range(0, timeout):
215 if not scheduled_or_running(progname):
216 # finished executing, bail out
219 raise TimeoutError(f"Program `{progname}` did not end in time")
221 wait_for_program("GENERATE")
222 wait_for_program("GENERATE_OFFLINE")
224 def _cnf_or_list(self, cnf, operation) -> CnfList:
226 Validate and wrap a CNF value into a list.
228 :param cnf: a single CNF value or a list of values
229 :type cnf: :py:class:`CnfList` or :py:class:`Cnf`
230 :param str operation: name of the operation that is being done
231 :raises: :py:class:`TypeError` if the type of the CNF object
232 is neither a list nor a CNF value
233 :returns: wrapped CNF value
234 :rtype: :py:class:`CnfList`
236 if isinstance(cnf, Cnf):
238 elif not isinstance(cnf, CnfList):
239 raise TypeError(f"Cannot {operation} value(s) of type `{type(cnf)}`")
243 class BinaryCnfStore(CnfStore):
244 """Implementation of the CNF store that uses get_ and set_cnf."""
246 #: how much to wait for arnied to report running
249 def __init__(self, backend_driver=CnfBinary):
251 Initialize this class.
253 :param backend_driver: driver to use to talk to the cnfvar backend
254 :type: :py:class:`CnfBinary`
256 super().__init__(backend_driver=backend_driver)
257 log.debug("Initialized BinaryCnfStore with driver `%s`", type(backend_driver))
259 def query(self, name=None, instance=None):
261 Query the CNF store and return a list of parsed CNFs.
263 :param str name: optional name of the CNFs to query
264 :param instance: optional CNF instance
265 :type instance: str or int
266 :returns: list of parsed CNF values
267 :rtype: :py:class:`CnfList`
271 user_cnfs = store.query("USER")
273 log.debug("Querying BinaryCnfStore with name=%s and instance=%s",
275 output = self._driver.get_cnf(name, instance)
278 # otherwise cnfvar raises a generic Malformed exception
281 return CnfList.from_cnf_string(output)
283 def commit(self, cnf, fix_problems=False):
285 Update or insert CNF variables from a list.
287 :param cnf: CNF instance or list of CNFs to update or insert
288 :type cnf: :py:class:`Cnf` or :py:class:`CnfList`
289 :param bool fix_problems: whether to automatically fix errors in the vars
291 .. note:: you can mix variables to insert and variables to update
292 in the same list as the system should handle it nicely
296 user_cnf = store.query("USER")\
297 .single_with_value("joe")
298 user_cnf.add_child("user_group_member_ref", "3")
299 store.commit(user_cnf)
301 cnf = self._cnf_or_list(cnf, operation="commit")
302 self._autofix_instances(cnf)
304 # set_cnf is demaning on lineno's
306 log.debug("Committing variables via binaries:\n%s", cnf)
308 self._call_arnied(arnied_wrapper.verify_running, timeout=self.ARNIED_TIMEOUT)
310 self._driver.set_cnf(input_str=str(cnf), fix_problems=fix_problems)
311 except subprocess.CalledProcessError as ex:
312 raise CommitException(cnf, ex.stderr) from None
313 self._call_arnied(arnied_wrapper.wait_for_generate)
315 def delete(self, cnf, fix_problems=False):
317 Delete a list of top-level CNF variables.
319 :param cnf: a single CNF value or a list of values
320 :type cnf: :py:class:`CnfList` or :py:class:`Cnf`
321 :param bool fix_problems: whether to automatically fix errors in the vars
325 user_cnfs = store.query("USER")\
326 .where(lambda c: c.value in ["joe", "jane"])
327 store.delete(user_cnfs)
329 cnf = self._cnf_or_list(cnf, operation="delete")
330 if any((c.parent is not None for c in cnf)):
331 raise RuntimeError("Calling delete is only supported on top-level CNF variables")
333 # set_cnf is demaning on lineno's
335 log.debug("Deleting variables via binaries:\n%s", cnf)
337 self._call_arnied(arnied_wrapper.verify_running, timeout=self.ARNIED_TIMEOUT)
339 self._driver.set_cnf(input_str=str(cnf), delete=True, fix_problems=fix_problems)
340 except subprocess.CalledProcessError as ex:
341 raise CommitException(cnf, ex.stderr) from None
342 self._call_arnied(arnied_wrapper.wait_for_generate)
344 def _call_arnied(self, fn, *args, **kwargs):
346 Simple proxy around the arnied to be overwritten in child classes.
348 :param fn: function to invoke in the arnied wrapper
349 :type fn: py:function
350 :param args: arguments to be passed to function
351 :param kwargs: named arguments to be passed to function
353 return fn(*args, **kwargs)
356 class CommitException(Exception):
357 """Custom exception for commit errors."""
359 def __init__(self, cnfvars, msg):
361 Initialize this exception.
363 :param cnfvars: list of CNF variables that would be committed
364 :type cnfvars: CnfList
365 :param str msg: error message
368 Error committing CNF variables!
369 ----------------------------
372 ----------------------------
376 super().__init__(msg)