1 # This Python file uses the following encoding: utf-8
3 # The software in this package is distributed under the GNU General
4 # Public License version 2 (with a special exception described below).
6 # A copy of GNU General Public License (GPL) is included in this distribution,
7 # in the file COPYING.GPL.
9 # As a special exception, if other files instantiate templates or use macros
10 # or inline functions from this file, or you compile this file and link it
11 # with other works to produce a work based on this file, this file
12 # does not by itself cause the resulting work to be covered
13 # by the GNU General Public License.
15 # However the source code for this file must still be made available
16 # in accordance with section (3) of the GNU General Public License.
18 # This exception does not invalidate any other reasons why a work based
19 # on this file might be covered by the GNU General Public License.
21 # Copyright (c) 2016-2018 Intra2net AG <info@intra2net.com>
26 ------------------------------------------------------
27 Miscellaneous system utility: Collection of various common system stuff / idioms.
29 Copyright: 2015 Intra2net AG
33 ------------------------------------------------------
34 The library exports the symbols below and some custom logging functions.
37 Wrapper for the default use case of the cumbersome "subprocess" library.
38 Acceps a list of arguments that describe the command invocation. Returns
39 ``True`` and the contents of ``stdout`` if the pipe returned sucessfully,
40 ``False`` plus ``stderr`` and the exit status otherwise. For example::
43 (success, output, _ret) = sysmisc.run_cmd_with_pipe([ "/usr/bin/date", "+%F" ])
45 print("Today is %s" % output)
47 print("Failed to read date from pipe.")
49 get_mountpoints_by_type
50 Extract mount points for the given file system type from */proc/mounts*.
51 Returns ``None`` if the file system is not mounted, a list of mount points
52 otherwise. Raises a test error if */proc/mounts* cannot be accessed.
55 Similar to run_cmd_with_pipe but allows processing of output line-by-line
56 as it becomes available. This may be necessary when the underlying binary
57 creates lots of output that cannot be buffered until the process finishes.
63 if re.match('\d', line):
64 print('found digits in line!')
65 sysmisc.read_linewise('dump_db', parse)
68 Return a hash of a flie.
71 Replace the reboot binary with a fake one.
74 Run a command and wait until a condition evaluates to True.
77 A context manager that temporarily changes the current working directory
79 The logging functions either use the format capability or play
80 the simple role of providing shorter names.
84 ------------------------------------------------------
88 from __future__ import print_function
98 from contextlib import contextmanager
100 llog = logging.getLogger('pyi2ncommon.sysmisc')
103 __all__ = ("inf", "run_cmd_with_pipe", "get_mountpoints_by_type", "read_linewise", "hash_file", "cheat_reboot", "RUN_RESULT_OK", "RUN_RESULT_TIMEDOUT", "RUN_RESULT_FAIL", "RUN_RESULT_NAME", "cmd_block_till")
106 ###############################################################################
108 ###############################################################################
113 A context manager which changes the working directory.
115 Changes current working directory to the given path, and then changes it
116 back to its previous value on exit.
118 Taken from comment for python recipe by Greg Warner at
119 http://code.activestate.com/recipes/576620-changedirectory-context-manager/
122 :arg str path: paht to temporarily switch to
124 orig_wd = os.getcwd()
132 def run_cmd_with_pipe(argv, inp=None):
134 Read from a process pipe.
136 :param argv: arguments to use for creating a process
138 :param inp: Text to be piped into the program’s standard input.
140 :returns: a processes' stdout along with a status info
141 :rtype: bool * str * (int option)
143 Executes a binary and reads its output from a pipe. Returns a triple
144 encoding the programm success, its output either from stdout or stderr,
145 as well the exit status if non-zero.
147 If your process creates a lot of output, consider using
148 :py:func:`read_linewise` instead.
150 llog.debug("About to execute \"" + " ".join(argv) + "\"")
152 if isinstance(inp, str):
153 stdin = subprocess.PIPE
154 p = subprocess.Popen(argv,
156 stdout=subprocess.PIPE,
157 stderr=subprocess.PIPE)
158 if p.stdin is not None:
159 p.stdin.write(inp.encode())
160 (stdout, stderr) = p.communicate()
161 exit_status = p.wait()
163 return False, stderr.decode(), exit_status
164 return True, stdout.decode(), None
167 procmounts = "/proc/mounts"
170 def get_mountpoints_by_type(fstype):
172 Determine where some filesystem is mounted by reading the list
173 of mountpoints from */proc/mounts*.
175 :param str fstype: filesystem type
176 :returns: any mountpoints found
177 :rtype: str list option or None
178 :raises: :py:class:`IOError` if failed to read the process mounts
180 llog.debug("Determine mountpoints of %s." % fstype)
183 with open(procmounts, "r") as m:
185 pat = re.compile("^[^\s]+\s+([^\s]+)\s+" + fstype + "\s+.*$")
187 for mp in map(lambda l: re.match(pat, l), lines)
190 raise IOError("Failed to read %s." % procmounts)
196 def read_linewise(cmd, func, **kwargs):
198 Run `cmd` using subprocess, applying `func` to each line of stdout/stderr.
200 :param str cmd: command to read linewise
201 :param func: function to apply on each stdout line
203 :param kwargs: extra arguments for the subprocess initiation
204 :returns: the process' returncode from :py:meth:`subprocess.Popen.wait`
207 Creates a subprocess.Popen object with given `cmd`, `bufsize`=1, `stdout`=PIPE,
208 `stderr`=STDOUT, `universal_newlines`=True and the given `kwargs`.
210 As opposed to :py:func:`run_cmd_with_pipe`, output is not gathered and
211 returned but processed as it becomes available and then discared. This
212 allows to process output even if there is much of it.
214 proc = subprocess.Popen(cmd, bufsize=1, stdout=subprocess.PIPE,
215 stderr=subprocess.STDOUT, universal_newlines=True,
218 for line in proc.stdout:
220 # (raises a ValueError in communicate() line:
221 # "Mixing iteration and read methods would lose data")
222 # if proc.poll() is not None:
225 #rest_output,_ = proc.communicate()
226 #for line in rest_output:
232 def hash_file(fname, new=hashlib.sha512, bs=4096):
236 :param str fname: name of the file to hash
237 :param new: constructor algorithm
238 :type new: builtin_function_or_method
239 :param int bs: read and write up to 'bs' bytes at a time
240 :returns: hexadecimal digest of the strings read from the file
244 with open(fname, "rb") as fp:
249 return hsh.hexdigest()
256 path="%s" # <- location of original executable, == location of script
257 backup="%s" # <- location of backup
258 chksum="%s" # <- sha512(orig_reboot)
262 echo "[$(date '+%%F %%T')] $*" &>> "${log}"
265 msg "Fake reboot invoked. Restoring executable from ${backup}."
267 if [ "${chksum}" = "$(sha512sum ${path} | cut -d ' ' -f 1)" ]; then
268 msg "Real reboot executable already in place at ${path}. Aborting."
272 if [ ! -x "${backup}" ]; then
273 msg "No backup executable at ${backup}!."
277 if [ "${chksum}" = "$(sha512sum ${backup} | cut -d ' ' -f 1)" ]
279 msg "Installing backup executable from ${backup} as ${path}."
280 if ! mv -f "${backup}" "${path}"; then
281 msg "Failed to replace ${path}."
284 msg "Checksum mismatch of ${backup}: Expected ${chksum}."
288 msg "Fake reboot successful -- next invocation will reboot indeed."
292 backup_infix = "backup"
293 backup_fmt = "%s.%s_%s"
294 logfile = "/var/log/cheat_reboot.log"
301 :raises: :py:class:`exceptions.OSError` if backup target already exists
303 This replaces the ``reboot-intranator`` executable by script which
304 replaces itself by the backed up executable upon the next invocation.
306 #path = utils.system_output("which reboot")
307 path = "/usr/intranator/bin/reboot-intranator"
308 suffix = uuid.uuid1()
309 backup = backup_fmt % (path, backup_infix, suffix)
311 if os.path.exists(backup):
312 raise OSError("Target %s already exists." % backup)
314 hexa = hash_file(path)
315 llog.debug("Found reboot at %s, hash %s; dst %s." % (path, hexa, backup))
316 subprocess.check_call(["mv", "-f", path, backup]) # backup existing binary
317 script = cheat_tpl % (path, backup, hexa, logfile)
318 with open(path, "w") as fp:
319 fp.write(script) # write script content to original location
320 mode = os.stat(path).st_mode
321 os.chmod(path, mode | stat.S_IXUSR | stat.S_IXGRP) # ug+x
324 RUN_RESULT_OK = 0 # → success, cond returned True within timeout
325 RUN_RESULT_TIMEDOUT = 1 # → success, but timeout elapsed
326 RUN_RESULT_FAIL = 2 # → fail
329 {RUN_RESULT_OK: "RUN_RESULT_OK", RUN_RESULT_TIMEDOUT: "RUN_RESULT_TIMEDOUT", RUN_RESULT_FAIL: "RUN_RESULT_FAIL"}
333 def cmd_block_till(cmd, timeout, cond, interval=1, *userdata, **kwuserdata):
335 Run ``cmd`` and wait until :py:func`cond` evaluates to True.
337 :param cmd: Command line or callback to execute. Function arguments must
338 have the same signature as :py:func:`run_cmd_with_pipe`.
339 :type cmd: [str] | types.FunctionType
340 :param int timeout: Blocking timeout
342 :returns: Pair of result and error message if appropriate or
344 :rtype: (run_result, str | None)
346 llog.debug("cmd_block_till: %r, %d s, %r", cmd, timeout, cond)
347 if isinstance(cmd, types.FunctionType):
349 elif isinstance(cmd, list):
350 succ, out, _ = run_cmd_with_pipe(cmd) # caution: never pass further arguments!
352 raise TypeError("cmd_block_till: invalid type (cmd=%r); expected "
353 "function or argv", cmd)
356 return RUN_RESULT_FAIL, "cmd_block_till: invalid timeout; nonnegative " \
359 return RUN_RESULT_FAIL, "cmd_block_till: command %r failed (%s)" \
361 t_0 = time.time() # brr; cf. PEP 418 as to why
362 while cond(*userdata, **kwuserdata) is False:
366 return RUN_RESULT_TIMEDOUT, "cmd_block_till: command %r exceeded " \
367 "%d s timeout" % (cmd, timeout)
368 llog.debug("cmd_block_till: condition not satisfied after %d s, "
369 "retrying for another %d s" % (dt, timeout - dt))
371 return RUN_RESULT_OK, None
374 ###############################################################################
376 ###############################################################################
378 CURRENT_TEST_STAGE = None
379 CURRENT_TEST_NAME = None
380 LOG_TAG = "%s/%s" % (os.path.basename(__file__), os.uname()[1])
384 def enter_test_stage(s):
385 """Group events into stages for status updates."""
386 global CURRENT_TEST_STAGE
387 CURRENT_TEST_STAGE = s
388 llog.info("Transitioning to test stage %s", s)
391 def progress(fmt, *args):
392 """Status updates that stand out among the log noise."""
393 if isinstance(CURRENT_TEST_STAGE, str):
394 label = "/%s" % CURRENT_TEST_STAGE
397 name = CURRENT_TEST_NAME if isinstance(CURRENT_TEST_NAME, str) else ""
398 fmt, label = str(fmt), str(label) # yes
399 llog.info("[%s%s] %s" % (name, label, fmt), *args)
400 # TODO: this method is more dynamic
401 # llog.info("[%s%s] %s%s" % (LOG_TAG, "", LOG_INDENT*indent, fmt), *args)
404 # these methods serve as shorter names
406 """Short name for INFO logging."""
407 llog.info(fmt, *args)
411 """Short name for DEBUG logging."""
412 llog.debug(fmt, *args)
416 """Short name for ERROR logging."""
417 llog.error(fmt, *args)
421 """Short name for WARN logging."""
422 llog.error(fmt, *args)
425 # these methods use the format capability
426 def log(level, text, *args, **kwargs):
427 """Log at any level using format capability."""
428 llog.log(level, text.format(*args), **kwargs)
431 def info(text, *args, **kwargs):
432 """Log at INFO level using format capability."""
433 log(logging.INFO, text, *args, **kwargs)
436 def debug(text, *args, **kwargs):
437 """Log at DEBUG level using format capability."""
438 log(logging.DEBUG, text, *args, **kwargs)
441 def error(text, *args, **kwargs):
442 """Log at ERROR level using format capability."""
443 log(logging.ERROR, text, *args, **kwargs)
446 def warn(text, *args, **kwargs):
447 """Log at WARN level using format capability."""
448 log(logging.WARN, text, *args, **kwargs)