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 Guest utility to wrap arnied related functionality through python calls.
27 .. note:: Partially DEPRECATED! Use :py:mod:`pyi2ncommon.arnied_api` or
28 :py:mod:`pyi2ncommon.cnfvar` whenever possible. In particluar, do not
29 use or extend functionality regarding configuration (`get_cnf`,
32 Copyright: Intra2net AG
34 There are three types of setting some cnfvar configuration:
36 1) static (:py:class:`set_cnf`) - oldest method using a static preprocessed
37 config file without modifying its content in any way
38 2) semi-dynamic (:py:class:`set_cnf_semidynamic`) - old method also using
39 static file but rather as a template, replacing regex-matched values to
40 adapt it to different configurations
41 3) dynamic (:py:class:`set_cnf_dynamic`) - new method using dictionaries
42 and custom cnfvar classes and writing them into config files of a desired
43 format (json, cnf, or raw)
47 ------------------------------------------------------
59 log = logging.getLogger('pyi2ncommon.arnied_wrapper')
61 from . import cnfvar_old
66 #: default set_cnf binary
67 BIN_SET_CNF = "/usr/intranator/bin/set_cnf"
68 #: default arnied_helper binary
69 BIN_ARNIED_HELPER = "/usr/intranator/bin/arnied_helper"
70 #: default location for template configuration files
72 #: default location for dumped configuration files
76 class ConfigError(Exception):
80 def run_cmd(cmd="", ignore_errors=False, vm=None, timeout=60):
82 Universal command run wrapper.
84 :param str cmd: command to run
85 :param bool ignore_errors: whether not to raise error on command failure
86 :param vm: vm to run on if running on a guest instead of the host
87 :type vm: :py:class:`virttest.qemu_vm.VM` or None
88 :param int timeout: amount of seconds to wait for the program to run
89 :returns: command result output where output (stdout/stderr) is bytes
90 (encoding dependent on environment and command given)
91 :rtype: :py:class:`subprocess.CompletedProcess`
92 :raises: :py:class:`OSError` if command failed and cannot be ignored
95 status, stdout = vm.session.cmd_status_output(cmd, timeout=timeout)
96 stdout = stdout.encode()
101 if not ignore_errors:
102 raise subprocess.CalledProcessError(status, cmd, stderr=stderr)
103 return subprocess.CompletedProcess(cmd, status,
104 stdout=stdout, stderr=stderr)
106 return subprocess.run(cmd, check=not ignore_errors, shell=True,
110 def verify_running(process='arnied', timeout=60, vm=None):
112 Verify if a given process is running via 'pgrep'.
114 :param str process: process to verify if running
115 :param int timeout: run verification timeout
116 :param vm: vm to run on if running on a guest instead of the host
117 :type vm: :py:class:`virttest.qemu_vm.VM` or None
118 :raises: :py:class:`RuntimeError` if process is not running
123 platform_str = " on %s" % vm.name
124 for i in range(timeout):
125 log.info("Checking whether %s is running%s (%i\%i)",
126 process, platform_str, i, timeout)
127 result = run_cmd(cmd="pgrep -l -x %s" % process,
128 ignore_errors=True, vm=vm)
129 if result.returncode == 0:
133 raise RuntimeError("Process %s does not seem to be running" % process)
136 # Basic functionality
139 def accept_licence(vm=None):
141 Accept the Intra2net license.
143 :param vm: vm to run on if running on a guest instead of the host
144 :type vm: :py:class:`virttest.qemu_vm.VM` or None
146 This is mostly useful for simplified webpage access.
148 cmd = 'echo "LICENSE_ACCEPTED,0: \\"1\\"" | set_cnf'
149 result = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
151 wait_for_generate(vm=vm)
154 def go_online(provider_id, wait_online=True, timeout=60, vm=None):
156 Go online with the given provider id.
158 :param provider_id: provider to go online with
159 :type provider_id: int
160 :param wait_online: whether to wait until online
161 :type wait_online: bool
162 :param int timeout: Seconds to wait in :py:func:`wait_for_online`
163 :param vm: vm to run on if running on a guest instead of the host
164 :type vm: :py:class:`virttest.qemu_vm.VM` or None
166 .. seealso:: :py:func:`go_offline`, :py:func:`wait_for_online`
168 log.info("Switching to online mode with provider %d", provider_id)
170 get_cnf_res = run_cmd(cmd='get_cnf PROVIDER %d' % provider_id, vm=vm)
171 if b'PROVIDER,' not in get_cnf_res.stdout:
172 log.warning('There is no PROVIDER %d on the vm. Skipping go_online.',
176 cmd = 'tell-connd --online P%i' % provider_id
177 result = run_cmd(cmd=cmd, vm=vm)
181 wait_for_online(provider_id, timeout=timeout, vm=vm)
184 def go_offline(wait_offline=True, vm=None):
188 :param wait_offline: whether to wait until offline
189 :type wait_offline: bool
190 :param vm: vm to run on if running on a guest instead of the host
191 :type vm: :py:class:`virttest.qemu_vm.VM` or None
193 .. seealso:: :py:func:`go_online`, :py:func:`wait_for_offline`
195 cmd = 'tell-connd --offline'
196 result = run_cmd(cmd=cmd, vm=vm)
200 if wait_offline is True:
201 wait_for_offline(vm=vm)
203 wait_for_offline(wait_offline, vm=vm)
206 def wait_for_offline(timeout=60, vm=None):
208 Wait for arnied to signal we are offline.
210 :param int timeout: maximum timeout for waiting
211 :param vm: vm to run on if running on a guest instead of the host
212 :type vm: :py:class:`virttest.qemu_vm.VM` or None
214 _wait_for_online_status('offline', None, timeout, vm)
217 def wait_for_online(provider_id, timeout=60, vm=None):
219 Wait for arnied to signal we are online.
221 :param provider_id: provider to go online with
222 :type provider_id: int
223 :param int timeout: maximum timeout for waiting
224 :param vm: vm to run on if running on a guest instead of the host
225 :type vm: :py:class:`virttest.qemu_vm.VM` or None
227 _wait_for_online_status('online', provider_id, timeout, vm)
230 def _wait_for_online_status(status, provider_id, timeout, vm):
231 # Don't use tell-connd --status here since the actual
232 # ONLINE signal to arnied is transmitted
233 # asynchronously via arnieclient_muxer.
235 if status == 'online':
236 expected_output = 'DEFAULT: 2'
237 set_status_func = lambda: go_online(provider_id, False, vm)
238 elif status == 'offline':
239 expected_output = 'DEFAULT: 0'
240 set_status_func = lambda: go_offline(False, vm)
242 raise ValueError('expect status "online" or "offline", not "{0}"!'
245 log.info("Waiting for arnied to be {0} within {1} seconds"
246 .format(status, timeout))
248 for i in range(timeout):
249 # arnied might invalidate the connd "connection barrier"
250 # after generate was running and switch to OFFLINE (race condition).
251 # -> tell arnied every ten seconds to go online again
252 if i % 10 == 0 and i != 0:
255 cmd = '/usr/intranator/bin/get_var ONLINE'
256 result = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
259 if expected_output in result.stdout.decode():
260 log.info("arnied is {0}. Continuing.".format(status))
265 raise RuntimeError("We didn't manage to go {0} within {1} seconds\n"
266 .format(status, timeout))
269 def disable_virscan(vm=None):
271 Disable virscan that could block GENERATE and thus all configurations.
273 :param vm: vm to run on if running on a guest instead of the host
274 :type vm: :py:class:`virttest.qemu_vm.VM` or None
276 log.info("Disabling virus database update")
277 unset_cnf("VIRSCAN_UPDATE_CRON", vm=vm)
279 cmd = "echo 'VIRSCAN_UPDATE_DNS_PUSH,0:\"0\"' |set_cnf"
280 result = run_cmd(cmd=cmd, vm=vm)
283 # TODO: this intervention should be solved in later arnied_helper tool
284 cmd = "rm -f /var/intranator/schedule/UPDATE_VIRSCAN_NODIAL*"
285 result = run_cmd(cmd=cmd, vm=vm)
287 log.info("Virus database update disabled")
290 def email_transfer(vm=None):
292 Transfer all the emails using the guest tool arnied_helper.
294 :param vm: vm to run on if running on a guest instead of the host
295 :type vm: :py:class:`virttest.qemu_vm.VM` or None
297 cmd = f"{BIN_ARNIED_HELPER} --transfer-mail"
298 result = run_cmd(cmd=cmd, vm=vm)
302 def wait_for_email_transfer(timeout=300, vm=None):
304 Wait until the mail queue is empty and all emails are sent.
306 :param int timeout: email transfer timeout
307 :param vm: vm to run on if running on a guest instead of the host
308 :type vm: :py:class:`virttest.qemu_vm.VM` or None
310 for i in range(timeout):
312 # Retrigger mail queue in case something is deferred
313 # by an amavisd-new reconfiguration
314 run_cmd(cmd='postqueue -f', vm=vm)
315 log.info('Waiting for SMTP queue to get empty (%i/%i s)',
317 if not run_cmd(cmd='postqueue -j', vm=vm).stdout:
318 log.debug('SMTP queue is empty')
321 log.warning('Timeout reached but SMTP queue still not empty after {} s'
325 def schedule(program, exec_time=0, optional_args="", vm=None):
327 Schedule a program to be executed at a given unix time stamp.
329 :param str program: program whose execution is scheduled
330 :param int exec_time: scheduled time of program's execution
331 :param str optional_args: optional command line arguments
332 :param vm: vm to run on if running on a guest instead of the host
333 :type vm: :py:class:`virttest.qemu_vm.VM` or None
335 log.info("Scheduling %s to be executed at %i", program, exec_time)
336 schedule_dir = "/var/intranator/schedule"
337 # clean previous schedules of the same program
338 files = vm.session.cmd("ls " + schedule_dir).split() if vm else os.listdir(schedule_dir)
339 for file_name in files:
340 if file_name.startswith(program.upper()):
341 log.debug("Removing previous scheduled %s", file_name)
343 vm.session.cmd("rm -f " + os.path.join(schedule_dir, file_name))
345 os.unlink(os.path.join(schedule_dir, file_name))
347 contents = "%i\n%s\n" % (exec_time, optional_args)
349 tmp_file = tempfile.NamedTemporaryFile(mode="w+",
350 prefix=program.upper() + "_",
353 log.debug("Created temporary file %s", tmp_file.name)
354 tmp_file.write(contents)
356 moved_tmp_file = os.path.join(schedule_dir, os.path.basename(tmp_file.name))
359 vm.copy_files_to(tmp_file.name, moved_tmp_file)
360 os.remove(tmp_file.name)
362 shutil.move(tmp_file.name, moved_tmp_file)
364 log.debug("Moved temporary file to %s", moved_tmp_file)
367 def wait_for_run(program, timeout=300, retries=10, vm=None):
369 Wait for a program using the guest arnied_helper tool.
371 :param str program: scheduled or running program to wait for
372 :param int timeout: program run timeout
373 :param int retries: number of tries to verify that the program is scheduled or running
374 :param vm: vm to run on if running on a guest instead of the host
375 :type vm: :py:class:`virttest.qemu_vm.VM` or None
377 log.info("Waiting for program %s to finish with timeout %i",
379 for i in range(retries):
380 cmd = f"{BIN_ARNIED_HELPER} --is-scheduled-or-running " \
382 check_scheduled = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
383 if check_scheduled.returncode == 0:
387 log.warning("The program %s was not scheduled and is not running", program)
389 cmd = f"{BIN_ARNIED_HELPER} --wait-for-program-end " \
390 f"{program.upper()} --wait-for-program-timeout {timeout}"
391 # add one second to make sure arnied_helper is finished when we expire
392 result = run_cmd(cmd=cmd, vm=vm, timeout=timeout+1)
393 log.debug(result.stdout)
396 def wait_for_arnied(timeout=60, vm=None):
398 Wait for arnied socket to be ready.
400 :param int timeout: maximum number of seconds to wait
401 :param vm: vm to run on if running on a guest instead of the host
402 :type vm: :py:class:`virttest.qemu_vm.VM` or None
404 cmd = f"{BIN_ARNIED_HELPER} --wait-for-arnied-socket " \
405 f"--wait-for-arnied-socket-timeout {timeout}"
406 # add one second to make sure arnied_helper is finished when we expire
407 result = run_cmd(cmd=cmd, vm=vm, timeout=timeout+1)
408 log.debug(result.stdout)
411 # Configuration functionality
413 def get_cnf(cnf_key, cnf_index=1, regex=".*", compact=False, timeout=30, vm=None):
415 Query arnied for a `cnf_key` and extract some information via regex.
417 :param str cnf_key: queried cnf key
418 :param int cnf_index: index of the cnf key
419 :param str regex: regex to apply on the queried cnf key data
420 :param bool compact: whether to retrieve compact version of the matched cnf keys
421 :param int timeout: arnied run verification timeout
422 :param vm: vm to run on if running on a guest instead of the host
423 :type vm: :py:class:`virttest.qemu_vm.VM` or None
424 :returns: extracted information via the regex
427 If `cnf_index` is set to -1, retrieve and perform regex matching on all instances.
429 wait_for_arnied(timeout=timeout, vm=vm)
432 platform_str = " from %s" % vm.name
433 log.info("Extracting arnied value %s for %s%s using pattern %s",
434 cnf_index, cnf_key, platform_str, regex)
435 cmd = "get_cnf%s %s%s" % (" -c " if compact else "", cnf_key,
436 " %s" % cnf_index if cnf_index != -1 else "")
437 # get_cnf creates latin1-encoded output, transfer from VM removes non-ascii
438 output = run_cmd(cmd=cmd, vm=vm).stdout.decode('latin1')
439 return re.search(regex, output, flags=re.DOTALL)
442 def get_cnf_id(cnf_key, value, timeout=30, vm=None):
444 Get the id of a configuration of type `cnf_key` and name `value`.
446 :param str cnf_key: queried cnf key
447 :param str value: cnf value of the cnf key
448 :param int timeout: arnied run verification timeout
449 :param vm: vm to run on if running on a guest instead of the host
450 :type vm: :py:class:`virttest.qemu_vm.VM` or None
451 :returns: the cnf id or -1 if no such cnf variable
454 wait_for_arnied(timeout=timeout, vm=vm)
455 regex = "%s,(\d+): \"%s\"" % (cnf_key, value)
456 cnf_id = get_cnf(cnf_key, cnf_index=-1, regex=regex, compact=True, vm=vm)
460 cnf_id = int(cnf_id.group(1))
461 log.info("Retrieved id \"%s\" for %s is %i", value, cnf_key, cnf_id)
465 def get_cnfvar(varname=None, instance=None, data=None, timeout=30, vm=None):
467 Invoke get_cnf and return a nested CNF structure.
469 :param str varname: "varname" field of the CNF_VAR to look up
470 :param instance: "instance" of that variable to return
472 :param str data: "data" field by which the resulting CNF_VAR list should be filtered
473 :param int timeout: arnied run verification timeout
474 :param vm: vm to run on if running on a guest instead of the host
475 :type vm: :py:class:`virttest.qemu_vm.VM` or None
476 :returns: the resulting "cnfvar" structure or None if the lookup fails or the result could not be parsed
477 :rtype: cnfvar option
479 wait_for_arnied(timeout=timeout, vm=vm)
480 # firstly, build argv for get_cnf
481 cmd = ["get_cnf", "-j"]
482 if varname is not None:
483 cmd.append("%s" % varname)
485 cmd.append("%d" % instance)
486 cmd_line = " ".join(cmd)
489 result = run_cmd(cmd=cmd_line, vm=vm)
490 (status, raw) = result.returncode, result.stdout
492 log.info("error %d executing \"%s\"", status, cmd_line)
496 # reading was successful, attempt to parse what we got
498 # The output from "get_cnf -j" is already utf-8. This contrast with
499 # the output of "get_cnf" (no json) which is latin1.
500 if isinstance(raw, bytes):
501 raw = raw.decode("utf-8")
502 cnf = cnfvar_old.read_cnf_json(raw)
503 except TypeError as exn:
504 log.info("error \"%s\" parsing result of \"%s\"", exn, cmd_line)
506 except cnfvar_old.InvalidCNF as exn:
507 log.info("error \"%s\" validating result of \"%s\"", exn, cmd_line)
511 return cnfvar_old.get_vars(cnf, data=data)
516 def get_cnfvar_id(varname, data, timeout=30, vm=None):
518 Similar to :py:func:`get_cnf_id` but uses :py:func:`get_cnfvar`.
520 :param str varname: "varname" field of the CNF_VAR to look up
521 :param str data: "data" field by which the resulting CNF_VAR list should be filtered
522 :param int timeout: arnied run verification timeout
523 :param vm: vm to run on if running on a guest instead of the host
524 :type vm: :py:class:`virttest.qemu_vm.VM` or None
525 :returns: the cnf id or -1 if no such cnf variable
528 wait_for_arnied(timeout=timeout, vm=vm)
529 log.info("Extracting from arnied CNF_VAR %s with data %s",
531 cnf = get_cnfvar(varname=varname, data=data, vm=vm)
532 variables = cnf["cnf"]
533 if len(variables) == 0:
534 log.info("CNF_VAR extraction unsuccessful, defaulting to -1")
537 first_instance = int(variables[0]["instance"])
538 log.info("CNF_VAR instance lookup yielded %d results, returning first value (%d)",
539 len(variables), first_instance)
540 return first_instance
543 def wait_for_generate(timeout=300, vm=None):
545 Wait for the 'generate' program to complete.
547 Arguments are similar to the ones from :py:method:`wait_for_run`.
549 wait_for_run('generate', timeout=timeout, retries=1, vm=vm)
550 wait_for_run('generate_offline', timeout=timeout, retries=1, vm=vm)
553 def unset_cnf(varname="", instance="", timeout=30, vm=None):
555 Remove configuration from arnied.
557 :param str varname: "varname" field of the CNF_VAR to unset
558 :param int instance: "instance" of that variable to unset
559 :param int timeout: arnied run verification timeout
560 :param vm: vm to run on if running on a guest instead of the host
561 :type vm: :py:class:`virttest.qemu_vm.VM` or None
563 wait_for_arnied(timeout=timeout, vm=vm)
565 cmd = "get_cnf %s %s | set_cnf -x" % (varname, instance)
566 run_cmd(cmd=cmd, vm=vm)
568 wait_for_generate(vm=vm)
571 def set_cnf(config_files, kind="cnf", timeout=30, vm=None):
573 Perform static arnied configuration through a set of config files.
575 :param config_files: config files to use for the configuration
576 :type config_files: [str]
577 :param str kind: "json" or "cnf"
578 :param int timeout: arnied run verification timeout
579 :param vm: vm to run on if running on a guest instead of the host
580 :type vm: :py:class:`virttest.qemu_vm.VM` or None
581 :raises: :py:class:`ConfigError` if cannot apply file
583 The config files must be provided and are always expected to be found on
584 the host. If these are absolute paths, they will be kept as is or
585 otherwise will be searched for in `SRC_CONFIG_DIR`. If a vm is provided,
586 the config files will be copied there as temporary files before applying.
588 log.info("Setting arnied configuration")
589 wait_for_arnied(timeout=timeout, vm=vm)
591 config_paths = prep_config_paths(config_files)
592 for config_path in config_paths:
593 with open(config_path, "rt", errors='replace') as config:
594 log.debug("Contents of applied %s:\n%s", config_path, config.read())
596 new_config_path = generate_config_path()
597 vm.copy_files_to(config_path, new_config_path)
598 config_path = new_config_path
599 argv = ["set_cnf", kind == "json" and "-j" or "", config_path]
601 result = run_cmd(" ".join(argv), ignore_errors=True, vm=vm)
602 logging.debug(result)
603 if result.returncode != 0:
604 raise ConfigError("Failed to apply config %s%s, set_cnf returned %d"
606 " on %s" % vm.name if vm is not None else "",
610 wait_for_generate(vm=vm)
611 except Exception as ex:
612 # handle cases of remote configuration that leads to connection meltdown
613 if vm is not None and isinstance(ex, sys.modules["aexpect"].ShellProcessTerminatedError):
614 log.info("Resetting connection to %s", vm.name)
615 vm.session = vm.wait_for_login(timeout=10)
616 log.debug("Connection reset via remote error: %s", ex)
621 def set_cnf_semidynamic(config_files, params_dict, regex_dict=None,
622 kind="cnf", timeout=30, vm=None):
624 Perform semi-dynamic arnied configuration from an updated version of the
627 :param config_files: config files to use for the configuration
628 :type config_files: [str]
629 :param params_dict: parameters to override the defaults in the config files
630 :type params_dict: {str, str}
631 :param regex_dict: regular expressions to use for matching the overriden parameters
632 :type regex_dict: {str, str} or None
633 :param str kind: "json" or "cnf"
634 :param int timeout: arnied run verification timeout
635 :param vm: vm to run on if running on a guest instead of the host
636 :type vm: :py:class:`virttest.qemu_vm.VM` or None
638 The config files must be provided and are always expected to be found on
639 the host. If these are absolute paths, they will be kept as is or
640 otherwise will be searched for in `SRC_CONFIG_DIR`. If a vm is provided,
641 the config files will be copied there as temporary files before applying.
643 log.info("Performing semi-dynamic arnied configuration")
645 config_paths = prep_cnf(config_files, params_dict, regex_dict)
646 set_cnf(config_paths, kind=kind, timeout=timeout, vm=vm)
648 log.info("Semi-dynamic arnied configuration successful!")
651 def set_cnf_dynamic(cnf, config_file=None, kind="cnf", timeout=30, vm=None):
653 Perform dynamic arnied configuration from fully generated config files.
655 :param cnf: one key with the same value as *kind* and a list of cnfvars as value
656 :type cnf: {str, str}
657 :param config_file: optional user supplied filename
658 :type config_file: str or None
659 :param str kind: "json", "cnf", or "raw"
660 :param int timeout: arnied run verification timeout
661 :param vm: vm to run on if running on a guest instead of the host
662 :type vm: :py:class:`virttest.qemu_vm.VM` or None
663 :raises: :py:class:`ValueError` if `kind` is not an acceptable value
664 :raises: :py:class:`ConfigError` if cannot apply file
666 The config file might not be provided in which case a temporary file will
667 be generated and saved on the host's `DUMP_CONFIG_DIR` of not provided as
668 an absolute path. If a vm is provided, the config file will be copied there
669 as a temporary file before applying.
671 if config_file is None:
672 config_path = generate_config_path(dumped=True)
673 elif os.path.isabs(config_file):
674 config_path = config_file
676 config_path = os.path.join(os.path.abspath(DUMP_CONFIG_DIR), config_file)
677 generated = config_file is None
678 config_file = os.path.basename(config_path)
679 log.info("Using %s cnf file %s%s",
680 "generated" if generated else "user-supplied",
681 config_file, " on %s" % vm.name if vm is not None else "")
683 # Important to write bytes here to ensure text is encoded with latin-1
684 fd = open(config_path, "wb")
687 "raw": cnfvar_old.write_cnf_raw,
688 "json": cnfvar_old.write_cnf_json,
689 "cnf": cnfvar_old.write_cnf
691 SET_CNF_METHODS[kind](cnf, out=fd)
693 raise ValueError("Invalid set_cnf method \"%s\"; expected \"json\" or \"cnf\""
697 log.info("Generated config file %s", config_path)
699 kind = "cnf" if kind != "json" else kind
700 set_cnf([config_path], kind=kind, timeout=timeout, vm=vm)
703 def set_cnf_pipe(cnf, timeout=30, block=False):
705 Set local configuration by talking to arnied via ``set_cnf``.
707 :param cnf: one key with the same value as *kind* and a list of cnfvars as value
708 :type cnf: {str, str}
709 :param int timeout: arnied run verification timeout
710 :param bool block: whether to wait for generate to complete the
712 :returns: whether ``set_cnf`` succeeded or not
715 This is obviously not generic but supposed to be run on the guest.
717 log.info("Setting arnied configuration through local pipe")
718 wait_for_arnied(timeout=timeout)
720 st, out, exit = sysmisc.run_cmd_with_pipe([BIN_SET_CNF, "-j"], inp=str(cnf))
723 log.error("Error applying configuration; status=%r" % exit)
724 log.error("and stderr:\n%s" % out)
726 log.debug("Configuration successfully passed to set_cnf, "
727 "read %d B from pipe" % len(out))
730 log.debug("Waiting for config job to complete")
733 log.debug("Exiting sucessfully")
737 def prep_config_paths(config_files, config_dir=None):
739 Prepare absolute paths for all configs at an expected location.
741 :param config_files: config files to use for the configuration
742 :type config_files: [str]
743 :param config_dir: config directory to prepend to the filepaths
744 :type config_dir: str or None
745 :returns: list of the full config paths
748 if config_dir is None:
749 config_dir = SRC_CONFIG_DIR
751 for config_file in config_files:
752 if os.path.isabs(config_file):
753 # Absolute path: The user requested a specific file
754 # f.e. needed for dynamic arnied config update
755 config_path = config_file
757 config_path = os.path.join(os.path.abspath(config_dir),
759 logging.debug("Using %s for original path %s", config_path, config_file)
760 config_paths.append(config_path)
764 def prep_cnf_value(config_file, value,
765 regex=None, template_key=None, ignore_fail=False):
767 Replace value in a provided arnied config file.
769 :param str config_file: file to use for the replacement
770 :param str value: value to replace the first matched group with
771 :param regex: regular expression to use when replacing a cnf value
772 :type regex: str or None
773 :param template_key: key of a quick template to use for the regex
774 :type template_key: str or None
775 :param bool ignore_fail: whether to ignore regex mismatching
776 :raises: :py:class:`ValueError` if (also default) `regex` doesn't have a match
778 In order to ensure better matching capabilities you are supposed to
779 provide a regex pattern with at least one subgroup to match your value.
780 What this means is that the value you like to replace is not directly
781 searched into the config text but matched within a larger regex in
782 in order to avoid any mismatch.
785 provider.cnf, 'PROVIDER_LOCALIP,0: "(\d+)"', 127.0.0.1
787 if template_key is None:
788 pattern = regex.encode()
790 samples = {"provider": 'PROVIDER_LOCALIP,\d+: "(\d+\.\d+\.\d+\.\d+)"',
791 "global_destination_addr": 'SPAMFILTER_GLOBAL_DESTINATION_ADDR,0: "bounce_target@(.*)"'}
792 pattern = samples[template_key].encode()
794 with open(config_file, "rb") as file_handle:
795 text = file_handle.read()
796 match_line = re.search(pattern, text)
798 if match_line is None and not ignore_fail:
799 raise ValueError("Pattern %s not found in %s" % (pattern, config_file))
800 elif match_line is not None:
801 old_line = match_line.group(0)
802 text = text[:match_line.start(1)] + value.encode() + text[match_line.end(1):]
803 line = re.search(pattern, text).group(0)
804 log.debug("Updating %s to %s in %s", old_line, line, config_file)
805 with open(config_file, "wb") as file_handle:
806 file_handle.write(text)
809 def prep_cnf(config_files, params_dict, regex_dict=None):
811 Update all config files with the default overriding parameters,
812 i.e. override the values hard-coded in those config files.
814 :param config_files: config files to use for the configuration
815 :type config_files: [str]
816 :param params_dict: parameters to override the defaults in the config files
817 :type params_dict: {str, str}
818 :param regex_dict: regular expressions to use for matching the overriden parameters
819 :type regex_dict: {str, str} or None
820 :returns: list of prepared (modified) config paths
823 log.info("Preparing %s template config files", len(config_files))
825 src_config_paths = prep_config_paths(config_files)
826 new_config_paths = []
827 for config_path in src_config_paths:
828 new_config_path = generate_config_path(dumped=True)
829 shutil.copy(config_path, new_config_path)
830 new_config_paths.append(new_config_path)
832 for config_path in new_config_paths:
833 for param_key in params_dict.keys():
834 if regex_dict is None:
835 regex_val = "\s+%s,\d+: \"(.*)\"" % param_key.upper()
836 elif param_key in regex_dict.keys():
837 regex_val = regex_dict[param_key] % param_key.upper()
838 elif re.match("\w*_\d+$", param_key):
839 final_parameter, parent_id = \
840 re.match("(\w*)_(\d+)$", param_key).group(1, 2)
841 regex_val = "\(%s\) %s,\d+: \"(.*)\"" \
842 % (parent_id, final_parameter.upper())
843 log.debug("Requested regex for %s is '%s'",
844 param_key, regex_val)
846 regex_val = "\s+%s,\d+: \"(.*)\"" % param_key.upper()
847 prep_cnf_value(config_path, params_dict[param_key],
848 regex=regex_val, ignore_fail=True)
849 log.info("Prepared template config file %s", config_path)
851 return new_config_paths
854 def generate_config_path(dumped=False):
856 Generate path for a temporary config name.
858 :param bool dumped: whether the file should be in the dump
859 directory or in temporary directory
860 :returns: generated config file path
863 dir = os.path.abspath(DUMP_CONFIG_DIR) if dumped else None
864 fd, filename = tempfile.mkstemp(suffix=".cnf", dir=dir)