1 from fcntl
import fcntl
, F_GETFL
, F_SETFL
2 from os
import O_NONBLOCK
, read
, path
4 from select
import select
5 from ceph_volume
import terminal
6 from ceph_volume
.util
import as_bytes
7 from ceph_volume
.util
.system
import which
, run_host_cmd
, host_rootfs
11 logger
= logging
.getLogger(__name__
)
14 def log_output(descriptor
, message
, terminal_logging
, logfile_logging
):
16 log output to both the logger and the terminal if terminal_logging is
21 message
= message
.strip()
22 line
= '%s %s' % (descriptor
, message
)
24 getattr(terminal
, descriptor
)(message
)
29 def log_descriptors(reads
, process
, terminal_logging
):
31 Helper to send output to the terminal while polling the subprocess
33 # these fcntl are set to O_NONBLOCK for the filedescriptors coming from
34 # subprocess so that the logging does not block. Without these a prompt in
35 # a subprocess output would hang and nothing would get printed. Note how
36 # these are just set when logging subprocess, not globally.
37 stdout_flags
= fcntl(process
.stdout
, F_GETFL
) # get current p.stdout flags
38 stderr_flags
= fcntl(process
.stderr
, F_GETFL
) # get current p.stderr flags
39 fcntl(process
.stdout
, F_SETFL
, stdout_flags | O_NONBLOCK
)
40 fcntl(process
.stderr
, F_SETFL
, stderr_flags | O_NONBLOCK
)
42 process
.stdout
.fileno(): 'stdout',
43 process
.stderr
.fileno(): 'stderr'
45 for descriptor
in reads
:
46 descriptor_name
= descriptor_names
[descriptor
]
48 message
= read(descriptor
, 1024)
49 if not isinstance(message
, str):
50 message
= message
.decode('utf-8')
51 log_output(descriptor_name
, message
, terminal_logging
, True)
52 except (IOError, OSError):
57 def obfuscate(command_
, on
=None):
59 Certain commands that are useful to log might contain information that
60 should be replaced by '*' like when creating OSDs and the keyrings are
61 being passed, which should not be logged.
63 :param on: A string (will match a flag) or an integer (will match an index)
65 If matching on a flag (when ``on`` is a string) it will obfuscate on the
66 value for that flag. That is a command like ['ls', '-l', '/'] that calls
67 `obfuscate(command, on='-l')` will obfustace '/' which is the value for
70 The reason for `on` to allow either a string or an integer, altering
71 behavior for both is because it is easier for ``run`` and ``call`` to just
72 pop a value to obfuscate (vs. allowing an index or a flag)
75 msg
= "Running command: %s" % ' '.join(command
)
76 if on
in [None, False]:
79 if isinstance(on
, int):
84 index
= command
.index(on
) + 1
86 # if the flag just doesn't exist then it doesn't matter just return
91 command
[index
] = '*' * len(command
[index
])
92 except IndexError: # the index was completely out of range
95 return "Running command: %s" % ' '.join(command
)
98 def run(command
, run_on_host
=False, **kw
):
100 A real-time-logging implementation of a remote subprocess.Popen call where
101 a command is just executed on the remote end and no other handling is done.
103 :param command: The command to pass in to the remote subprocess.Popen as a list
104 :param stop_on_error: If a nonzero exit status is return, it raises a ``RuntimeError``
105 :param fail_msg: If a nonzero exit status is returned this message will be included in the log
107 executable
= which(command
.pop(0), run_on_host
)
108 command
.insert(0, executable
)
109 if run_on_host
and path
.isdir(host_rootfs
):
110 command
= run_host_cmd
+ command
111 stop_on_error
= kw
.pop('stop_on_error', True)
112 command_msg
= obfuscate(command
, kw
.pop('obfuscate', None))
113 fail_msg
= kw
.pop('fail_msg', None)
114 logger
.info(command_msg
)
115 terminal
.write(command_msg
)
116 terminal_logging
= kw
.pop('terminal_logging', True)
118 process
= subprocess
.Popen(
120 stdout
=subprocess
.PIPE
,
121 stderr
=subprocess
.PIPE
,
127 reads
, _
, _
= select(
128 [process
.stdout
.fileno(), process
.stderr
.fileno()],
131 log_descriptors(reads
, process
, terminal_logging
)
133 if process
.poll() is not None:
134 # ensure we do not have anything pending in stdout or stderr
135 log_descriptors(reads
, process
, terminal_logging
)
139 returncode
= process
.wait()
141 msg
= "command returned non-zero exit status: %s" % returncode
143 logger
.warning(fail_msg
)
145 terminal
.warning(fail_msg
)
147 raise RuntimeError(msg
)
150 terminal
.warning(msg
)
154 def call(command
, run_on_host
=False, **kw
):
156 Similar to ``subprocess.Popen`` with the following changes:
158 * returns stdout, stderr, and exit code (vs. just the exit code)
159 * logs the full contents of stderr and stdout (separately) to the file log
161 By default, no terminal output is given, not even the command that is going
164 Useful when system calls are needed to act on output, and that same output
165 shouldn't get displayed on the terminal.
167 Optionally, the command can be displayed on the terminal and the log file,
168 and log file output can be turned off. This is useful to prevent sensitive
169 output going to stderr/stdout and being captured on a log file.
171 :param terminal_verbose: Log command output to terminal, defaults to False, and
172 it is forcefully set to True if a return code is non-zero
173 :param logfile_verbose: Log stderr/stdout output to log file. Defaults to True
174 :param verbose_on_failure: On a non-zero exit status, it will forcefully set logging ON for
175 the terminal. Defaults to True
177 executable
= which(command
.pop(0), run_on_host
)
178 command
.insert(0, executable
)
179 if run_on_host
and path
.isdir(host_rootfs
):
180 command
= run_host_cmd
+ command
181 terminal_verbose
= kw
.pop('terminal_verbose', False)
182 logfile_verbose
= kw
.pop('logfile_verbose', True)
183 verbose_on_failure
= kw
.pop('verbose_on_failure', True)
184 show_command
= kw
.pop('show_command', False)
185 command_msg
= "Running command: %s" % ' '.join(command
)
186 stdin
= kw
.pop('stdin', None)
187 logger
.info(command_msg
)
189 terminal
.write(command_msg
)
191 process
= subprocess
.Popen(
193 stdout
=subprocess
.PIPE
,
194 stderr
=subprocess
.PIPE
,
195 stdin
=subprocess
.PIPE
,
201 stdout_stream
, stderr_stream
= process
.communicate(as_bytes(stdin
))
203 stdout_stream
= process
.stdout
.read()
204 stderr_stream
= process
.stderr
.read()
205 returncode
= process
.wait()
206 if not isinstance(stdout_stream
, str):
207 stdout_stream
= stdout_stream
.decode('utf-8')
208 if not isinstance(stderr_stream
, str):
209 stderr_stream
= stderr_stream
.decode('utf-8')
210 stdout
= stdout_stream
.splitlines()
211 stderr
= stderr_stream
.splitlines()
214 # set to true so that we can log the stderr/stdout that callers would
215 # do anyway as long as verbose_on_failure is set (defaults to True)
216 if verbose_on_failure
:
217 terminal_verbose
= True
218 # logfiles aren't disruptive visually, unlike the terminal, so this
219 # should always be on when there is a failure
220 logfile_verbose
= True
222 # the following can get a messed up order in the log if the system call
223 # returns output with both stderr and stdout intermingled. This separates
226 log_output('stdout', line
, terminal_verbose
, logfile_verbose
)
228 log_output('stderr', line
, terminal_verbose
, logfile_verbose
)
229 return stdout
, stderr
, returncode