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-2018 Intra2net AG <info@intra2net.com>
24 ------------------------------------------------------
25 Read / write / merge guest cnf var sets, even on host.
27 .. note:: DEPRECATED! Please do not extend this or add new uses of this module,
28 use :py:mod:`pyi2ncommon.arnied_api` or :py:mod:`pyi2ncommon.cnfvar`
31 Copyright: Intra2net AG
35 ------------------------------------------------------
37 This module can be viewed as a convenience-wrapper around module
38 :py:mod:`cnfvar`. It uses many of its function but provides some higher-level
39 interfaces, most of all class :py:class:`SimpleCnf`. It is completely
40 independent of the :py:mod:`cnfline` package and its included subclasses
41 (modules in `shared.cnfline`, starting with ``build_`` and ``configure_``).
43 Class :py:class:`SimpleCnf` represents a hierarchical set of conf vars and
44 provides functions to deal with that hierarchy. Under the hood, all functions
45 here (and probably also in :py:mod:`cnfvar`) work with conf vars represented as
46 dictionaries and lists thereof. Conf var dicts have keys `number`, `varname`,
47 `instance`, `data`, `comment` and possibly `parent` and/or `children`.
48 `varname` is a regular lower-case string, `data` is a (utf8) string, `comment`
49 is usually None, `number`, `parent` and `instance` are int. If a conf var has
50 children, then this is a list of conf var dicts. `parent` is only present if a
51 conf-var is a child. Several conf vars, if not wrapped in a
52 :py:class:`SimpleCnf`, appear either as simple list of conf var dicts or as a
53 dict with a single key `cnf` whose value is a list of conf var dicts. (Function
54 :py:func:`get_cnf` returns the former given the latter).
56 .. todo:: Exceptions Invalid[Json]Cnf are used inconsistently (e.g. check type
57 of function arguments `-->` should be ValueError) and difference
58 between them is unclear. Also name differs only in case from
59 :py:class:`cnfvar.InvalidCNF`
63 ------------------------------------------------------
71 log = logging.getLogger('pyi2ncommon.simple_cnf')
73 from . import arnied_wrapper
74 from . import cnfvar_old
77 ###############################################################################
79 ###############################################################################
81 #: timeout for copying temporary config files to VM objects (seconds)
82 COPY_FILES_TIMEOUT = 15
84 #: additional location of configuration files
85 ADD_CNFFILE_PATH = "/tmp/configs"
88 ###############################################################################
90 ###############################################################################
93 class InvalidCnf(Exception):
94 """Exception that indicates a general problem with conf var processing."""
96 def __init__(self, m):
97 """Create an invalid config exception."""
98 msg = "Invalid CNF_VAR: %s" % m
99 super(InvalidCnf, self).__init__(msg)
104 """Get a string version of the exception message."""
105 return "%s %s" % (self.pfx, self.msg)
108 class InvalidJsonCnf(InvalidCnf):
109 """Exception that indicates a general problem with conf var processing."""
111 def __init__(self, m):
112 """Create an invalid JSON config exception."""
113 super(InvalidJsonCnf, self).__init__(m)
114 self.pfx = "[CNF:JSON]"
117 ###############################################################################
118 # auxiliary functions
119 ###############################################################################
124 "Convert" a config dict to a list of conf var dicts.
126 This just removes the top-level 'cnf' key and returns its value.
128 :param cnf: config dictionary
129 :type cnf: {str, [dict]}
130 :returns: list of cnf var dicts
131 :rtype: [{str, int or str or None}]
132 :raises: :py:class:`InvalidJsonCnf` if there is no `cnf` field found
134 cnf_vars = cnf.get("cnf")
136 raise InvalidJsonCnf("toplevel \"cnf\" field required")
142 Get a (quite) safe temporary file name for config file.
144 :returns: temporary file name
148 file_handle, file_name = tempfile.mkstemp(prefix="simple_%d_" % int(now),
150 os.close(file_handle)
155 def set_values(cnf_vars, replacements):
157 Recursively replace values in configuration
159 Works in-place, meaning that no new configuration is created and returned
160 but instead argument `cnf_vars` is modified (and nothing returned).
162 :param cnf_vars: config where replacements are to be made
163 :type cnf_vars: [{str, int or str or None}] or {str, [dict]}
164 :param replacements: what to replace and what to replace it with
165 :type replacements: {str, str} or [(str, str)]
166 :raises: :py:class:`InvalidJsonCnf` if cnf_vars is neither dict or list
168 # determine set replace_me of keys to replace and function get that returns
169 # value for key or empty string if key not in replacements
172 if isinstance(replacements, dict):
173 replace_me = set(k.lower() for k in replacements.keys())
174 get = lambda var: str(replacements.get(var, "")) # pylint: disable=function-redefined
175 elif isinstance(replacements, list):
176 replace_me = set(r[0].lower() for r in replacements)
178 def get(var): # pylint: disable=function-redefined
179 """Get replacement value for given variable name."""
181 return str(next(r[1] for r in replacements if r[0] == var))
182 except StopIteration:
185 raise TypeError("replacements must be dictionary or key-value list")
187 # check type of arg "cnf_vars"
188 if isinstance(cnf_vars, dict):
189 cnf_vars = cnf_vars["cnf"] # operate on the var list
190 if not isinstance(cnf_vars, list):
191 raise InvalidJsonCnf("ill-formed CNF_VAR: expected list, got %s (%s)"
192 % (type(cnf_vars), cnf_vars))
195 """Internal recursive function to replace values."""
197 varname = var["varname"].lower()
198 if varname in replace_me:
199 var["data"] = str(get(varname))
200 children = var.get("children", None)
201 if children is not None:
204 # apply function on complete cnf_vars
208 def lookup_cnf_file(fname):
210 Searches for config file with given name in default locations.
212 :param str fname: file name of config file (without path)
213 :returns: first existing config file found in default locations
215 :raises: :py:class:`IOError` if no such config file was found
217 locations = [arnied_wrapper.SRC_CONFIG_DIR, ADD_CNFFILE_PATH]
218 for path in locations:
219 fullpath = os.path.join(path, fname)
220 if os.path.isfile(fullpath):
222 raise IOError("config file %s does not exist in any of the readable "
223 "locations %s" % (fname, locations))
226 ###############################################################################
228 ###############################################################################
231 class SimpleCnf(object):
233 Representation of hierarchical configuration of variables.
235 Based on C++ `cnf_vars` as visualized by *get_cnf*.
237 Internal data representation: list of conf var dicts; see module doc for
241 def __init__(self, cnf=None):
243 Creates a simple configuration.
245 Does not check whether given cnf list contains only valid data.
246 Does not recurse into dicts.
248 :param cnf: initial set of conf var data (default: None = empty conf)
249 :type cnf: list or anything that :py:func:`get_cnf` can read
253 elif isinstance(cnf, list):
255 elif isinstance(cnf, dict):
256 self.__cnfvars = get_cnf(cnf)
258 raise InvalidCnf ("cannot handle %s type inputs" % type (cnf))
260 def _find_new_number(self, cnf_vars):
261 """Recursive helper function to find new unique (line) number."""
264 new_numbers = [1, ] # in case cnf_vars is empty
265 for cnf_var in cnf_vars:
266 new_numbers.append(cnf_var['number'] + 1)
268 new_numbers.append(self._find_new_number(cnf_var['children']))
271 return max(new_numbers) # this is max(all numbers) + 1
273 def _find_new_instance(self, varname):
275 Find an instance number for variable with non-unique varname.
277 Will only check on top level, is not recursive.
279 :param str varname: name of conf var; will be converted to lower-case
280 :returns: instance number for which there is no other conf var of same
281 name (0 if there is not other conf var with that name)
285 varname = varname.lower()
286 for entry in self.__cnfvars:
287 if entry['varname'] == varname:
288 result = max(result, entry['number']+1)
291 def add(self, varname, data='', number=None, instance=None, children=None):
293 Add a cnf var to config on top level.
295 :param str varname: name of conf var; only required arg; case ignored
296 :param str data: conf var's value
297 :param int number: line number of that conf var; if given as None
298 (default) the function looks through config to find
299 a new number that is not taken; must be positive!
300 Value will be ignored if children are given.
301 :param int instance: Instance of the new conf var or None (default).
302 If None, then function looks through config to
303 find a new unique instance number
304 :param children: child confs for given conf var. Children's parent
305 and line attributes will be set in this function
306 :type children: :py:class:`SimpleCnf`
309 instance = self._find_new_instance(varname)
311 number = self._find_new_number(self.__cnfvars) # need top number
313 for child in children:
314 new_dict = child.get_single_dict()
315 new_dict['parent'] = number
316 new_children.append(new_dict)
317 cnfvar_old.renumber_vars({'cnf':new_children}, number)
318 children = new_children
320 number = self._find_new_number(self.__cnfvars)
322 new_var = dict(varname=varname.lower(), data=data,
323 number=number, comment=None, instance=instance)
325 new_var['children'] = children # only add if non-empty
326 self.__cnfvars.append(new_var)
328 def add_single(self, varname, data=u'', number=None):
330 Add a single cnf var to config on top level.
334 return self.add (varname, data=data, number=number)
336 def append_file_generic(self, reader, cnf, replacements=None):
338 Append conf var data from file.
340 If `replacements` are given, calls :py:meth:`set_values` with these
341 before adding values to config.
343 :param cnf: file name or dictionary of conf vars
344 :type cnf: str or {str, int or str or None}
345 :param replacements: see help in :py:meth:`set_values`
347 log.info("append CNF_VARs from file")
349 if callable(reader) is False:
350 raise TypeError("append_file_generic: reader must be callable, "
351 "not %s" % type(reader))
352 if isinstance(cnf, dict):
353 new_vars = get_cnf(cnf)
354 elif isinstance(cnf, str):
355 fullpath = lookup_cnf_file(cnf)
356 with open(fullpath, "rb") as chan:
357 cnfdata = chan.read()
358 tmp = reader(cnfdata)
359 new_vars = get_cnf(tmp)
361 raise InvalidCnf("Cannot append object \"%s\" of type \"%s\"."
364 if replacements is not None:
365 set_values(new_vars, replacements)
367 self.__cnfvars.extend(new_vars)
369 def append_file(self, cnf, replacements=None):
370 """Append conf var data from file."""
371 return self.append_file_generic(cnfvar_old.read_cnf, cnf,
372 replacements=replacements)
374 def append_file_json(self, cnf, replacements=None):
375 """Append conf var data from json file."""
376 return self.append_file_generic(cnfvar_old.read_cnf_json, cnf,
377 replacements=replacements)
379 def append_guest_vars(self, vm=None, varname=None, replacements=None):
381 Append content from machine's "real" config to this object.
383 Runs `get_cnf -j [varname]` on local host or VM (depending on arg
384 `vm`), converts output and appends it to this objects' conf var set.
385 If replacements are given, runs :py:meth:`set_values`, first.
387 :param vm: a guest vm or None to run on local host
388 :type vm: VM object or None
389 :param str varname: optional root of conf vars to append. If given as
390 None (default), append complete conf
391 :param replacements: see help in :py:meth:`set_values`
393 cnf = arnied_wrapper.get_cnfvar(varname=varname, vm=vm)
394 new_vars = get_cnf(cnf)
396 log.info("apply substitutions to extracted CNF_VARs")
397 if replacements is not None:
398 set_values(new_vars, replacements)
400 current = self.__cnfvars
401 current.extend(new_vars)
403 def save(self, filename=None):
405 Saves this object's configuration data to a file.
407 The output file's content can be interpreted by `set_cnf -j`.
409 :param str filename: name of file to write config to; if None (default)
410 the config will be written to a temporary file
411 :returns: filename that was written to
414 log.info("save configuration")
415 current = self.__cnfvars
417 raise InvalidCnf("No variables to write.")
420 # create temporary filename
421 filename = arnied_wrapper.generate_config_path(dumped=True)
423 with open(filename, 'w') as out:
424 cnfvar_old.output_json({"cnf": current}, out, renumber=True)
428 def apply(self, vm=None, renumber=True):
430 Apply object's config on VM or local host.
432 Runs a `set_cnf` with complete internal config data, possibly waits for
433 generate to finish afterwards.
435 :param vm: a guest vm or None to apply on local host
436 :type vm: VM object or None
437 :param bool renumber: re-number conf vars before application
439 current = self.__cnfvars
441 log.info("enforce consistent CNF_LINE numbering")
442 cnfvar_old.renumber_vars(current)
443 log.info("inject configuration %s" % "into guest" if vm else "in place")
444 arnied_wrapper.set_cnf_dynamic({"cnf": current},
445 config_file=gen_tmpname(), vm=vm)
449 Get a config in json format, ready for `set_cnf -j`.
451 :returns: config in json format
454 return cnfvar_old.dump_json_string({"cnf": self.__cnfvars}, renumber=True)
456 def pretty_print(self, print_func=None):
458 Get a string representation of this simple_cnf that is human-readable
460 Result is valid json with nice line breaks and indentation but not
461 renumbered (so may not be fit for parsing)
463 for line in json.dumps({"cnf": self.__cnfvars}, check_circular=False,
464 indent=4, sort_keys=True).splitlines():
465 if print_func is None:
472 Return an iterator over the contents of this simple cnf.
474 The iteration might not be ordered by line number nor entry nor
475 anything else. No guarantees made!
477 The entries returned by the iterator are :py:class:`SimpleCnf`.
481 for cnf_list in iter(my_cnf['PROXY_ACCESSLIST']):
482 print('checking proxy list {0} with {1} children'
483 .format(cnf_list.get_value(), len(cnf_list)))
485 # self.__cnfvars is a list of dicts, each with the same fields
486 for dict_entry in self.__cnfvars:
487 yield SimpleCnf([dict_entry, ])
489 def __getitem__(self, item):
491 Called by `cnf['key']` or `cnf[line_number]`; returns subset of cnf.
493 Processing time is O(n) where n is the number of top-level entries in
499 all.append_guest_vars()
500 len(all) # --> probably huge
501 len(all['user']) # should give the number of users
503 # should result in the same as all['user']:
505 users.append_guest_vars(varname='user')
507 :param item: line number or value to specify a cnf subset;
508 if string value, will be converted to lower case
509 :type item: int or str
510 :returns: another simple cnf that contains a subset of this simple cnf
511 :rtype: :py:class:`SimpleCnf`
513 .. seealso:: method :py:func:`get` (more general than this)
515 # determine whether arg 'item' is a key name or a line number
516 if isinstance(item, int): # is line number
518 else: # assume key name
522 # search all entries for matches
523 results = [dict_entry for dict_entry in self.__cnfvars
524 if dict_entry[dict_key] == item]
526 # convert result to a simple cnf
527 return SimpleCnf(results)
531 Get the number of top-level entries in cnf.
533 :returns: number of top-level entries in cnf
536 return len(self.__cnfvars)
538 def get(self, name=None, value=None, instance=None, line=None):
540 Get a subset of this config that matches ALL of given criteria.
542 For example, if :py:func:`get_cnf` contains the line
543 '1121 USER,1: "admin"', all of these examples will result in the same
546 cnf.get(name='user', value='admin')
547 cnf.get(name='user', instance=1)
548 cnf.get(name='user').get(value='admin')
551 :param str name: conf var name (key) or None to not use this criterion;
552 will be converted to lower case
553 :param str value: value of conf var or None to not use this criterion
554 :param int instance: instance number of value in a list (e.g. USERS)
555 or None to not use this criterion
556 :param int line: line number of None to not use this criterion
557 :returns: a simple cnf that contains only entries that match ALL of the
558 given criteria. If nothing matches the given criteria, an
559 empty simple cnf will be returned
560 :rtype: :py:class:`SimpleCnf`
562 .. seealso:: method :py:func:`__getitem__` (less general than this)
565 name_test = lambda test_val: True
568 name_test = lambda test_val: name == test_val['varname']
571 value_test = lambda test_val: True
574 value_test = lambda test_val: test_val['data'] == value
577 instance_test = lambda test_val: True
578 elif not isinstance(instance, int):
579 raise ValueError('expect int value for instance!')
581 instance_test = lambda test_val: instance == test_val['instance']
584 line_test = lambda test_val: True
585 elif not isinstance(line, int):
586 raise ValueError('expect int value for line number!')
588 line_test = lambda test_val: test_val['number'] == line
590 return SimpleCnf(list(entry for entry in self.__cnfvars
591 if name_test(entry) and value_test(entry)
592 and instance_test(entry) and line_test(entry)))
594 def get_children(self):
596 Get children of simple cnf of just 1 entry.
598 :returns: simple cnf children or an empty simple cnf if entry has
600 :rtype: :py:class:`SimpleCnf`
601 :raises: :py:class:`ValueError` if this simple cnf has more
605 raise ValueError('get_children only possible if len == 1 (is {0})!'
608 result = self.__cnfvars[0]['children']
617 return SimpleCnf(result)
621 Get a value of a simple cnf of just 1 entry.
623 :returns: str cnf value/data
625 :raises: :py:class:`ValueError` if this simple cnf has more
629 raise ValueError('get_value only possible if len == 1 (is {0})!'
631 return self.__cnfvars[0]['data']
633 def get_single_dict(self):
635 Get a dictionary of a simple cnf of just 1 entry.
637 :returns: dictionary of a simple cnf
638 :rtype: {str, int or str or None}
641 raise ValueError('get_single_dict only possible if len == 1 (is {0})!'
643 return self.__cnfvars[0]
645 def __eq__(self, other_cnf):
647 Determine wether `self` == `other_cnf`.
649 :param other_cnf: cnf to compare with
650 :type other_cnf: :py:class:`SimpleCnf`
651 :returns: whether all cnf var entries are equal
654 key_func = lambda cnf_var_entry: cnf_var_entry['number']
656 if isinstance (other_cnf, SimpleCnf) is False:
659 return sorted(self.__cnfvars, key=key_func) \
660 == sorted(other_cnf.__cnfvars, key=key_func) # pylint: disable=protected-access