Fix get_cnf if working on local machine
[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
27Copyright: Intra2net AG
28
29
30There are three types of setting some cnfvar configuration:
31
321) static (:py:class:`set_cnf`) - oldest method using a static preprocessed
33 config file without modifying its content in any way
342) semi-dynamic (:py:class:`set_cnf_semidynamic`) - old method also using
35 static file but rather as a template, replacing regex-matched values to
36 adapt it to different configurations
373) dynamic (:py:class:`set_cnf_dynamic`) - new method using dictionaries
38 and custom cnfvar classes and writing them into config files of a desired
39 format (json, cnf, or raw)
40
f49f6323
PD
41
42INTERFACE
43------------------------------------------------------
44
45"""
46
47import os
48import sys
49import time
50import re
51import subprocess
52import shutil
53import tempfile
54import logging
3de8b4d8 55log = logging.getLogger('pyi2ncommon.arnied_wrapper')
f49f6323 56
30521dad
PD
57from .cnfline import build_cnfvar
58from . import cnfvar
59from . import sysmisc
7102665a 60
30521dad 61
f49f6323
PD
62
63#: default set_cnf binary
64BIN_SET_CNF = "/usr/intranator/bin/set_cnf"
f1ca3964
SA
65#: default arnied_helper binary
66BIN_ARNIED_HELPER = "/usr/intranator/bin/arnied_helper"
f49f6323
PD
67#: default location for template configuration files
68SRC_CONFIG_DIR = "."
69#: default location for dumped configuration files
70DUMP_CONFIG_DIR = "."
71
72
73class ConfigError(Exception):
74 pass
75
76
dfa7c024 77def run_cmd(cmd="", ignore_errors=False, vm=None, timeout=60):
f49f6323
PD
78 """
79 Universal command run wrapper.
80
81 :param str cmd: command to run
82 :param bool ignore_errors: whether not to raise error on command failure
83 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 84 :type vm: :py:class:`virttest.qemu_vm.VM` or None
dfa7c024 85 :param int timeout: amount of seconds to wait for the program to run
2dd87078
CH
86 :returns: command result output where output (stdout/stderr) is bytes
87 (encoding dependent on environment and command given)
88 :rtype: :py:class:`subprocess.CompletedProcess`
f49f6323
PD
89 :raises: :py:class:`OSError` if command failed and cannot be ignored
90 """
91 if vm is not None:
dfa7c024 92 status, stdout = vm.session.cmd_status_output(cmd, timeout=timeout)
f49f6323
PD
93 stdout = stdout.encode()
94 stderr = b""
95 if status != 0:
96 stderr = stdout
97 stdout = b""
98 if not ignore_errors:
99 raise subprocess.CalledProcessError(status, cmd, stderr=stderr)
2dd87078
CH
100 return subprocess.CompletedProcess(cmd, status,
101 stdout=stdout, stderr=stderr)
f49f6323 102 else:
2dd87078
CH
103 return subprocess.run(cmd, check=not ignore_errors, shell=True,
104 capture_output=True)
f49f6323
PD
105
106
107def verify_running(process='arnied', timeout=60, vm=None):
108 """
109 Verify if a given process is running via 'pgrep'.
f49f6323
PD
110
111 :param str process: process to verify if running
112 :param int timeout: run verification timeout
113 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 114 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
115 :raises: :py:class:`RuntimeError` if process is not running
116 """
117 platform_str = ""
118 if vm is not None:
119 vm.verify_alive()
120 platform_str = " on %s" % vm.name
121 for i in range(timeout):
122 log.info("Checking whether %s is running%s (%i\%i)",
123 process, platform_str, i, timeout)
124 result = run_cmd(cmd="pgrep -l -x %s" % process,
125 ignore_errors=True, vm=vm)
126 if result.returncode == 0:
127 log.debug(result)
128 return
129 time.sleep(1)
130 raise RuntimeError("Process %s does not seem to be running" % process)
131
132
133# Basic functionality
134
135
136def accept_licence(vm=None):
137 """
138 Accept the Intra2net license.
139
140 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 141 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
142
143 This is mostly useful for simplified webpage access.
144 """
145 cmd = 'echo "LICENSE_ACCEPTED,0: \\"1\\"" | set_cnf'
146 result = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
147 log.debug(result)
1ac0318c 148 wait_for_generate(vm=vm)
f49f6323
PD
149
150
151def go_online(provider_id, wait_online=True, timeout=60, vm=None):
152 """
153 Go online with the given provider id.
154
155 :param provider_id: provider to go online with
156 :type provider_id: int
157 :param wait_online: whether to wait until online
158 :type wait_online: bool
159 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 160 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
161
162 .. seealso:: :py:func:`go_offline`, :py:func:`wait_for_online`
163 """
164 log.info("Switching to online mode with provider %d", provider_id)
165
166 get_cnf_res = run_cmd(cmd='get_cnf PROVIDER %d' % provider_id, vm=vm)
167 if b'PROVIDER,' not in get_cnf_res.stdout:
33b0346a
CH
168 log.warning('There is no PROVIDER %d on the vm. Skipping go_online.',
169 provider_id)
f49f6323
PD
170 return
171
172 cmd = 'tell-connd --online P%i' % provider_id
173 result = run_cmd(cmd=cmd, vm=vm)
174 log.debug(result)
175
176 if wait_online:
177 wait_for_online(provider_id, timeout=timeout, vm=vm)
178
179
180def go_offline(wait_offline=True, vm=None):
181 """
182 Go offline.
183
184 :param wait_offline: whether to wait until offline
185 :type wait_offline: bool
186 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 187 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
188
189 .. seealso:: :py:func:`go_online`, :py:func:`wait_for_offline`
190 """
191 cmd = 'tell-connd --offline'
192 result = run_cmd(cmd=cmd, vm=vm)
193 log.debug(result)
194
195 if wait_offline:
196 if wait_offline is True:
197 wait_for_offline(vm=vm)
198 else:
199 wait_for_offline(wait_offline, vm=vm)
200
201
202def wait_for_offline(timeout=60, vm=None):
203 """
204 Wait for arnied to signal we are offline.
205
206 :param int timeout: maximum timeout for waiting
207 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 208 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
209 """
210 _wait_for_online_status('offline', None, timeout, vm)
211
212
213def wait_for_online(provider_id, timeout=60, vm=None):
214 """
215 Wait for arnied to signal we are online.
216
217 :param provider_id: provider to go online with
218 :type provider_id: int
219 :param int timeout: maximum timeout for waiting
220 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 221 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
222 """
223 _wait_for_online_status('online', provider_id, timeout, vm)
224
225
226def _wait_for_online_status(status, provider_id, timeout, vm):
227 # Don't use tell-connd --status here since the actual
228 # ONLINE signal to arnied is transmitted
229 # asynchronously via arnieclient_muxer.
230
231 if status == 'online':
232 expected_output = 'DEFAULT: 2'
233 set_status_func = lambda: go_online(provider_id, False, vm)
234 elif status == 'offline':
235 expected_output = 'DEFAULT: 0'
236 set_status_func = lambda: go_offline(False, vm)
237 else:
238 raise ValueError('expect status "online" or "offline", not "{0}"!'
239 .format(status))
240
241 log.info("Waiting for arnied to be {0} within {1} seconds"
242 .format(status, timeout))
243
244 for i in range(timeout):
245 # arnied might invalidate the connd "connection barrier"
246 # after generate was running and switch to OFFLINE (race condition).
247 # -> tell arnied every ten seconds to go online again
248 if i % 10 == 0 and i != 0:
249 set_status_func()
250
251 cmd = '/usr/intranator/bin/get_var ONLINE'
252 result = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
253 log.debug(result)
254
255 if expected_output in result.stdout.decode():
256 log.info("arnied is {0}. Continuing.".format(status))
257 return
258
259 time.sleep(1)
260
261 raise RuntimeError("We didn't manage to go {0} within {1} seconds\n"
262 .format(status, timeout))
263
264
265def disable_virscan(vm=None):
266 """
267 Disable virscan that could block GENERATE and thus all configurations.
268
269 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 270 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
271 """
272 log.info("Disabling virus database update")
273 unset_cnf("VIRSCAN_UPDATE_CRON", vm=vm)
274
275 cmd = "echo 'VIRSCAN_UPDATE_DNS_PUSH,0:\"0\"' |set_cnf"
276 result = run_cmd(cmd=cmd, vm=vm)
277 log.debug(result)
278
279 # TODO: this intervention should be solved in later arnied_helper tool
280 cmd = "rm -f /var/intranator/schedule/UPDATE_VIRSCAN_NODIAL*"
281 result = run_cmd(cmd=cmd, vm=vm)
282 log.debug(result)
283 log.info("Virus database update disabled")
284
285
286def email_transfer(vm=None):
287 """
288 Transfer all the emails using the guest tool arnied_helper.
289
290 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 291 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323 292 """
f1ca3964 293 cmd = f"{BIN_ARNIED_HELPER} --transfer-mail"
f49f6323
PD
294 result = run_cmd(cmd=cmd, vm=vm)
295 log.debug(result)
296
297
298def wait_for_email_transfer(timeout=300, vm=None):
299 """
300 Wait until the mail queue is empty and all emails are sent.
301
302 :param int timeout: email transfer timeout
303 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 304 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
305 """
306 for i in range(timeout):
307 if i % 10 == 0:
308 # Retrigger mail queue in case something is deferred
309 # by an amavisd-new reconfiguration
310 run_cmd(cmd='postqueue -f', vm=vm)
12d603bf 311 log.info('Waiting for SMTP queue to get empty (%i/%i s)',
f49f6323 312 i, timeout)
68d12e8a 313 if not run_cmd(cmd='postqueue -j', vm=vm).stdout:
12d603bf
CH
314 log.debug('SMTP queue is empty')
315 return
f49f6323 316 time.sleep(1)
12d603bf
CH
317 log.warning('Timeout reached but SMTP queue still not empty after {} s'
318 .format(timeout))
f49f6323
PD
319
320
321def schedule(program, exec_time=0, optional_args="", vm=None):
322 """
323 Schedule a program to be executed at a given unix time stamp.
324
325 :param str program: program whose execution is scheduled
326 :param int exec_time: scheduled time of program's execution
327 :param str optional_args: optional command line arguments
328 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 329 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
330 """
331 log.info("Scheduling %s to be executed at %i", program, exec_time)
f49f6323 332 schedule_dir = "/var/intranator/schedule"
ef5c0c20 333 # clean previous schedules of the same program
f49f6323
PD
334 files = vm.session.cmd("ls " + schedule_dir).split() if vm else os.listdir(schedule_dir)
335 for file_name in files:
336 if file_name.startswith(program.upper()):
337 log.debug("Removing previous scheduled %s", file_name)
338 if vm:
ef5c0c20 339 vm.session.cmd("rm -f " + os.path.join(schedule_dir, file_name))
f49f6323
PD
340 else:
341 os.unlink(os.path.join(schedule_dir, file_name))
ef5c0c20
SA
342
343 contents = "%i\n%s\n" % (exec_time, optional_args)
344
345 tmp_file = tempfile.NamedTemporaryFile(mode="w+",
346 prefix=program.upper() + "_",
347 dir=DUMP_CONFIG_DIR,
348 delete=False)
349 log.debug("Created temporary file %s", tmp_file.name)
350 tmp_file.write(contents)
351 tmp_file.close()
352 moved_tmp_file = os.path.join(schedule_dir, os.path.basename(tmp_file.name))
353
f49f6323 354 if vm:
ef5c0c20
SA
355 vm.copy_files_to(tmp_file.name, moved_tmp_file)
356 os.remove(tmp_file.name)
f49f6323
PD
357 else:
358 shutil.move(tmp_file.name, moved_tmp_file)
ef5c0c20 359
f49f6323
PD
360 log.debug("Moved temporary file to %s", moved_tmp_file)
361
362
363def wait_for_run(program, timeout=300, retries=10, vm=None):
364 """
365 Wait for a program using the guest arnied_helper tool.
366
367 :param str program: scheduled or running program to wait for
368 :param int timeout: program run timeout
369 :param int retries: number of tries to verify that the program is scheduled or running
370 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 371 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
372 """
373 log.info("Waiting for program %s to finish with timeout %i",
374 program, timeout)
375 for i in range(retries):
f1ca3964 376 cmd = f"{BIN_ARNIED_HELPER} --is-scheduled-or-running " \
f49f6323
PD
377 + program.upper()
378 check_scheduled = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
379 if check_scheduled.returncode == 0:
380 break
381 time.sleep(1)
80d82f13
PD
382 else:
383 log.warning("The program %s was not scheduled and is not running", program)
384 return
f1ca3964
SA
385 cmd = f"{BIN_ARNIED_HELPER} --wait-for-program-end " \
386 f"{program.upper()} --wait-for-program-timeout {timeout}"
387 # add one second to make sure arnied_helper is finished when we expire
388 result = run_cmd(cmd=cmd, vm=vm, timeout=timeout+1)
389 log.debug(result.stdout)
390
391
392def wait_for_arnied(timeout=60, vm=None):
393 """
394 Wait for arnied socket to be ready.
395
396 :param int timeout: maximum number of seconds to wait
397 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 398 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f1ca3964
SA
399 """
400 cmd = f"{BIN_ARNIED_HELPER} --wait-for-arnied-socket " \
401 f"--wait-for-arnied-socket-timeout {timeout}"
3472b405
SA
402 # add one second to make sure arnied_helper is finished when we expire
403 result = run_cmd(cmd=cmd, vm=vm, timeout=timeout+1)
f49f6323
PD
404 log.debug(result.stdout)
405
406
407# Configuration functionality
408
409def get_cnf(cnf_key, cnf_index=1, regex=".*", compact=False, timeout=30, vm=None):
410 """
411 Query arnied for a `cnf_key` and extract some information via regex.
412
413 :param str cnf_key: queried cnf key
414 :param int cnf_index: index of the cnf key
415 :param str regex: regex to apply on the queried cnf key data
416 :param bool compact: whether to retrieve compact version of the matched cnf keys
417 :param int timeout: arnied run verification timeout
418 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 419 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
420 :returns: extracted information via the regex
421 :rtype: Match object
422
423 If `cnf_index` is set to -1, retrieve and perform regex matching on all instances.
424 """
f1ca3964 425 wait_for_arnied(timeout=timeout, vm=vm)
f49f6323
PD
426 platform_str = ""
427 if vm is not None:
428 platform_str = " from %s" % vm.name
429 log.info("Extracting arnied value %s for %s%s using pattern %s",
430 cnf_index, cnf_key, platform_str, regex)
431 cmd = "get_cnf%s %s%s" % (" -c " if compact else "", cnf_key,
432 " %s" % cnf_index if cnf_index != -1 else "")
6c691734
CH
433 # get_cnf creates latin1-encoded output, transfer from VM removes non-ascii
434 output = run_cmd(cmd=cmd, vm=vm).stdout.decode('latin1')
f49f6323
PD
435 return re.search(regex, output, flags=re.DOTALL)
436
437
438def get_cnf_id(cnf_key, value, timeout=30, vm=None):
439 """
440 Get the id of a configuration of type `cnf_key` and name `value`.
441
442 :param str cnf_key: queried cnf key
443 :param str value: cnf value of the cnf key
444 :param int timeout: arnied run verification timeout
445 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 446 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
447 :returns: the cnf id or -1 if no such cnf variable
448 :rtype: int
449 """
f1ca3964 450 wait_for_arnied(timeout=timeout, vm=vm)
f49f6323
PD
451 regex = "%s,(\d+): \"%s\"" % (cnf_key, value)
452 cnf_id = get_cnf(cnf_key, cnf_index=-1, regex=regex, compact=True, vm=vm)
453 if cnf_id is None:
454 cnf_id = -1
455 else:
456 cnf_id = int(cnf_id.group(1))
457 log.info("Retrieved id \"%s\" for %s is %i", value, cnf_key, cnf_id)
458 return cnf_id
459
460
461def get_cnfvar(varname=None, instance=None, data=None, timeout=30, vm=None):
462 """
463 Invoke get_cnf and return a nested CNF structure.
464
465 :param str varname: "varname" field of the CNF_VAR to look up
466 :param instance: "instance" of that variable to return
467 :type instance: int
468 :param str data: "data" field by which the resulting CNF_VAR list should be filtered
469 :param int timeout: arnied run verification timeout
470 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 471 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
472 :returns: the resulting "cnfvar" structure or None if the lookup fails or the result could not be parsed
473 :rtype: cnfvar option
474 """
f1ca3964 475 wait_for_arnied(timeout=timeout, vm=vm)
f49f6323
PD
476 # firstly, build argv for get_cnf
477 cmd = ["get_cnf", "-j"]
478 if varname is not None:
479 cmd.append("%s" % varname)
480 if instance:
481 cmd.append("%d" % instance)
482 cmd_line = " ".join(cmd)
483
484 # now invoke get_cnf
485 result = run_cmd(cmd=cmd_line, vm=vm)
486 (status, raw) = result.returncode, result.stdout
487 if status != 0:
488 log.info("error %d executing \"%s\"", status, cmd_line)
489 log.debug(raw)
490 return None
491
492 # reading was successful, attempt to parse what we got
493 try:
35072357
JR
494 # The output from "get_cnf -j" is already utf-8. This contrast with
495 # the output of "get_cnf" (no json) which is latin1.
496 if isinstance(raw, bytes):
497 raw = raw.decode("utf-8")
f49f6323
PD
498 cnf = cnfvar.read_cnf_json(raw)
499 except TypeError as exn:
500 log.info("error \"%s\" parsing result of \"%s\"", exn, cmd_line)
501 return None
502 except cnfvar.InvalidCNF as exn:
503 log.info("error \"%s\" validating result of \"%s\"", exn, cmd_line)
504 return None
505
506 if data is not None:
507 return cnfvar.get_vars(cnf, data=data)
508
509 return cnf
510
511
512def get_cnfvar_id(varname, data, timeout=30, vm=None):
513 """
514 Similar to :py:func:`get_cnf_id` but uses :py:func:`get_cnfvar`.
515
516 :param str varname: "varname" field of the CNF_VAR to look up
517 :param str data: "data" field by which the resulting CNF_VAR list should be filtered
518 :param int timeout: arnied run verification timeout
519 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 520 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
521 :returns: the cnf id or -1 if no such cnf variable
522 :rtype: int
523 """
f1ca3964 524 wait_for_arnied(timeout=timeout, vm=vm)
f49f6323
PD
525 log.info("Extracting from arnied CNF_VAR %s with data %s",
526 varname, data)
527 cnf = get_cnfvar(varname=varname, data=data, vm=vm)
528 variables = cnf["cnf"]
529 if len(variables) == 0:
530 log.info("CNF_VAR extraction unsuccessful, defaulting to -1")
531 # preserve behavior
532 return -1
533 first_instance = int(variables[0]["instance"])
534 log.info("CNF_VAR instance lookup yielded %d results, returning first value (%d)",
535 len(variables), first_instance)
536 return first_instance
537
538
80d82f13 539def wait_for_generate(timeout=300, vm=None):
f49f6323
PD
540 """
541 Wait for the 'generate' program to complete.
542
80d82f13 543 Arguments are similar to the ones from :py:method:`wait_for_run`.
f49f6323 544 """
80d82f13
PD
545 wait_for_run('generate', timeout=timeout, retries=1, vm=vm)
546 wait_for_run('generate_offline', timeout=timeout, retries=1, vm=vm)
f49f6323
PD
547
548
d9446155 549def unset_cnf(varname="", instance="", timeout=30, vm=None):
f49f6323
PD
550 """
551 Remove configuration from arnied.
552
553 :param str varname: "varname" field of the CNF_VAR to unset
554 :param int instance: "instance" of that variable to unset
d9446155 555 :param int timeout: arnied run verification timeout
f49f6323 556 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 557 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323 558 """
f1ca3964 559 wait_for_arnied(timeout=timeout, vm=vm)
d9446155 560
f49f6323
PD
561 cmd = "get_cnf %s %s | set_cnf -x" % (varname, instance)
562 run_cmd(cmd=cmd, vm=vm)
563
80d82f13 564 wait_for_generate(vm=vm)
f49f6323
PD
565
566
567def set_cnf(config_files, kind="cnf", timeout=30, vm=None):
568 """
569 Perform static arnied configuration through a set of config files.
570
571 :param config_files: config files to use for the configuration
572 :type config_files: [str]
573 :param str kind: "json" or "cnf"
574 :param int timeout: arnied run verification timeout
575 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 576 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
577 :raises: :py:class:`ConfigError` if cannot apply file
578
579 The config files must be provided and are always expected to be found on
580 the host. If these are absolute paths, they will be kept as is or
581 otherwise will be searched for in `SRC_CONFIG_DIR`. If a vm is provided,
582 the config files will be copied there as temporary files before applying.
583 """
584 log.info("Setting arnied configuration")
f1ca3964 585 wait_for_arnied(timeout=timeout, vm=vm)
f49f6323
PD
586
587 config_paths = prep_config_paths(config_files)
588 for config_path in config_paths:
589 with open(config_path, "rt", errors='replace') as config:
590 log.debug("Contents of applied %s:\n%s", config_path, config.read())
591 if vm is not None:
592 new_config_path = generate_config_path()
593 vm.copy_files_to(config_path, new_config_path)
594 config_path = new_config_path
595 argv = ["set_cnf", kind == "json" and "-j" or "", config_path]
596
597 result = run_cmd(" ".join(argv), ignore_errors=True, vm=vm)
598 logging.debug(result)
599 if result.returncode != 0:
600 raise ConfigError("Failed to apply config %s%s, set_cnf returned %d"
601 % (config_path,
602 " on %s" % vm.name if vm is not None else "",
603 result.returncode))
604
605 try:
80d82f13 606 wait_for_generate(vm=vm)
f49f6323
PD
607 except Exception as ex:
608 # handle cases of remote configuration that leads to connection meltdown
609 if vm is not None and isinstance(ex, sys.modules["aexpect"].ShellProcessTerminatedError):
610 log.info("Resetting connection to %s", vm.name)
611 vm.session = vm.wait_for_login(timeout=10)
612 log.debug("Connection reset via remote error: %s", ex)
613 else:
614 raise ex
615
616
617def set_cnf_semidynamic(config_files, params_dict, regex_dict=None,
618 kind="cnf", timeout=30, vm=None):
619 """
620 Perform semi-dynamic arnied configuration from an updated version of the
621 config files.
622
623 :param config_files: config files to use for the configuration
624 :type config_files: [str]
625 :param params_dict: parameters to override the defaults in the config files
626 :type params_dict: {str, str}
627 :param regex_dict: regular expressions to use for matching the overriden parameters
628 :type regex_dict: {str, str} or None
629 :param str kind: "json" or "cnf"
630 :param int timeout: arnied run verification timeout
631 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 632 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
633
634 The config files must be provided and are always expected to be found on
635 the host. If these are absolute paths, they will be kept as is or
636 otherwise will be searched for in `SRC_CONFIG_DIR`. If a vm is provided,
637 the config files will be copied there as temporary files before applying.
638 """
639 log.info("Performing semi-dynamic arnied configuration")
640
641 config_paths = prep_cnf(config_files, params_dict, regex_dict)
642 set_cnf(config_paths, kind=kind, timeout=timeout, vm=vm)
643
644 log.info("Semi-dynamic arnied configuration successful!")
645
646
647def set_cnf_dynamic(cnf, config_file=None, kind="cnf", timeout=30, vm=None):
648 """
649 Perform dynamic arnied configuration from fully generated config files.
650
651 :param cnf: one key with the same value as *kind* and a list of cnfvars as value
652 :type cnf: {str, str}
653 :param config_file: optional user supplied filename
654 :type config_file: str or None
655 :param str kind: "json", "cnf", or "raw"
656 :param int timeout: arnied run verification timeout
657 :param vm: vm to run on if running on a guest instead of the host
f4c8f40e 658 :type vm: :py:class:`virttest.qemu_vm.VM` or None
f49f6323
PD
659 :raises: :py:class:`ValueError` if `kind` is not an acceptable value
660 :raises: :py:class:`ConfigError` if cannot apply file
661
662 The config file might not be provided in which case a temporary file will
663 be generated and saved on the host's `DUMP_CONFIG_DIR` of not provided as
664 an absolute path. If a vm is provided, the config file will be copied there
665 as a temporary file before applying.
666 """
667 if config_file is None:
668 config_path = generate_config_path(dumped=True)
669 elif os.path.isabs(config_file):
670 config_path = config_file
671 else:
672 config_path = os.path.join(os.path.abspath(DUMP_CONFIG_DIR), config_file)
673 generated = config_file is None
674 config_file = os.path.basename(config_path)
675 log.info("Using %s cnf file %s%s",
676 "generated" if generated else "user-supplied",
677 config_file, " on %s" % vm.name if vm is not None else "")
678
02fe6179 679 # Important to write bytes here to ensure text is encoded with latin-1
1626d17e 680 fd = open(config_path, "wb")
f49f6323
PD
681 try:
682 SET_CNF_METHODS = {
683 "raw": cnfvar.write_cnf_raw,
684 "json": cnfvar.write_cnf_json,
685 "cnf": cnfvar.write_cnf
686 }
687 SET_CNF_METHODS[kind](cnf, out=fd)
688 except KeyError:
689 raise ValueError("Invalid set_cnf method \"%s\"; expected \"json\" or \"cnf\""
690 % kind)
691 finally:
692 fd.close()
693 log.info("Generated config file %s", config_path)
694
695 kind = "cnf" if kind != "json" else kind
696 set_cnf([config_path], kind=kind, timeout=timeout, vm=vm)
697
698
699def set_cnf_pipe(cnf, timeout=30, block=False):
700 """
701 Set local configuration by talking to arnied via ``set_cnf``.
702
703 :param cnf: one key with the same value as *kind* and a list of cnfvars as value
704 :type cnf: {str, str}
705 :param int timeout: arnied run verification timeout
706 :param bool block: whether to wait for generate to complete the
707 configuration change
708 :returns: whether ``set_cnf`` succeeded or not
709 :rtype: bool
710
711 This is obviously not generic but supposed to be run on the guest.
712 """
713 log.info("Setting arnied configuration through local pipe")
f1ca3964 714 wait_for_arnied(timeout=timeout)
f49f6323
PD
715
716 st, out, exit = sysmisc.run_cmd_with_pipe([BIN_SET_CNF, "-j"], inp=str(cnf))
717
718 if st is False:
719 log.error("Error applying configuration; status=%r" % exit)
720 log.error("and stderr:\n%s" % out)
721 return False
722 log.debug("Configuration successfully passed to set_cnf, "
723 "read %d B from pipe" % len(out))
724
725 if block is True:
726 log.debug("Waiting for config job to complete")
727 wait_for_generate()
728
729 log.debug("Exiting sucessfully")
730 return True
731
732
733def prep_config_paths(config_files, config_dir=None):
734 """
735 Prepare absolute paths for all configs at an expected location.
736
737 :param config_files: config files to use for the configuration
738 :type config_files: [str]
739 :param config_dir: config directory to prepend to the filepaths
740 :type config_dir: str or None
741 :returns: list of the full config paths
742 :rtype: [str]
743 """
744 if config_dir is None:
745 config_dir = SRC_CONFIG_DIR
746 config_paths = []
747 for config_file in config_files:
748 if os.path.isabs(config_file):
749 # Absolute path: The user requested a specific file
750 # f.e. needed for dynamic arnied config update
751 config_path = config_file
752 else:
753 config_path = os.path.join(os.path.abspath(config_dir),
754 config_file)
755 logging.debug("Using %s for original path %s", config_path, config_file)
756 config_paths.append(config_path)
757 return config_paths
758
759
760def prep_cnf_value(config_file, value,
761 regex=None, template_key=None, ignore_fail=False):
762 """
763 Replace value in a provided arnied config file.
764
765 :param str config_file: file to use for the replacement
766 :param str value: value to replace the first matched group with
767 :param regex: regular expression to use when replacing a cnf value
768 :type regex: str or None
769 :param template_key: key of a quick template to use for the regex
770 :type template_key: str or None
771 :param bool ignore_fail: whether to ignore regex mismatching
772 :raises: :py:class:`ValueError` if (also default) `regex` doesn't have a match
773
774 In order to ensure better matching capabilities you are supposed to
775 provide a regex pattern with at least one subgroup to match your value.
776 What this means is that the value you like to replace is not directly
777 searched into the config text but matched within a larger regex in
778 in order to avoid any mismatch.
779
780 Example:
781 provider.cnf, 'PROVIDER_LOCALIP,0: "(\d+)"', 127.0.0.1
782 """
783 if template_key is None:
784 pattern = regex.encode()
785 else:
786 samples = {"provider": 'PROVIDER_LOCALIP,\d+: "(\d+\.\d+\.\d+\.\d+)"',
787 "global_destination_addr": 'SPAMFILTER_GLOBAL_DESTINATION_ADDR,0: "bounce_target@(.*)"'}
788 pattern = samples[template_key].encode()
789
790 with open(config_file, "rb") as file_handle:
791 text = file_handle.read()
792 match_line = re.search(pattern, text)
793
794 if match_line is None and not ignore_fail:
795 raise ValueError("Pattern %s not found in %s" % (pattern, config_file))
796 elif match_line is not None:
797 old_line = match_line.group(0)
798 text = text[:match_line.start(1)] + value.encode() + text[match_line.end(1):]
799 line = re.search(pattern, text).group(0)
800 log.debug("Updating %s to %s in %s", old_line, line, config_file)
801 with open(config_file, "wb") as file_handle:
802 file_handle.write(text)
803
804
805def prep_cnf(config_files, params_dict, regex_dict=None):
806 """
bcd9beb1 807 Update all config files with the default overriding parameters,
f49f6323
PD
808 i.e. override the values hard-coded in those config files.
809
810 :param config_files: config files to use for the configuration
811 :type config_files: [str]
812 :param params_dict: parameters to override the defaults in the config files
813 :type params_dict: {str, str}
814 :param regex_dict: regular expressions to use for matching the overriden parameters
815 :type regex_dict: {str, str} or None
816 :returns: list of prepared (modified) config paths
817 :rtype: [str]
818 """
819 log.info("Preparing %s template config files", len(config_files))
820
821 src_config_paths = prep_config_paths(config_files)
822 new_config_paths = []
823 for config_path in src_config_paths:
824 new_config_path = generate_config_path(dumped=True)
825 shutil.copy(config_path, new_config_path)
826 new_config_paths.append(new_config_path)
827
828 for config_path in new_config_paths:
829 for param_key in params_dict.keys():
830 if regex_dict is None:
831 regex_val = "\s+%s,\d+: \"(.*)\"" % param_key.upper()
832 elif param_key in regex_dict.keys():
833 regex_val = regex_dict[param_key] % param_key.upper()
834 elif re.match("\w*_\d+$", param_key):
835 final_parameter, parent_id = \
836 re.match("(\w*)_(\d+)$", param_key).group(1, 2)
837 regex_val = "\(%s\) %s,\d+: \"(.*)\"" \
838 % (parent_id, final_parameter.upper())
839 log.debug("Requested regex for %s is '%s'",
840 param_key, regex_val)
841 else:
842 regex_val = "\s+%s,\d+: \"(.*)\"" % param_key.upper()
843 prep_cnf_value(config_path, params_dict[param_key],
844 regex=regex_val, ignore_fail=True)
845 log.info("Prepared template config file %s", config_path)
846
847 return new_config_paths
848
849
850def generate_config_path(dumped=False):
851 """
852 Generate path for a temporary config name.
853
854 :param bool dumped: whether the file should be in the dump
855 directory or in temporary directory
856 :returns: generated config file path
857 :rtype: str
858 """
859 dir = os.path.abspath(DUMP_CONFIG_DIR) if dumped else None
860 fd, filename = tempfile.mkstemp(suffix=".cnf", dir=dir)
861 os.close(fd)
862 os.unlink(filename)
863 return filename
864
865
f49f6323
PD
866# enum
867Delete = 0
868Update = 1
869Add = 2
870Child = 3
871
872
873def batch_update_cnf(cnf, vars):
874 """
875 Perform a batch update of multiple cnf variables.
876
877 :param cnf: CNF variable to update
878 :type cnf: BuildCnfVar object
879 :param vars: tuples of enumerated action and subtuple with data
880 :type vars: [(int, (str, int, str))]
881 :returns: updated CNF variable
882 :rtype: BuildCnfVar object
883
884 The actions are indexed in the same order: delete, update, add, child.
885 """
886 last = 0
887 for (action, data) in vars:
888 if action == Update:
889 var, ref, val = data
890 last = cnf.update_cnf(var, ref, val)
ff0ff458 891 elif action == Add:
f49f6323
PD
892 var, ref, val = data
893 last = cnf.add_cnf(var, ref, val)
894 elif action == Delete:
895 last = cnf.del_cnf(data)
896 elif action == Child: # only one depth supported
897 var, ref, val = data
898 # do not update last
899 cnf.add_cnf(var, ref, val, different_parent_line_no=last)
900 return cnf
901
902
903def build_cnf(kind, instance=0, vals=[], data="", filename=None):
904 """
905 Build a CNF variable and save it in a config file.
906
907 :param str kind: name of the CNF variable
908 :param int instance: instance number of the CNF variable
909 :param vals: tuples of enumerated action and subtuple with data
910 :type vals: [(int, (str, int, str))]
911 :param str data: data for the CNF variable
912 :param filename: optional custom name of the config file
913 :type filename: str or None
914 :returns: name of the saved config file
915 :rtype: str
916 """
917 builder = build_cnfvar.BuildCnfVar(kind, instance=instance, data=data)
918 batch_update_cnf(builder, vals)
919 filename = generate_config_path(dumped=True) if filename is None else filename
920 [filename] = prep_config_paths([filename], DUMP_CONFIG_DIR)
921 builder.save(filename)
922 return filename
923