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 DEPRECATED! Please do not extend this or add new uses of this module, use
28 :py:mod:`pyi2ncommon.arnied_api` or :py:mod:`pyi2ncommon.cnfvar` instead!
30 Copyright: Intra2net AG
34 ------------------------------------------------------
36 This module can be viewed as a convenience-wrapper around module
37 :py:mod:`cnfvar`. It uses many of its function but provides some higher-level
38 interfaces, most of all class :py:class:`SimpleCnf`. It is completely
39 independent of the :py:mod:`cnfline` package and its included subclasses
40 (modules in `shared.cnfline`, starting with ``build_`` and ``configure_``).
42 Class :py:class:`SimpleCnf` represents a hierarchical set of conf vars and
43 provides functions to deal with that hierarchy. Under the hood, all functions
44 here (and probably also in :py:mod:`cnfvar`) work with conf vars represented as
45 dictionaries and lists thereof. Conf var dicts have keys `number`, `varname`,
46 `instance`, `data`, `comment` and possibly `parent` and/or `children`.
47 `varname` is a regular lower-case string, `data` is a (utf8) string, `comment`
48 is usually None, `number`, `parent` and `instance` are int. If a conf var has
49 children, then this is a list of conf var dicts. `parent` is only present if a
50 conf-var is a child. Several conf vars, if not wrapped in a
51 :py:class:`SimpleCnf`, appear either as simple list of conf var dicts or as a
52 dict with a single key `cnf` whose value is a list of conf var dicts. (Function
53 :py:func:`get_cnf` returns the former given the latter).
55 .. todo:: Exceptions Invalid[Json]Cnf are used inconsistently (e.g. check type
56 of function arguments `-->` should be ValueError) and difference
57 between them is unclear. Also name differs only in case from
58 :py:class:`cnfvar.InvalidCNF`
62 ------------------------------------------------------
70 log = logging.getLogger('pyi2ncommon.simple_cnf')
72 from . import arnied_wrapper
73 from . import cnfvar_old
76 ###############################################################################
78 ###############################################################################
80 #: timeout for copying temporary config files to VM objects (seconds)
81 COPY_FILES_TIMEOUT = 15
83 #: additional location of configuration files
84 ADD_CNFFILE_PATH = "/tmp/configs"
87 ###############################################################################
89 ###############################################################################
92 class InvalidCnf(Exception):
93 """Exception that indicates a general problem with conf var processing."""
95 def __init__(self, m):
96 """Create an invalid config exception."""
97 msg = "Invalid CNF_VAR: %s" % m
98 super(InvalidCnf, self).__init__(msg)
103 """Get a string version of the exception message."""
104 return "%s %s" % (self.pfx, self.msg)
107 class InvalidJsonCnf(InvalidCnf):
108 """Exception that indicates a general problem with conf var processing."""
110 def __init__(self, m):
111 """Create an invalid JSON config exception."""
112 super(InvalidJsonCnf, self).__init__(m)
113 self.pfx = "[CNF:JSON]"
116 ###############################################################################
117 # auxiliary functions
118 ###############################################################################
123 "Convert" a config dict to a list of conf var dicts.
125 This just removes the top-level 'cnf' key and returns its value.
127 :param cnf: config dictionary
128 :type cnf: {str, [dict]}
129 :returns: list of cnf var dicts
130 :rtype: [{str, int or str or None}]
131 :raises: :py:class:`InvalidJsonCnf` if there is no `cnf` field found
133 cnf_vars = cnf.get("cnf")
135 raise InvalidJsonCnf("toplevel \"cnf\" field required")
141 Get a (quite) safe temporary file name for config file.
143 :returns: temporary file name
147 file_handle, file_name = tempfile.mkstemp(prefix="simple_%d_" % int(now),
149 os.close(file_handle)
154 def set_values(cnf_vars, replacements):
156 Recursively replace values in configuration
158 Works in-place, meaning that no new configuration is created and returned
159 but instead argument `cnf_vars` is modified (and nothing returned).
161 :param cnf_vars: config where replacements are to be made
162 :type cnf_vars: [{str, int or str or None}] or {str, [dict]}
163 :param replacements: what to replace and what to replace it with
164 :type replacements: {str, str} or [(str, str)]
165 :raises: :py:class:`InvalidJsonCnf` if cnf_vars is neither dict or list
167 # determine set replace_me of keys to replace and function get that returns
168 # value for key or empty string if key not in replacements
171 if isinstance(replacements, dict):
172 replace_me = set(k.lower() for k in replacements.keys())
173 get = lambda var: str(replacements.get(var, "")) # pylint: disable=function-redefined
174 elif isinstance(replacements, list):
175 replace_me = set(r[0].lower() for r in replacements)
177 def get(var): # pylint: disable=function-redefined
178 """Get replacement value for given variable name."""
180 return str(next(r[1] for r in replacements if r[0] == var))
181 except StopIteration:
184 raise TypeError("replacements must be dictionary or key-value list")
186 # check type of arg "cnf_vars"
187 if isinstance(cnf_vars, dict):
188 cnf_vars = cnf_vars["cnf"] # operate on the var list
189 if not isinstance(cnf_vars, list):
190 raise InvalidJsonCnf("ill-formed CNF_VAR: expected list, got %s (%s)"
191 % (type(cnf_vars), cnf_vars))
194 """Internal recursive function to replace values."""
196 varname = var["varname"].lower()
197 if varname in replace_me:
198 var["data"] = str(get(varname))
199 children = var.get("children", None)
200 if children is not None:
203 # apply function on complete cnf_vars
207 def lookup_cnf_file(fname):
209 Searches for config file with given name in default locations.
211 :param str fname: file name of config file (without path)
212 :returns: first existing config file found in default locations
214 :raises: :py:class:`IOError` if no such config file was found
216 locations = [arnied_wrapper.SRC_CONFIG_DIR, ADD_CNFFILE_PATH]
217 for path in locations:
218 fullpath = os.path.join(path, fname)
219 if os.path.isfile(fullpath):
221 raise IOError("config file %s does not exist in any of the readable "
222 "locations %s" % (fname, locations))
225 ###############################################################################
227 ###############################################################################
230 class SimpleCnf(object):
232 Representation of hierarchical configuration of variables.
234 Based on C++ `cnf_vars` as visualized by *get_cnf*.
236 Internal data representation: list of conf var dicts; see module doc for
240 def __init__(self, cnf=None):
242 Creates a simple configuration.
244 Does not check whether given cnf list contains only valid data.
245 Does not recurse into dicts.
247 :param cnf: initial set of conf var data (default: None = empty conf)
248 :type cnf: list or anything that :py:func:`get_cnf` can read
252 elif isinstance(cnf, list):
254 elif isinstance(cnf, dict):
255 self.__cnfvars = get_cnf(cnf)
257 raise InvalidCnf ("cannot handle %s type inputs" % type (cnf))
259 def _find_new_number(self, cnf_vars):
260 """Recursive helper function to find new unique (line) number."""
263 new_numbers = [1, ] # in case cnf_vars is empty
264 for cnf_var in cnf_vars:
265 new_numbers.append(cnf_var['number'] + 1)
267 new_numbers.append(self._find_new_number(cnf_var['children']))
270 return max(new_numbers) # this is max(all numbers) + 1
272 def _find_new_instance(self, varname):
274 Find an instance number for variable with non-unique varname.
276 Will only check on top level, is not recursive.
278 :param str varname: name of conf var; will be converted to lower-case
279 :returns: instance number for which there is no other conf var of same
280 name (0 if there is not other conf var with that name)
284 varname = varname.lower()
285 for entry in self.__cnfvars:
286 if entry['varname'] == varname:
287 result = max(result, entry['number']+1)
290 def add(self, varname, data='', number=None, instance=None, children=None):
292 Add a cnf var to config on top level.
294 :param str varname: name of conf var; only required arg; case ignored
295 :param str data: conf var's value
296 :param int number: line number of that conf var; if given as None
297 (default) the function looks through config to find
298 a new number that is not taken; must be positive!
299 Value will be ignored if children are given.
300 :param int instance: Instance of the new conf var or None (default).
301 If None, then function looks through config to
302 find a new unique instance number
303 :param children: child confs for given conf var. Children's parent
304 and line attributes will be set in this function
305 :type children: :py:class:`SimpleCnf`
308 instance = self._find_new_instance(varname)
310 number = self._find_new_number(self.__cnfvars) # need top number
312 for child in children:
313 new_dict = child.get_single_dict()
314 new_dict['parent'] = number
315 new_children.append(new_dict)
316 cnfvar_old.renumber_vars({'cnf':new_children}, number)
317 children = new_children
319 number = self._find_new_number(self.__cnfvars)
321 new_var = dict(varname=varname.lower(), data=data,
322 number=number, comment=None, instance=instance)
324 new_var['children'] = children # only add if non-empty
325 self.__cnfvars.append(new_var)
327 def add_single(self, varname, data=u'', number=None):
329 Add a single cnf var to config on top level.
333 return self.add (varname, data=data, number=number)
335 def append_file_generic(self, reader, cnf, replacements=None):
337 Append conf var data from file.
339 If `replacements` are given, calls :py:meth:`set_values` with these
340 before adding values to config.
342 :param cnf: file name or dictionary of conf vars
343 :type cnf: str or {str, int or str or None}
344 :param replacements: see help in :py:meth:`set_values`
346 log.info("append CNF_VARs from file")
348 if callable(reader) is False:
349 raise TypeError("append_file_generic: reader must be callable, "
350 "not %s" % type(reader))
351 if isinstance(cnf, dict):
352 new_vars = get_cnf(cnf)
353 elif isinstance(cnf, str):
354 fullpath = lookup_cnf_file(cnf)
355 with open(fullpath, "rb") as chan:
356 cnfdata = chan.read()
357 tmp = reader(cnfdata)
358 new_vars = get_cnf(tmp)
360 raise InvalidCnf("Cannot append object \"%s\" of type \"%s\"."
363 if replacements is not None:
364 set_values(new_vars, replacements)
366 self.__cnfvars.extend(new_vars)
368 def append_file(self, cnf, replacements=None):
369 """Append conf var data from file."""
370 return self.append_file_generic(cnfvar_old.read_cnf, cnf,
371 replacements=replacements)
373 def append_file_json(self, cnf, replacements=None):
374 """Append conf var data from json file."""
375 return self.append_file_generic(cnfvar_old.read_cnf_json, cnf,
376 replacements=replacements)
378 def append_guest_vars(self, vm=None, varname=None, replacements=None):
380 Append content from machine's "real" config to this object.
382 Runs `get_cnf -j [varname]` on local host or VM (depending on arg
383 `vm`), converts output and appends it to this objects' conf var set.
384 If replacements are given, runs :py:meth:`set_values`, first.
386 :param vm: a guest vm or None to run on local host
387 :type vm: VM object or None
388 :param str varname: optional root of conf vars to append. If given as
389 None (default), append complete conf
390 :param replacements: see help in :py:meth:`set_values`
392 cnf = arnied_wrapper.get_cnfvar(varname=varname, vm=vm)
393 new_vars = get_cnf(cnf)
395 log.info("apply substitutions to extracted CNF_VARs")
396 if replacements is not None:
397 set_values(new_vars, replacements)
399 current = self.__cnfvars
400 current.extend(new_vars)
402 def save(self, filename=None):
404 Saves this object's configuration data to a file.
406 The output file's content can be interpreted by `set_cnf -j`.
408 :param str filename: name of file to write config to; if None (default)
409 the config will be written to a temporary file
410 :returns: filename that was written to
413 log.info("save configuration")
414 current = self.__cnfvars
416 raise InvalidCnf("No variables to write.")
419 # create temporary filename
420 filename = arnied_wrapper.generate_config_path(dumped=True)
422 with open(filename, 'w') as out:
423 cnfvar_old.output_json({"cnf": current}, out, renumber=True)
427 def apply(self, vm=None, renumber=True):
429 Apply object's config on VM or local host.
431 Runs a `set_cnf` with complete internal config data, possibly waits for
432 generate to finish afterwards.
434 :param vm: a guest vm or None to apply on local host
435 :type vm: VM object or None
436 :param bool renumber: re-number conf vars before application
438 current = self.__cnfvars
440 log.info("enforce consistent CNF_LINE numbering")
441 cnfvar_old.renumber_vars(current)
442 log.info("inject configuration %s" % "into guest" if vm else "in place")
443 arnied_wrapper.set_cnf_dynamic({"cnf": current},
444 config_file=gen_tmpname(), vm=vm)
448 Get a config in json format, ready for `set_cnf -j`.
450 :returns: config in json format
453 return cnfvar_old.dump_json_string({"cnf": self.__cnfvars}, renumber=True)
455 def pretty_print(self, print_func=None):
457 Get a string representation of this simple_cnf that is human-readable
459 Result is valid json with nice line breaks and indentation but not
460 renumbered (so may not be fit for parsing)
462 for line in json.dumps({"cnf": self.__cnfvars}, check_circular=False,
463 indent=4, sort_keys=True).splitlines():
464 if print_func is None:
471 Return an iterator over the contents of this simple cnf.
473 The iteration might not be ordered by line number nor entry nor
474 anything else. No guarantees made!
476 The entries returned by the iterator are :py:class:`SimpleCnf`.
480 for cnf_list in iter(my_cnf['PROXY_ACCESSLIST']):
481 print('checking proxy list {0} with {1} children'
482 .format(cnf_list.get_value(), len(cnf_list)))
484 # self.__cnfvars is a list of dicts, each with the same fields
485 for dict_entry in self.__cnfvars:
486 yield SimpleCnf([dict_entry, ])
488 def __getitem__(self, item):
490 Called by `cnf['key']` or `cnf[line_number]`; returns subset of cnf.
492 Processing time is O(n) where n is the number of top-level entries in
498 all.append_guest_vars()
499 len(all) # --> probably huge
500 len(all['user']) # should give the number of users
502 # should result in the same as all['user']:
504 users.append_guest_vars(varname='user')
506 :param item: line number or value to specify a cnf subset;
507 if string value, will be converted to lower case
508 :type item: int or str
509 :returns: another simple cnf that contains a subset of this simple cnf
510 :rtype: :py:class:`SimpleCnf`
512 .. seealso:: method :py:func:`get` (more general than this)
514 # determine whether arg 'item' is a key name or a line number
515 if isinstance(item, int): # is line number
517 else: # assume key name
521 # search all entries for matches
522 results = [dict_entry for dict_entry in self.__cnfvars
523 if dict_entry[dict_key] == item]
525 # convert result to a simple cnf
526 return SimpleCnf(results)
530 Get the number of top-level entries in cnf.
532 :returns: number of top-level entries in cnf
535 return len(self.__cnfvars)
537 def get(self, name=None, value=None, instance=None, line=None):
539 Get a subset of this config that matches ALL of given criteria.
541 For example, if :py:func:`get_cnf` contains the line
542 '1121 USER,1: "admin"', all of these examples will result in the same
545 cnf.get(name='user', value='admin')
546 cnf.get(name='user', instance=1)
547 cnf.get(name='user').get(value='admin')
550 :param str name: conf var name (key) or None to not use this criterion;
551 will be converted to lower case
552 :param str value: value of conf var or None to not use this criterion
553 :param int instance: instance number of value in a list (e.g. USERS)
554 or None to not use this criterion
555 :param int line: line number of None to not use this criterion
556 :returns: a simple cnf that contains only entries that match ALL of the
557 given criteria. If nothing matches the given criteria, an
558 empty simple cnf will be returned
559 :rtype: :py:class:`SimpleCnf`
561 .. seealso:: method :py:func:`__getitem__` (less general than this)
564 name_test = lambda test_val: True
567 name_test = lambda test_val: name == test_val['varname']
570 value_test = lambda test_val: True
573 value_test = lambda test_val: test_val['data'] == value
576 instance_test = lambda test_val: True
577 elif not isinstance(instance, int):
578 raise ValueError('expect int value for instance!')
580 instance_test = lambda test_val: instance == test_val['instance']
583 line_test = lambda test_val: True
584 elif not isinstance(line, int):
585 raise ValueError('expect int value for line number!')
587 line_test = lambda test_val: test_val['number'] == line
589 return SimpleCnf(list(entry for entry in self.__cnfvars
590 if name_test(entry) and value_test(entry)
591 and instance_test(entry) and line_test(entry)))
593 def get_children(self):
595 Get children of simple cnf of just 1 entry.
597 :returns: simple cnf children or an empty simple cnf if entry has
599 :rtype: :py:class:`SimpleCnf`
600 :raises: :py:class:`ValueError` if this simple cnf has more
604 raise ValueError('get_children only possible if len == 1 (is {0})!'
607 result = self.__cnfvars[0]['children']
616 return SimpleCnf(result)
620 Get a value of a simple cnf of just 1 entry.
622 :returns: str cnf value/data
624 :raises: :py:class:`ValueError` if this simple cnf has more
628 raise ValueError('get_value only possible if len == 1 (is {0})!'
630 return self.__cnfvars[0]['data']
632 def get_single_dict(self):
634 Get a dictionary of a simple cnf of just 1 entry.
636 :returns: dictionary of a simple cnf
637 :rtype: {str, int or str or None}
640 raise ValueError('get_single_dict only possible if len == 1 (is {0})!'
642 return self.__cnfvars[0]
644 def __eq__(self, other_cnf):
646 Determine wether `self` == `other_cnf`.
648 :param other_cnf: cnf to compare with
649 :type other_cnf: :py:class:`SimpleCnf`
650 :returns: whether all cnf var entries are equal
653 key_func = lambda cnf_var_entry: cnf_var_entry['number']
655 if isinstance (other_cnf, SimpleCnf) is False:
658 return sorted(self.__cnfvars, key=key_func) \
659 == sorted(other_cnf.__cnfvars, key=key_func) # pylint: disable=protected-access