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