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