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
26 - BinaryCnfStore: alternative store class implementation using the `set_cnf`
27 and `get_cnf` binaries
29 .. seealso:: Overview Diagram linked to from doc main page
31 .. codeauthor:: Intra2net
38 from .. import arnied_wrapper, arnied_api
39 from . import CnfBinary, Cnf, CnfList
41 log = logging.getLogger("pyi2ncommon.cnfvar.store")
45 """Main CNF store class that uses the varlink API."""
47 def __init__(self, backend_driver=arnied_api.Arnied):
49 Initialize this class.
51 :param backend_driver: driver to use to talk to the cnfvar backend
52 :type: :py:class:`arnied_api.Arnied`
54 self._driver = backend_driver
55 # TODO: implement `self._wait_for_arnied()` which should busy-loop with
56 # the arnied varlink socket and handle "Disconnected" errors, then perhaps
57 # drop the old binary cnf store method from the arnied wrapper
58 log.debug(f"Initialized cnf store with driver `{backend_driver.__name__}`")
60 def query(self, name=None, instance=None):
62 Query the CNF store and return a list of parsed CNFs.
64 :param str name: optional name of the CNFs to query
65 :param instance: optional CNF instance
66 :type instance: str or int
67 :returns: list of parsed CNF values
68 :rtype: :py:class:`CnfList`
72 user_cnfs = store.query("USER")
74 log.debug("Querying CnfStore with name=%s and instance=%s via arnied API",
77 # the API only expects an object if there is a name (and it's not case-insensitive)
78 query = arnied_api.GetCnfQuery(name.upper(), instance) if name else None
79 api_ret = self._driver.get_cnf(query)
81 # NOTE: logging all output here would result in huge lines when querying
82 # all variables via `store.query()`
83 log.debug("Arnied API returned %d cnfvars", len(api_ret.vars))
84 return CnfList.from_api_structure(api_ret.vars)
86 def commit(self, cnf, fix_problems=False):
88 Update or insert CNF variables from a list.
90 :param cnf: CNF instance or list of CNFs to update or insert
91 :type cnf: :py:class:`Cnf` or :py:class:`CnfList`
92 :raises: :py:class:`CommitException` if the arnied API complains
93 :param bool fix_problems: whether to automatically fix errors in the vars
95 .. note:: you can mix variables to insert and variables to update
96 in the same list as the system should handle it nicely
100 user_cnf = store.query("USER")\
101 .single_with_value("joe")
102 user_cnf.add_child("user_group_member_ref", "3")
103 store.commit(user_cnf)
105 cnf = self._cnf_or_list(cnf, operation="commit")
106 self._autofix_instances(cnf)
107 log.debug("Committing variables via arnied API:\n%s", cnf)
109 arnied_cnfs = cnf.to_api_structure()
110 self._do_commit(cnf, arnied_cnfs, fix_problems=fix_problems)
112 def delete(self, cnf, fix_problems=False):
114 Delete a list of top-level CNF variables.
116 :param cnf: a single CNF value or a list of values
117 :type cnf: :py:class:`CnfList` or :py:class:`Cnf`
118 :param bool fix_problems: whether to automatically fix errors in the vars
122 user_cnfs = store.query("USER")\
123 .where(lambda c: c.value in ["joe", "jane"])
124 store.delete(user_cnfs)
126 cnf = self._cnf_or_list(cnf, operation="delete")
127 if any((c.parent is not None for c in cnf)):
128 raise RuntimeError("Calling delete is only supported on top-level CNF variables")
129 log.debug("Deleting variables via arnied API:\n%s", cnf)
131 arnied_cnfs = cnf.to_api_structure()
132 for c in arnied_cnfs:
135 self._do_commit(cnf, arnied_cnfs, fix_problems=fix_problems)
137 def _autofix_instances(self, cnfs):
139 Auto-assign a valid instance value to all top-level vars in a list.
141 :param cnfs: list of cnfvars to fix
142 :type cnfs: :py:class`CnfList`
144 When the instance of a cnfvar is -1, the backend initializes it
145 automatically. However, it starts on 1, whereas many variables are not
146 allowed to start on one (those that are meant to be unique, f.e.).
147 This method can be used in child classes to use an alternative scheme,
148 however for performance reasons the base API class uses the default and
149 relies on the cnfvar backend to do this job.
151 ..todo:: This method compensates for limitations in production code that
152 might end up fixed up there deprecating our patching here.
155 def _do_commit(self, original_cnfs, arnied_cnfs, fix_problems=False):
157 Set cnfvars and commit changes.
159 :param original_cnfs: list of CNFs to print in case of errors
160 :type original_cnfs: :py:class:`CnfList`
161 :param arnied_cnfs: list of cnfvars to pass to the arnied API
162 :type arnied_cnfs: [:py:class:`arnied_api.CnfVar`]
163 :param bool fix_problems: whether to automatically fix errors in the vars
164 :raises: :py:class:`CommitException` if the arnied API complains
167 ret = self._driver.set_commit_cnf(vars=arnied_cnfs, username=None,
169 fix_commit=fix_problems)
170 except arnied_api.CnfCommitError as ex:
171 # fatal errors, we will handle it in our custom exception
175 for r in ret.results:
176 # message can contain HTML escape codes
177 msg = html.unescape(r.result_msg)
179 # `result_type` is defined as int in the varlink API,
180 # but the meaning of the codes are:
181 # enum result_type { OK=0, WARN=1, FAIL_TEMP=2, FAIL=3 }
182 if r.result_type == 1:
183 log.debug("Warning in `` %s,%s: \"%s\" ``: {msg} (code=%s)",
184 r.name, r.instance, r.data, r.result_type)
185 elif r.result_type > 1:
186 errors.append(f"Error in `` {r.name},{r.instance}: \"{r.data}\" ``"
190 log.debug("Error sending variables:\n%s", arnied_cnfs)
191 raise CommitException(original_cnfs, "\n".join(errors))
193 self._wait_for_generate()
195 def _wait_for_generate(self, timeout=300):
197 Wait for the 'generate' program to end.
199 :param int timeout: program run timeout
200 :raises: :py:class:`TimeoutError` if the program did not finish on time
202 def scheduled_or_running(progname):
203 ret = self._driver.is_scheduled_or_running(progname)
204 return ret.status in [arnied_api.ProgramStatus.Scheduled,
205 arnied_api.ProgramStatus.Running]
207 def wait_for_program(progname):
208 log.debug("Waiting for `%s` to be running", progname)
210 if scheduled_or_running(progname):
211 # if is running or scheduled, break to wait for completion
215 # after trying and retrying, program is not scheduled nor
216 # running, so it is safe to assume it has already executed
219 # program running or scheduled, wait
220 log.debug("Waiting for `%s` to finish", progname)
221 for _ in range(0, timeout):
222 if not scheduled_or_running(progname):
223 # finished executing, bail out
226 raise TimeoutError(f"Program `{progname}` did not end in time")
228 wait_for_program("GENERATE")
229 wait_for_program("GENERATE_OFFLINE")
231 def _cnf_or_list(self, cnf, operation) -> CnfList:
233 Validate and wrap a CNF value into a list.
235 :param cnf: a single CNF value or a list of values
236 :type cnf: :py:class:`CnfList` or :py:class:`Cnf`
237 :param str operation: name of the operation that is being done
238 :raises: :py:class:`TypeError` if the type of the CNF object
239 is neither a list nor a CNF value
240 :returns: wrapped CNF value
241 :rtype: :py:class:`CnfList`
243 if isinstance(cnf, Cnf):
245 elif not isinstance(cnf, CnfList):
246 raise TypeError(f"Cannot {operation} value(s) of type `{type(cnf)}`")
250 class BinaryCnfStore(CnfStore):
251 """Implementation of the CNF store that uses `get_cnf` and `set_cnf`."""
253 #: how much to wait for arnied to report running
256 def __init__(self, backend_driver=CnfBinary):
258 Initialize this class.
260 :param backend_driver: driver to use to talk to the cnfvar backend
261 :type: :py:class:`CnfBinary`
263 super().__init__(backend_driver=backend_driver)
264 # We assume that any external events happening to arnied require reinitializing
265 # the cnf store which is bound to the lifespan of a single arnied process.
266 self._call_arnied(arnied_wrapper.verify_running, timeout=self.ARNIED_TIMEOUT)
267 log.debug(f"Initialized binary cnf store with driver `{backend_driver.__name__}`")
269 def query(self, name=None, instance=None):
271 Query the CNF store and return a list of parsed CNFs.
273 :param str name: optional name of the CNFs to query
274 :param instance: optional CNF instance
275 :type instance: str or int
276 :returns: list of parsed CNF values
277 :rtype: :py:class:`CnfList`
281 user_cnfs = store.query("USER")
283 log.debug("Querying BinaryCnfStore with name=%s and instance=%s",
285 output = self._driver.get_cnf(name, instance=instance)
288 # otherwise cnfvar raises a generic Malformed exception
291 return CnfList.from_cnf_string(output)
293 def commit(self, cnf, fix_problems=False):
295 Update or insert CNF variables from a list.
297 :param cnf: CNF instance or list of CNFs to update or insert
298 :type cnf: :py:class:`Cnf` or :py:class:`CnfList`
299 :param bool fix_problems: whether to automatically fix errors in the vars
301 .. note:: you can mix variables to insert and variables to update
302 in the same list as the system should handle it nicely
306 user_cnf = store.query("USER")\
307 .single_with_value("joe")
308 user_cnf.add_child("user_group_member_ref", "3")
309 store.commit(user_cnf)
311 cnf = self._cnf_or_list(cnf, operation="commit")
312 self._autofix_instances(cnf)
314 # set_cnf is demanding on lineno's
316 log.debug("Committing variables via binaries:\n%s", cnf)
319 self._driver.set_cnf(input_str=str(cnf), fix_problems=fix_problems)
320 except subprocess.CalledProcessError as ex:
321 raise CommitException(cnf, ex.stderr) from None
322 self._call_arnied(arnied_wrapper.wait_for_generate)
324 def delete(self, cnf, fix_problems=False):
326 Delete a list of top-level CNF variables.
328 :param cnf: a single CNF value or a list of values
329 :type cnf: :py:class:`CnfList` or :py:class:`Cnf`
330 :param bool fix_problems: whether to automatically fix errors in the vars
334 user_cnfs = store.query("USER")\
335 .where(lambda c: c.value in ["joe", "jane"])
336 store.delete(user_cnfs)
338 cnf = self._cnf_or_list(cnf, operation="delete")
339 if any((c.parent is not None for c in cnf)):
340 raise RuntimeError("Calling delete is only supported on top-level CNF variables")
342 # set_cnf is demanding on lineno's
344 log.debug("Deleting variables via binaries:\n%s", cnf)
347 self._driver.set_cnf(input_str=str(cnf), delete=True, fix_problems=fix_problems)
348 except subprocess.CalledProcessError as ex:
349 raise CommitException(cnf, ex.stderr) from None
350 self._call_arnied(arnied_wrapper.wait_for_generate)
352 def _call_arnied(self, fn, *args, **kwargs):
354 Simple proxy around the arnied to be overwritten in child classes.
356 :param fn: function to invoke in the arnied wrapper
357 :type fn: py:function
358 :param args: arguments to be passed to function
359 :param kwargs: named arguments to be passed to function
361 return fn(*args, **kwargs)
364 #: pattern for more verbose error message for :py:class:`CommitException`
365 COMMIT_EXCEPTION_MESSAGE = """\
366 Error committing CNF variables!
367 ----------------------------
370 ----------------------------
376 class CommitException(Exception):
377 """Custom exception for commit errors."""
379 def __init__(self, cnfvars, msg):
381 Initialize this exception.
383 :param cnfvars: list of CNF variables that would be committed
384 :type cnfvars: CnfList
385 :param str msg: error message
387 super().__init__(msg)
388 self.message = COMMIT_EXCEPTION_MESSAGE.format(cnfvars=cnfvars, msg=msg)