102a33092c5abaf3f7af6c1e0667e563548788b8
[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 Interaction with central arnied daemon.
26
27 All functions (except :py:func:`schedule` result in calling a binary
28 (either :py:data:`BIN_ARNIED_HELPER` or *tell-connd*).
29
30 For changes of configuration (*set_cnf*, *get_cnf*), refer to :py:mod:`pyi2ncommon.cnfvar`.
31
32 Copyright: Intra2net AG
33
34
35 INTERFACE
36 ------------------------------------------------------
37
38 """
39
40 import os
41 import sys
42 import time
43 import re
44 import subprocess
45 import shutil
46 import tempfile
47 import logging
48 log = logging.getLogger('pyi2ncommon.arnied_wrapper')
49
50
51 #: default arnied_helper binary
52 BIN_ARNIED_HELPER = "/usr/intranator/bin/arnied_helper"
53
54
55 def run_cmd(cmd="", ignore_errors=False, vm=None, timeout=60):
56     """
57     Universal command run wrapper.
58
59     :param str cmd: command to run
60     :param bool ignore_errors: whether not to raise error on command failure
61     :param vm: vm to run on if running on a guest instead of the host
62     :type vm: :py:class:`virttest.qemu_vm.VM` or None
63     :param int timeout: amount of seconds to wait for the program to run
64     :returns: command result output where output (stdout/stderr) is bytes
65               (encoding dependent on environment and command given)
66     :rtype: :py:class:`subprocess.CompletedProcess`
67     :raises: :py:class:`OSError` if command failed and cannot be ignored
68     """
69     if vm is not None:
70         status, stdout = vm.session.cmd_status_output(cmd, timeout=timeout)
71         stdout = stdout.encode()
72         stderr = b""
73         if status != 0:
74             stderr = stdout
75             stdout = b""
76             if not ignore_errors:
77                 raise subprocess.CalledProcessError(status, cmd, stderr=stderr)
78         return subprocess.CompletedProcess(cmd, status,
79                                            stdout=stdout, stderr=stderr)
80     else:
81         return subprocess.run(cmd, check=not ignore_errors, shell=True,
82                               capture_output=True)
83
84
85 def verify_running(process='arnied', timeout=60, vm=None):
86     """
87     Verify if a given process is running via 'pgrep'.
88
89     :param str process: process to verify if running
90     :param int timeout: run verification timeout
91     :param vm: vm to run on if running on a guest instead of the host
92     :type vm: :py:class:`virttest.qemu_vm.VM` or None
93     :raises: :py:class:`RuntimeError` if process is not running
94     """
95     platform_str = ""
96     if vm is not None:
97         vm.verify_alive()
98         platform_str = " on %s" % vm.name
99     for i in range(timeout):
100         log.info("Checking whether %s is running%s (%i\%i)",
101                  process, platform_str, i, timeout)
102         result = run_cmd(cmd="pgrep -l -x %s" % process,
103                          ignore_errors=True, vm=vm)
104         if result.returncode == 0:
105             log.debug(result)
106             return
107         time.sleep(1)
108     raise RuntimeError("Process %s does not seem to be running" % process)
109
110
111 # Basic functionality
112
113
114 def go_online(provider_id, wait_online=True, timeout=60, vm=None):
115     """
116     Go online with the given provider id.
117
118     :param provider_id: provider to go online with
119     :type provider_id: int
120     :param wait_online: whether to wait until online
121     :type wait_online: bool
122     :param int timeout: Seconds to wait in :py:func:`wait_for_online`
123     :param vm: vm to run on if running on a guest instead of the host
124     :type vm: :py:class:`virttest.qemu_vm.VM` or None
125
126     .. seealso:: :py:func:`go_offline`, :py:func:`wait_for_online`
127     """
128     log.info("Switching to online mode with provider %d", provider_id)
129
130     cmd = 'tell-connd --online P%i' % provider_id
131     result = run_cmd(cmd=cmd, vm=vm)
132     log.debug(result)
133
134     if wait_online:
135         wait_for_online(provider_id, timeout=timeout, vm=vm)
136
137
138 def go_offline(wait_offline=True, vm=None):
139     """
140     Go offline.
141
142     :param wait_offline: whether to wait until offline
143     :type wait_offline: bool
144     :param vm: vm to run on if running on a guest instead of the host
145     :type vm: :py:class:`virttest.qemu_vm.VM` or None
146
147     .. seealso:: :py:func:`go_online`, :py:func:`wait_for_offline`
148     """
149     cmd = 'tell-connd --offline'
150     result = run_cmd(cmd=cmd, vm=vm)
151     log.debug(result)
152
153     if wait_offline:
154         if wait_offline is True:
155             wait_for_offline(vm=vm)
156         else:
157             wait_for_offline(wait_offline, vm=vm)
158
159
160 def wait_for_offline(timeout=60, vm=None):
161     """
162     Wait for arnied to signal we are offline.
163
164     :param int timeout: maximum timeout for waiting
165     :param vm: vm to run on if running on a guest instead of the host
166     :type vm: :py:class:`virttest.qemu_vm.VM` or None
167     """
168     _wait_for_online_status('offline', None, timeout, vm)
169
170
171 def wait_for_online(provider_id, timeout=60, vm=None):
172     """
173     Wait for arnied to signal we are online.
174
175     :param provider_id: provider to go online with
176     :type provider_id: int
177     :param int timeout: maximum timeout for waiting
178     :param vm: vm to run on if running on a guest instead of the host
179     :type vm: :py:class:`virttest.qemu_vm.VM` or None
180     """
181     _wait_for_online_status('online', provider_id, timeout, vm)
182
183
184 def _wait_for_online_status(status, provider_id, timeout, vm):
185     # Don't use tell-connd --status here since the actual
186     # ONLINE signal to arnied is transmitted
187     # asynchronously via arnieclient_muxer.
188
189     if status == 'online':
190         expected_output = 'DEFAULT: 2'
191         set_status_func = lambda: go_online(provider_id, False, vm)
192     elif status == 'offline':
193         expected_output = 'DEFAULT: 0'
194         set_status_func = lambda: go_offline(False, vm)
195     else:
196         raise ValueError('expect status "online" or "offline", not "{0}"!'
197                          .format(status))
198
199     log.info("Waiting for arnied to be {0} within {1} seconds"
200              .format(status, timeout))
201
202     for i in range(timeout):
203         # arnied might invalidate the connd "connection barrier"
204         # after generate was running and switch to OFFLINE (race condition).
205         # -> tell arnied every ten seconds to go online again
206         if i % 10 == 0 and i != 0:
207             set_status_func()
208
209         cmd = '/usr/intranator/bin/get_var ONLINE'
210         result = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
211         log.debug(result)
212
213         if expected_output in result.stdout.decode():
214             log.info("arnied is {0}. Continuing.".format(status))
215             return
216
217         time.sleep(1)
218
219     raise RuntimeError("We didn't manage to go {0} within {1} seconds\n"
220                        .format(status, timeout))
221
222
223 def email_transfer(vm=None):
224     """
225     Transfer all the emails using the guest tool arnied_helper.
226
227     :param vm: vm to run on if running on a guest instead of the host
228     :type vm: :py:class:`virttest.qemu_vm.VM` or None
229     """
230     cmd = f"{BIN_ARNIED_HELPER} --transfer-mail"
231     result = run_cmd(cmd=cmd, vm=vm)
232     log.debug(result)
233
234
235 def wait_for_email_transfer(timeout=300, vm=None):
236     """
237     Wait until the mail queue is empty and all emails are sent.
238
239     :param int timeout: email transfer timeout
240     :param vm: vm to run on if running on a guest instead of the host
241     :type vm: :py:class:`virttest.qemu_vm.VM` or None
242     """
243     for i in range(timeout):
244         if i % 10 == 0:
245             # Retrigger mail queue in case something is deferred
246             # by an amavisd-new reconfiguration
247             run_cmd(cmd='postqueue -f', vm=vm)
248             log.info('Waiting for SMTP queue to get empty (%i/%i s)',
249                      i, timeout)
250         if not run_cmd(cmd='postqueue -j', vm=vm).stdout:
251             log.debug('SMTP queue is empty')
252             return
253         time.sleep(1)
254     log.warning('Timeout reached but SMTP queue still not empty after {} s'
255                 .format(timeout))
256
257
258 def schedule(program, exec_time=0, optional_args="", vm=None):
259     """
260     Schedule a program to be executed at a given unix time stamp.
261
262     :param str program: program whose execution is scheduled
263     :param int exec_time: scheduled time of program's execution
264     :param str optional_args: optional command line arguments
265     :param vm: vm to run on if running on a guest instead of the host
266     :type vm: :py:class:`virttest.qemu_vm.VM` or None
267     """
268     log.info("Scheduling %s to be executed at %i", program, exec_time)
269     schedule_dir = "/var/intranator/schedule"
270     # clean previous schedules of the same program
271     files = vm.session.cmd("ls " + schedule_dir).split() if vm else os.listdir(schedule_dir)
272     for file_name in files:
273         if file_name.startswith(program.upper()):
274             log.debug("Removing previous scheduled %s", file_name)
275             if vm:
276                 vm.session.cmd("rm -f " + os.path.join(schedule_dir, file_name))
277             else:
278                 os.unlink(os.path.join(schedule_dir, file_name))
279
280     contents = "%i\n%s\n" % (exec_time, optional_args)
281
282     tmp_file = tempfile.NamedTemporaryFile(mode="w+",
283                                         prefix=program.upper() + "_",
284                                         delete=False)
285     log.debug("Created temporary file %s", tmp_file.name)
286     tmp_file.write(contents)
287     tmp_file.close()
288     moved_tmp_file = os.path.join(schedule_dir, os.path.basename(tmp_file.name))
289
290     if vm:
291         vm.copy_files_to(tmp_file.name, moved_tmp_file)
292         os.remove(tmp_file.name)
293     else:
294         shutil.move(tmp_file.name, moved_tmp_file)
295
296     log.debug("Moved temporary file to %s", moved_tmp_file)
297
298
299 def wait_for_run(program, timeout=300, retries=10, vm=None):
300     """
301     Wait for a program using the guest arnied_helper tool.
302
303     :param str program: scheduled or running program to wait for
304     :param int timeout: program run timeout
305     :param int retries: number of tries to verify that the program is scheduled or running
306     :param vm: vm to run on if running on a guest instead of the host
307     :type vm: :py:class:`virttest.qemu_vm.VM` or None
308     """
309     log.info("Waiting for program %s to finish with timeout %i",
310              program, timeout)
311     for i in range(retries):
312         cmd = f"{BIN_ARNIED_HELPER} --is-scheduled-or-running " \
313             + program.upper()
314         check_scheduled = run_cmd(cmd=cmd, ignore_errors=True, vm=vm)
315         if check_scheduled.returncode == 0:
316             break
317         time.sleep(1)
318     else:
319         log.warning("The program %s was not scheduled and is not running", program)
320         return
321     cmd = f"{BIN_ARNIED_HELPER} --wait-for-program-end " \
322           f"{program.upper()} --wait-for-program-timeout {timeout}"
323     # add one second to make sure arnied_helper is finished when we expire
324     result = run_cmd(cmd=cmd, vm=vm, timeout=timeout+1)
325     log.debug(result.stdout)
326
327
328 def wait_for_arnied(timeout=60, vm=None):
329     """
330     Wait for arnied socket to be ready.
331
332     :param int timeout: maximum number of seconds to wait
333     :param vm: vm to run on if running on a guest instead of the host
334     :type vm: :py:class:`virttest.qemu_vm.VM` or None
335     """
336     cmd = f"{BIN_ARNIED_HELPER} --wait-for-arnied-socket " \
337           f"--wait-for-arnied-socket-timeout {timeout}"
338     # add one second to make sure arnied_helper is finished when we expire
339     result = run_cmd(cmd=cmd, vm=vm, timeout=timeout+1)
340     log.debug(result.stdout)
341
342
343 # Configuration functionality
344
345
346 def wait_for_generate(timeout=300, vm=None):
347     """
348     Wait for the 'generate' program to complete.
349
350     Arguments are similar to the ones from :py:func:`wait_for_run`.
351     """
352     wait_for_run('generate', timeout=timeout, retries=1, vm=vm)
353     wait_for_run('generate_offline', timeout=timeout, retries=1, vm=vm)