initial code repo
[stor4nfv.git] / src / ceph / src / ceph-volume / ceph_volume / process.py
diff --git a/src/ceph/src/ceph-volume/ceph_volume/process.py b/src/ceph/src/ceph-volume/ceph_volume/process.py
new file mode 100644 (file)
index 0000000..4b6a9c2
--- /dev/null
@@ -0,0 +1,199 @@
+from fcntl import fcntl, F_GETFL, F_SETFL
+from os import O_NONBLOCK, read
+import subprocess
+from select import select
+from ceph_volume import terminal
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+
+def log_output(descriptor, message, terminal_logging):
+    """
+    log output to both the logger and the terminal if terminal_logging is
+    enabled
+    """
+    if not message:
+        return
+    message = message.strip()
+    line = '%s %s' % (descriptor, message)
+    if terminal_logging:
+        getattr(terminal, descriptor)(message)
+    logger.info(line)
+
+
+def log_descriptors(reads, process, terminal_logging):
+    """
+    Helper to send output to the terminal while polling the subprocess
+    """
+    # these fcntl are set to O_NONBLOCK for the filedescriptors coming from
+    # subprocess so that the logging does not block. Without these a prompt in
+    # a subprocess output would hang and nothing would get printed. Note how
+    # these are just set when logging subprocess, not globally.
+    stdout_flags = fcntl(process.stdout, F_GETFL) # get current p.stdout flags
+    stderr_flags = fcntl(process.stderr, F_GETFL) # get current p.stderr flags
+    fcntl(process.stdout, F_SETFL, stdout_flags | O_NONBLOCK)
+    fcntl(process.stderr, F_SETFL, stderr_flags | O_NONBLOCK)
+    descriptor_names = {
+        process.stdout.fileno(): 'stdout',
+        process.stderr.fileno(): 'stderr'
+    }
+    for descriptor in reads:
+        descriptor_name = descriptor_names[descriptor]
+        try:
+            log_output(descriptor_name, read(descriptor, 1024), terminal_logging)
+        except (IOError, OSError):
+            # nothing else to log
+            pass
+
+
+def obfuscate(command_, on=None):
+    """
+    Certain commands that are useful to log might contain information that
+    should be replaced by '*' like when creating OSDs and the keyryings are
+    being passed, which should not be logged.
+
+    :param on: A string (will match a flag) or an integer (will match an index)
+
+    If matching on a flag (when ``on`` is a string) it will obfuscate on the
+    value for that flag. That is a command like ['ls', '-l', '/'] that calls
+    `obfuscate(command, on='-l')` will obfustace '/' which is the value for
+    `-l`.
+
+    The reason for `on` to allow either a string or an integer, altering
+    behavior for both is because it is easier for ``run`` and ``call`` to just
+    pop a value to obfuscate (vs. allowing an index or a flag)
+    """
+    command = command_[:]
+    msg = "Running command: %s" % ' '.join(command)
+    if on in [None, False]:
+        return msg
+
+    if isinstance(on, int):
+        index = on
+
+    else:
+        try:
+            index = command.index(on) + 1
+        except ValueError:
+            # if the flag just doesn't exist then it doesn't matter just return
+            # the base msg
+            return msg
+
+    try:
+        command[index] = '*' * len(command[index])
+    except IndexError: # the index was completely out of range
+        return msg
+
+    return "Running command: %s" % ' '.join(command)
+
+
+def run(command, **kw):
+    """
+    A real-time-logging implementation of a remote subprocess.Popen call where
+    a command is just executed on the remote end and no other handling is done.
+
+    :param command: The command to pass in to the remote subprocess.Popen as a list
+    :param stop_on_error: If a nonzero exit status is return, it raises a ``RuntimeError``
+    """
+    stop_on_error = kw.pop('stop_on_error', True)
+    command_msg = obfuscate(command, kw.pop('obfuscate', None))
+    stdin = kw.pop('stdin', None)
+    logger.info(command_msg)
+    terminal.write(command_msg)
+    terminal_logging = kw.pop('terminal_logging', True)
+
+    process = subprocess.Popen(
+        command,
+        stdin=subprocess.PIPE,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        close_fds=True,
+        **kw
+    )
+
+    if stdin:
+        process.communicate(stdin)
+    while True:
+        reads, _, _ = select(
+            [process.stdout.fileno(), process.stderr.fileno()],
+            [], []
+        )
+        log_descriptors(reads, process, terminal_logging)
+
+        if process.poll() is not None:
+            # ensure we do not have anything pending in stdout or stderr
+            log_descriptors(reads, process, terminal_logging)
+
+            break
+
+    returncode = process.wait()
+    if returncode != 0:
+        msg = "command returned non-zero exit status: %s" % returncode
+        if stop_on_error:
+            raise RuntimeError(msg)
+        else:
+            if terminal_logging:
+                terminal.warning(msg)
+            logger.warning(msg)
+
+
+def call(command, **kw):
+    """
+    Similar to ``subprocess.Popen`` with the following changes:
+
+    * returns stdout, stderr, and exit code (vs. just the exit code)
+    * logs the full contents of stderr and stdout (separately) to the file log
+
+    By default, no terminal output is given, not even the command that is going
+    to run.
+
+    Useful when system calls are needed to act on output, and that same output
+    shouldn't get displayed on the terminal.
+
+    :param terminal_verbose: Log command output to terminal, defaults to False, and
+                             it is forcefully set to True if a return code is non-zero
+    """
+    terminal_verbose = kw.pop('terminal_verbose', False)
+    show_command = kw.pop('show_command', False)
+    command_msg = "Running command: %s" % ' '.join(command)
+    stdin = kw.pop('stdin', None)
+    logger.info(command_msg)
+    if show_command:
+        terminal.write(command_msg)
+
+    process = subprocess.Popen(
+        command,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        stdin=subprocess.PIPE,
+        close_fds=True,
+        **kw
+    )
+    if stdin:
+        stdout_stream, stderr_stream = process.communicate(stdin)
+    else:
+        stdout_stream = process.stdout.read()
+        stderr_stream = process.stderr.read()
+    returncode = process.wait()
+    if not isinstance(stdout_stream, str):
+        stdout_stream = stdout_stream.decode('utf-8')
+    if not isinstance(stderr_stream, str):
+        stderr_stream = stderr_stream.decode('utf-8')
+    stdout = stdout_stream.splitlines()
+    stderr = stderr_stream.splitlines()
+
+    if returncode != 0:
+        # set to true so that we can log the stderr/stdout that callers would
+        # do anyway
+        terminal_verbose = True
+
+    # the following can get a messed up order in the log if the system call
+    # returns output with both stderr and stdout intermingled. This separates
+    # that.
+    for line in stdout:
+        log_output('stdout', line, terminal_verbose)
+    for line in stderr:
+        log_output('stderr', line, terminal_verbose)
+    return stdout, stderr, returncode