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