Convert the old cnfvar module into a cnfvar string parsing module
[pyi2ncommon] / src / arnied_wrapper.py
CommitLineData
11cbb815
PD
1# The software in this package is distributed under the GNU General
2# Public License version 2 (with a special exception described below).
3#
4# A copy of GNU General Public License (GPL) is included in this distribution,
5# in the file COPYING.GPL.
6#
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.
12#
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.
15#
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.
18#
19# Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com>
20
f49f6323
PD
21"""
22
23SUMMARY
24------------------------------------------------------
25Guest utility to wrap arnied related functionality through python calls.
26
b7e04a3e
CH
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`,
30 `set_cnf`).
f49f6323 31
d7cdea2c 32Copyright: Intra2net AG
f49f6323
PD
33
34There are three types of setting some cnfvar configuration:
35
361) static (:py:class:`set_cnf`) - oldest method using a static preprocessed
37 config file without modifying its content in any way
382) 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
413) 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)
44
f49f6323
PD
45
46INTERFACE
47------------------------------------------------------
48
49"""
50
51import os
52import sys
53import time
54import re
55import subprocess
56import shutil
57import tempfile
58import logging
3de8b4d8 59log = logging.getLogger('pyi2ncommon.arnied_wrapper')
f49f6323 60
30521dad 61from . import sysmisc
7102665a 62
30521dad 63
f49f6323
PD
64
65#: default set_cnf binary
66BIN_SET_CNF = "/usr/intranator/bin/set_cnf"
f1ca3964
SA
67#: default arnied_helper binary
68BIN_ARNIED_HELPER = "/usr/intranator/bin/arnied_helper"
f49f6323
PD
69#: default location for template configuration files
70SRC_CONFIG_DIR = "."
71#: default location for dumped configuration files
72DUMP_CONFIG_DIR = "."
73
74
75class ConfigError(Exception):
76 pass
77
78
dfa7c024 79def run_cmd(cmd="", ignore_errors=False, vm=None, timeout=60):
f49f6323
PD
80 """
81 Universal command run wrapper.
82
83 :param str cmd: command to run
84 :param bool ignore_errors: whether not to raise error on command failure
85 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 86 :type vm: :py:class:`virttest.qemu_vm.VM` or None
dfa7c024 87 :param int timeout: amount of seconds to wait for the program to run
2dd87078
CH
88 :returns: command result output where output (stdout/stderr) is bytes
89 (encoding dependent on environment and command given)
90 :rtype: :py:class:`subprocess.CompletedProcess`
f49f6323
PD
91 :raises: :py:class:`OSError` if command failed and cannot be ignored
92 """
93 if vm is not None:
dfa7c024 94 status, stdout = vm.session.cmd_status_output(cmd, timeout=timeout)
f49f6323
PD
95 stdout = stdout.encode()
96 stderr = b""
97 if status != 0:
98 stderr = stdout
99 stdout = b""
100 if not ignore_errors:
101 raise subprocess.CalledProcessError(status, cmd, stderr=stderr)
2dd87078
CH
102 return subprocess.CompletedProcess(cmd, status,
103 stdout=stdout, stderr=stderr)
f49f6323 104 else:
2dd87078
CH
105 return subprocess.run(cmd, check=not ignore_errors, shell=True,
106 capture_output=True)
f49f6323
PD
107
108
109def verify_running(process='arnied', timeout=60, vm=None):
110 """
111 Verify if a given process is running via 'pgrep'.
f49f6323
PD
112
113 :param str process: process to verify if running
114 :param int timeout: run verification timeout
115 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 116 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
117 :raises: :py:class:`RuntimeError` if process is not running
118 """
119 platform_str = ""
120 if vm is not None:
121 vm.verify_alive()
122 platform_str = " on %s" % vm.name
123 for i in range(timeout):
124 log.info("Checking whether %s is running%s (%i\%i)",
125 process, platform_str, i, timeout)
126 result = run_cmd(cmd="pgrep -l -x %s" % process,
127 ignore_errors=True, vm=vm)
128 if result.returncode == 0:
129 log.debug(result)
130 return
131 time.sleep(1)
132 raise RuntimeError("Process %s does not seem to be running" % process)
133
134
135# Basic functionality
136
137
138def accept_licence(vm=None):
139 """
140 Accept the Intra2net license.
141
142 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 143 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
144
145 This is mostly useful for simplified webpage access.
146 """
147 cmd = 'echo "LICENSE_ACCEPTED,0: \\"1\\"" | set_cnf'
148 result = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
149 log.debug(result)
1ac0318c 150 wait_for_generate(vm=vm)
f49f6323
PD
151
152
153def go_online(provider_id, wait_online=True, timeout=60, vm=None):
154 """
155 Go online with the given provider id.
156
157 :param provider_id: provider to go online with
158 :type provider_id: int
159 :param wait_online: whether to wait until online
160 :type wait_online: bool
7628bc48 161 :param int timeout: Seconds to wait in :py:func:`wait_for_online`
f49f6323 162 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 163 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
164
165 .. seealso:: :py:func:`go_offline`, :py:func:`wait_for_online`
166 """
167 log.info("Switching to online mode with provider %d", provider_id)
168
169 get_cnf_res = run_cmd(cmd='get_cnf PROVIDER %d' % provider_id, vm=vm)
170 if b'PROVIDER,' not in get_cnf_res.stdout:
33b0346a
CH
171 log.warning('There is no PROVIDER %d on the vm. Skipping go_online.',
172 provider_id)
f49f6323
PD
173 return
174
175 cmd = 'tell-connd --online P%i' % provider_id
176 result = run_cmd(cmd=cmd, vm=vm)
177 log.debug(result)
178
179 if wait_online:
180 wait_for_online(provider_id, timeout=timeout, vm=vm)
181
182
183def go_offline(wait_offline=True, vm=None):
184 """
185 Go offline.
186
187 :param wait_offline: whether to wait until offline
188 :type wait_offline: bool
189 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 190 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
191
192 .. seealso:: :py:func:`go_online`, :py:func:`wait_for_offline`
193 """
194 cmd = 'tell-connd --offline'
195 result = run_cmd(cmd=cmd, vm=vm)
196 log.debug(result)
197
198 if wait_offline:
199 if wait_offline is True:
200 wait_for_offline(vm=vm)
201 else:
202 wait_for_offline(wait_offline, vm=vm)
203
204
205def wait_for_offline(timeout=60, vm=None):
206 """
207 Wait for arnied to signal we are offline.
208
209 :param int timeout: maximum timeout for waiting
210 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 211 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
212 """
213 _wait_for_online_status('offline', None, timeout, vm)
214
215
216def wait_for_online(provider_id, timeout=60, vm=None):
217 """
218 Wait for arnied to signal we are online.
219
220 :param provider_id: provider to go online with
221 :type provider_id: int
222 :param int timeout: maximum timeout for waiting
223 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 224 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
225 """
226 _wait_for_online_status('online', provider_id, timeout, vm)
227
228
229def _wait_for_online_status(status, provider_id, timeout, vm):
230 # Don't use tell-connd --status here since the actual
231 # ONLINE signal to arnied is transmitted
232 # asynchronously via arnieclient_muxer.
233
234 if status == 'online':
235 expected_output = 'DEFAULT: 2'
236 set_status_func = lambda: go_online(provider_id, False, vm)
237 elif status == 'offline':
238 expected_output = 'DEFAULT: 0'
239 set_status_func = lambda: go_offline(False, vm)
240 else:
241 raise ValueError('expect status "online" or "offline", not "{0}"!'
242 .format(status))
243
244 log.info("Waiting for arnied to be {0} within {1} seconds"
245 .format(status, timeout))
246
247 for i in range(timeout):
248 # arnied might invalidate the connd "connection barrier"
249 # after generate was running and switch to OFFLINE (race condition).
250 # -> tell arnied every ten seconds to go online again
251 if i % 10 == 0 and i != 0:
252 set_status_func()
253
254 cmd = '/usr/intranator/bin/get_var ONLINE'
255 result = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
256 log.debug(result)
257
258 if expected_output in result.stdout.decode():
259 log.info("arnied is {0}. Continuing.".format(status))
260 return
261
262 time.sleep(1)
263
264 raise RuntimeError("We didn't manage to go {0} within {1} seconds\n"
265 .format(status, timeout))
266
267
268def disable_virscan(vm=None):
269 """
270 Disable virscan that could block GENERATE and thus all configurations.
271
272 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 273 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
274 """
275 log.info("Disabling virus database update")
276 unset_cnf("VIRSCAN_UPDATE_CRON", vm=vm)
277
278 cmd = "echo 'VIRSCAN_UPDATE_DNS_PUSH,0:\"0\"' |set_cnf"
279 result = run_cmd(cmd=cmd, vm=vm)
280 log.debug(result)
281
282 # TODO: this intervention should be solved in later arnied_helper tool
283 cmd = "rm -f /var/intranator/schedule/UPDATE_VIRSCAN_NODIAL*"
284 result = run_cmd(cmd=cmd, vm=vm)
285 log.debug(result)
286 log.info("Virus database update disabled")
287
288
289def email_transfer(vm=None):
290 """
291 Transfer all the emails using the guest tool arnied_helper.
292
293 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 294 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323 295 """
f1ca3964 296 cmd = f"{BIN_ARNIED_HELPER} --transfer-mail"
f49f6323
PD
297 result = run_cmd(cmd=cmd, vm=vm)
298 log.debug(result)
299
300
301def wait_for_email_transfer(timeout=300, vm=None):
302 """
303 Wait until the mail queue is empty and all emails are sent.
304
305 :param int timeout: email transfer timeout
306 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 307 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
308 """
309 for i in range(timeout):
310 if i % 10 == 0:
311 # Retrigger mail queue in case something is deferred
312 # by an amavisd-new reconfiguration
313 run_cmd(cmd='postqueue -f', vm=vm)
12d603bf 314 log.info('Waiting for SMTP queue to get empty (%i/%i s)',
f49f6323 315 i, timeout)
68d12e8a 316 if not run_cmd(cmd='postqueue -j', vm=vm).stdout:
12d603bf
CH
317 log.debug('SMTP queue is empty')
318 return
f49f6323 319 time.sleep(1)
12d603bf
CH
320 log.warning('Timeout reached but SMTP queue still not empty after {} s'
321 .format(timeout))
f49f6323
PD
322
323
324def schedule(program, exec_time=0, optional_args="", vm=None):
325 """
326 Schedule a program to be executed at a given unix time stamp.
327
328 :param str program: program whose execution is scheduled
329 :param int exec_time: scheduled time of program's execution
330 :param str optional_args: optional command line arguments
331 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 332 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
333 """
334 log.info("Scheduling %s to be executed at %i", program, exec_time)
f49f6323 335 schedule_dir = "/var/intranator/schedule"
ef5c0c20 336 # clean previous schedules of the same program
f49f6323
PD
337 files = vm.session.cmd("ls " + schedule_dir).split() if vm else os.listdir(schedule_dir)
338 for file_name in files:
339 if file_name.startswith(program.upper()):
340 log.debug("Removing previous scheduled %s", file_name)
341 if vm:
ef5c0c20 342 vm.session.cmd("rm -f " + os.path.join(schedule_dir, file_name))
f49f6323
PD
343 else:
344 os.unlink(os.path.join(schedule_dir, file_name))
ef5c0c20
SA
345
346 contents = "%i\n%s\n" % (exec_time, optional_args)
347
348 tmp_file = tempfile.NamedTemporaryFile(mode="w+",
349 prefix=program.upper() + "_",
350 dir=DUMP_CONFIG_DIR,
351 delete=False)
352 log.debug("Created temporary file %s", tmp_file.name)
353 tmp_file.write(contents)
354 tmp_file.close()
355 moved_tmp_file = os.path.join(schedule_dir, os.path.basename(tmp_file.name))
356
f49f6323 357 if vm:
ef5c0c20
SA
358 vm.copy_files_to(tmp_file.name, moved_tmp_file)
359 os.remove(tmp_file.name)
f49f6323
PD
360 else:
361 shutil.move(tmp_file.name, moved_tmp_file)
ef5c0c20 362
f49f6323
PD
363 log.debug("Moved temporary file to %s", moved_tmp_file)
364
365
366def wait_for_run(program, timeout=300, retries=10, vm=None):
367 """
368 Wait for a program using the guest arnied_helper tool.
369
370 :param str program: scheduled or running program to wait for
371 :param int timeout: program run timeout
372 :param int retries: number of tries to verify that the program is scheduled or running
373 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 374 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
375 """
376 log.info("Waiting for program %s to finish with timeout %i",
377 program, timeout)
378 for i in range(retries):
f1ca3964 379 cmd = f"{BIN_ARNIED_HELPER} --is-scheduled-or-running " \
f49f6323
PD
380 + program.upper()
381 check_scheduled = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
382 if check_scheduled.returncode == 0:
383 break
384 time.sleep(1)
80d82f13
PD
385 else:
386 log.warning("The program %s was not scheduled and is not running", program)
387 return
f1ca3964
SA
388 cmd = f"{BIN_ARNIED_HELPER} --wait-for-program-end " \
389 f"{program.upper()} --wait-for-program-timeout {timeout}"
390 # add one second to make sure arnied_helper is finished when we expire
391 result = run_cmd(cmd=cmd, vm=vm, timeout=timeout+1)
392 log.debug(result.stdout)
393
394
395def wait_for_arnied(timeout=60, vm=None):
396 """
397 Wait for arnied socket to be ready.
398
399 :param int timeout: maximum number of seconds to wait
400 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 401 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f1ca3964
SA
402 """
403 cmd = f"{BIN_ARNIED_HELPER} --wait-for-arnied-socket " \
404 f"--wait-for-arnied-socket-timeout {timeout}"
3472b405
SA
405 # add one second to make sure arnied_helper is finished when we expire
406 result = run_cmd(cmd=cmd, vm=vm, timeout=timeout+1)
f49f6323
PD
407 log.debug(result.stdout)
408
409
410# Configuration functionality
411
412def get_cnf(cnf_key, cnf_index=1, regex=".*", compact=False, timeout=30, vm=None):
413 """
414 Query arnied for a `cnf_key` and extract some information via regex.
415
416 :param str cnf_key: queried cnf key
417 :param int cnf_index: index of the cnf key
418 :param str regex: regex to apply on the queried cnf key data
419 :param bool compact: whether to retrieve compact version of the matched cnf keys
420 :param int timeout: arnied run verification timeout
421 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 422 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
423 :returns: extracted information via the regex
424 :rtype: Match object
425
426 If `cnf_index` is set to -1, retrieve and perform regex matching on all instances.
427 """
f1ca3964 428 wait_for_arnied(timeout=timeout, vm=vm)
f49f6323
PD
429 platform_str = ""
430 if vm is not None:
431 platform_str = " from %s" % vm.name
432 log.info("Extracting arnied value %s for %s%s using pattern %s",
433 cnf_index, cnf_key, platform_str, regex)
434 cmd = "get_cnf%s %s%s" % (" -c " if compact else "", cnf_key,
435 " %s" % cnf_index if cnf_index != -1 else "")
6c691734
CH
436 # get_cnf creates latin1-encoded output, transfer from VM removes non-ascii
437 output = run_cmd(cmd=cmd, vm=vm).stdout.decode('latin1')
f49f6323
PD
438 return re.search(regex, output, flags=re.DOTALL)
439
440
441def get_cnf_id(cnf_key, value, timeout=30, vm=None):
442 """
443 Get the id of a configuration of type `cnf_key` and name `value`.
444
445 :param str cnf_key: queried cnf key
446 :param str value: cnf value of the cnf key
447 :param int timeout: arnied run verification timeout
448 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 449 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
450 :returns: the cnf id or -1 if no such cnf variable
451 :rtype: int
452 """
f1ca3964 453 wait_for_arnied(timeout=timeout, vm=vm)
f49f6323
PD
454 regex = "%s,(\d+): \"%s\"" % (cnf_key, value)
455 cnf_id = get_cnf(cnf_key, cnf_index=-1, regex=regex, compact=True, vm=vm)
456 if cnf_id is None:
457 cnf_id = -1
458 else:
459 cnf_id = int(cnf_id.group(1))
460 log.info("Retrieved id \"%s\" for %s is %i", value, cnf_key, cnf_id)
461 return cnf_id
462
463
80d82f13 464def wait_for_generate(timeout=300, vm=None):
f49f6323
PD
465 """
466 Wait for the 'generate' program to complete.
467
80d82f13 468 Arguments are similar to the ones from :py:method:`wait_for_run`.
f49f6323 469 """
80d82f13
PD
470 wait_for_run('generate', timeout=timeout, retries=1, vm=vm)
471 wait_for_run('generate_offline', timeout=timeout, retries=1, vm=vm)
f49f6323
PD
472
473
d9446155 474def unset_cnf(varname="", instance="", timeout=30, vm=None):
f49f6323
PD
475 """
476 Remove configuration from arnied.
477
478 :param str varname: "varname" field of the CNF_VAR to unset
479 :param int instance: "instance" of that variable to unset
d9446155 480 :param int timeout: arnied run verification timeout
f49f6323 481 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 482 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323 483 """
f1ca3964 484 wait_for_arnied(timeout=timeout, vm=vm)
d9446155 485
f49f6323
PD
486 cmd = "get_cnf %s %s | set_cnf -x" % (varname, instance)
487 run_cmd(cmd=cmd, vm=vm)
488
80d82f13 489 wait_for_generate(vm=vm)
f49f6323
PD
490
491
492def set_cnf(config_files, kind="cnf", timeout=30, vm=None):
493 """
494 Perform static arnied configuration through a set of config files.
495
496 :param config_files: config files to use for the configuration
497 :type config_files: [str]
498 :param str kind: "json" or "cnf"
499 :param int timeout: arnied run verification timeout
500 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 501 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
502 :raises: :py:class:`ConfigError` if cannot apply file
503
504 The config files must be provided and are always expected to be found on
505 the host. If these are absolute paths, they will be kept as is or
506 otherwise will be searched for in `SRC_CONFIG_DIR`. If a vm is provided,
507 the config files will be copied there as temporary files before applying.
b0075fa2
PD
508
509 ..todo:: The static method must be deprecated after we drop and convert
510 lots of use cases for it to dynamic only.
f49f6323
PD
511 """
512 log.info("Setting arnied configuration")
f1ca3964 513 wait_for_arnied(timeout=timeout, vm=vm)
f49f6323
PD
514
515 config_paths = prep_config_paths(config_files)
516 for config_path in config_paths:
517 with open(config_path, "rt", errors='replace') as config:
518 log.debug("Contents of applied %s:\n%s", config_path, config.read())
519 if vm is not None:
520 new_config_path = generate_config_path()
521 vm.copy_files_to(config_path, new_config_path)
522 config_path = new_config_path
523 argv = ["set_cnf", kind == "json" and "-j" or "", config_path]
524
525 result = run_cmd(" ".join(argv), ignore_errors=True, vm=vm)
526 logging.debug(result)
527 if result.returncode != 0:
528 raise ConfigError("Failed to apply config %s%s, set_cnf returned %d"
529 % (config_path,
530 " on %s" % vm.name if vm is not None else "",
531 result.returncode))
532
533 try:
80d82f13 534 wait_for_generate(vm=vm)
f49f6323
PD
535 except Exception as ex:
536 # handle cases of remote configuration that leads to connection meltdown
537 if vm is not None and isinstance(ex, sys.modules["aexpect"].ShellProcessTerminatedError):
538 log.info("Resetting connection to %s", vm.name)
539 vm.session = vm.wait_for_login(timeout=10)
540 log.debug("Connection reset via remote error: %s", ex)
541 else:
542 raise ex
543
544
545def set_cnf_semidynamic(config_files, params_dict, regex_dict=None,
546 kind="cnf", timeout=30, vm=None):
547 """
548 Perform semi-dynamic arnied configuration from an updated version of the
549 config files.
550
551 :param config_files: config files to use for the configuration
552 :type config_files: [str]
553 :param params_dict: parameters to override the defaults in the config files
554 :type params_dict: {str, str}
555 :param regex_dict: regular expressions to use for matching the overriden parameters
556 :type regex_dict: {str, str} or None
557 :param str kind: "json" or "cnf"
558 :param int timeout: arnied run verification timeout
559 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 560 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
561
562 The config files must be provided and are always expected to be found on
563 the host. If these are absolute paths, they will be kept as is or
564 otherwise will be searched for in `SRC_CONFIG_DIR`. If a vm is provided,
565 the config files will be copied there as temporary files before applying.
b0075fa2
PD
566
567 ..todo:: The semi-dynamic method must be deprecated after we drop and convert
568 lots of use cases for it to dynamic only.
f49f6323
PD
569 """
570 log.info("Performing semi-dynamic arnied configuration")
571
572 config_paths = prep_cnf(config_files, params_dict, regex_dict)
573 set_cnf(config_paths, kind=kind, timeout=timeout, vm=vm)
574
575 log.info("Semi-dynamic arnied configuration successful!")
576
577
f49f6323
PD
578def set_cnf_pipe(cnf, timeout=30, block=False):
579 """
580 Set local configuration by talking to arnied via ``set_cnf``.
581
582 :param cnf: one key with the same value as *kind* and a list of cnfvars as value
583 :type cnf: {str, str}
584 :param int timeout: arnied run verification timeout
585 :param bool block: whether to wait for generate to complete the
586 configuration change
587 :returns: whether ``set_cnf`` succeeded or not
588 :rtype: bool
589
590 This is obviously not generic but supposed to be run on the guest.
591 """
592 log.info("Setting arnied configuration through local pipe")
f1ca3964 593 wait_for_arnied(timeout=timeout)
f49f6323
PD
594
595 st, out, exit = sysmisc.run_cmd_with_pipe([BIN_SET_CNF, "-j"], inp=str(cnf))
596
597 if st is False:
598 log.error("Error applying configuration; status=%r" % exit)
599 log.error("and stderr:\n%s" % out)
600 return False
601 log.debug("Configuration successfully passed to set_cnf, "
602 "read %d B from pipe" % len(out))
603
604 if block is True:
605 log.debug("Waiting for config job to complete")
606 wait_for_generate()
607
608 log.debug("Exiting sucessfully")
609 return True
610
611
612def prep_config_paths(config_files, config_dir=None):
613 """
614 Prepare absolute paths for all configs at an expected location.
615
616 :param config_files: config files to use for the configuration
617 :type config_files: [str]
618 :param config_dir: config directory to prepend to the filepaths
619 :type config_dir: str or None
620 :returns: list of the full config paths
621 :rtype: [str]
622 """
623 if config_dir is None:
624 config_dir = SRC_CONFIG_DIR
625 config_paths = []
626 for config_file in config_files:
627 if os.path.isabs(config_file):
628 # Absolute path: The user requested a specific file
629 # f.e. needed for dynamic arnied config update
630 config_path = config_file
631 else:
632 config_path = os.path.join(os.path.abspath(config_dir),
633 config_file)
634 logging.debug("Using %s for original path %s", config_path, config_file)
635 config_paths.append(config_path)
636 return config_paths
637
638
639def prep_cnf_value(config_file, value,
640 regex=None, template_key=None, ignore_fail=False):
641 """
642 Replace value in a provided arnied config file.
643
644 :param str config_file: file to use for the replacement
645 :param str value: value to replace the first matched group with
646 :param regex: regular expression to use when replacing a cnf value
647 :type regex: str or None
648 :param template_key: key of a quick template to use for the regex
649 :type template_key: str or None
650 :param bool ignore_fail: whether to ignore regex mismatching
651 :raises: :py:class:`ValueError` if (also default) `regex` doesn't have a match
652
653 In order to ensure better matching capabilities you are supposed to
654 provide a regex pattern with at least one subgroup to match your value.
655 What this means is that the value you like to replace is not directly
656 searched into the config text but matched within a larger regex in
657 in order to avoid any mismatch.
658
659 Example:
660 provider.cnf, 'PROVIDER_LOCALIP,0: "(\d+)"', 127.0.0.1
661 """
662 if template_key is None:
663 pattern = regex.encode()
664 else:
665 samples = {"provider": 'PROVIDER_LOCALIP,\d+: "(\d+\.\d+\.\d+\.\d+)"',
666 "global_destination_addr": 'SPAMFILTER_GLOBAL_DESTINATION_ADDR,0: "bounce_target@(.*)"'}
667 pattern = samples[template_key].encode()
668
669 with open(config_file, "rb") as file_handle:
670 text = file_handle.read()
671 match_line = re.search(pattern, text)
672
673 if match_line is None and not ignore_fail:
674 raise ValueError("Pattern %s not found in %s" % (pattern, config_file))
675 elif match_line is not None:
676 old_line = match_line.group(0)
677 text = text[:match_line.start(1)] + value.encode() + text[match_line.end(1):]
678 line = re.search(pattern, text).group(0)
679 log.debug("Updating %s to %s in %s", old_line, line, config_file)
680 with open(config_file, "wb") as file_handle:
681 file_handle.write(text)
682
683
684def prep_cnf(config_files, params_dict, regex_dict=None):
685 """
bcd9beb1 686 Update all config files with the default overriding parameters,
f49f6323
PD
687 i.e. override the values hard-coded in those config files.
688
689 :param config_files: config files to use for the configuration
690 :type config_files: [str]
691 :param params_dict: parameters to override the defaults in the config files
692 :type params_dict: {str, str}
693 :param regex_dict: regular expressions to use for matching the overriden parameters
694 :type regex_dict: {str, str} or None
695 :returns: list of prepared (modified) config paths
696 :rtype: [str]
697 """
698 log.info("Preparing %s template config files", len(config_files))
699
700 src_config_paths = prep_config_paths(config_files)
701 new_config_paths = []
702 for config_path in src_config_paths:
703 new_config_path = generate_config_path(dumped=True)
704 shutil.copy(config_path, new_config_path)
705 new_config_paths.append(new_config_path)
706
707 for config_path in new_config_paths:
708 for param_key in params_dict.keys():
709 if regex_dict is None:
710 regex_val = "\s+%s,\d+: \"(.*)\"" % param_key.upper()
711 elif param_key in regex_dict.keys():
712 regex_val = regex_dict[param_key] % param_key.upper()
713 elif re.match("\w*_\d+$", param_key):
714 final_parameter, parent_id = \
715 re.match("(\w*)_(\d+)$", param_key).group(1, 2)
716 regex_val = "\(%s\) %s,\d+: \"(.*)\"" \
717 % (parent_id, final_parameter.upper())
718 log.debug("Requested regex for %s is '%s'",
719 param_key, regex_val)
720 else:
721 regex_val = "\s+%s,\d+: \"(.*)\"" % param_key.upper()
722 prep_cnf_value(config_path, params_dict[param_key],
723 regex=regex_val, ignore_fail=True)
724 log.info("Prepared template config file %s", config_path)
725
726 return new_config_paths
727
728
729def generate_config_path(dumped=False):
730 """
731 Generate path for a temporary config name.
732
733 :param bool dumped: whether the file should be in the dump
734 directory or in temporary directory
735 :returns: generated config file path
736 :rtype: str
737 """
738 dir = os.path.abspath(DUMP_CONFIG_DIR) if dumped else None
739 fd, filename = tempfile.mkstemp(suffix=".cnf", dir=dir)
740 os.close(fd)
741 os.unlink(filename)
742 return filename