2 # Copyright (c) 2017 All rights reserved
3 # This program and the accompanying materials
4 # are made available under the terms of the Apache License, Version 2.0
5 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
10 import utils_log as log
16 from time import sleep
17 from threading import Thread
19 from Queue import Queue
21 from queue import Queue # python 3.x
24 LOG_LEVEL = log.LOG_LEVEL
27 def _subprocess_setup():
28 # Python installs a SIGPIPE handler by default. This is usually not what
29 # non-Python subprocesses expect.
30 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
33 # NOTE(flaper87): The following globals are used by `mask_password`
34 _SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
36 # NOTE(ldbragst): Let's build a list of regex objects using the list of
37 # _SANITIZE_KEYS we already have. This way, we only have to add the new key
38 # to the list of _SANITIZE_KEYS and we can generate regular expressions
39 # for XML and JSON automatically.
40 _SANITIZE_PATTERNS_2 = []
41 _SANITIZE_PATTERNS_1 = []
44 def mask_password(message, secret="***"):
45 """Replace password with 'secret' in message.
47 :param message: The string which includes security information.
48 :param secret: value with which to replace passwords.
49 :returns: The unicode value of message with the password fields masked.
53 >>> mask_password("'adminPass' : 'aaaaa'")
55 >>> mask_password("'admin_pass' : 'aaaaa'")
56 "'admin_pass' : '***'"
57 >>> mask_password('"password" : "aaaaa"')
59 >>> mask_password("'original_password' : 'aaaaa'")
60 "'original_password' : '***'"
61 >>> mask_password("u'original_password' : u'aaaaa'")
62 "u'original_password' : u'***'"
65 message = six.text_type(message)
66 except UnicodeDecodeError:
67 # NOTE(jecarey): Temporary fix to handle cases where message is a
68 # byte string. A better solution will be provided in Kilo.
71 # NOTE(ldbragst): Check to see if anything in message contains any key
72 # specified in _SANITIZE_KEYS, if not then just return the message since
73 # we don't have to mask any passwords.
74 if not any(key in message for key in _SANITIZE_KEYS):
77 substitute = r'\g<1>' + secret + r'\g<2>'
78 for pattern in _SANITIZE_PATTERNS_2:
79 message = re.sub(pattern, substitute, message)
81 substitute = r'\g<1>' + secret
82 for pattern in _SANITIZE_PATTERNS_1:
83 message = re.sub(pattern, substitute, message)
88 class ProcessExecutionError(Exception):
90 def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
92 self.exit_code = exit_code
96 self.description = description
98 if description is None:
99 description = "Unexpected error while running command."
100 if exit_code is None:
102 message = ("%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r"
103 % (description, cmd, exit_code, stdout, stderr))
104 super(ProcessExecutionError, self).__init__(message)
107 def enqueue_output(out, queue):
108 for line in iter(out.readline, b''):
110 queue.put("##Finished##")
114 def execute(cmd, **kwargs):
115 """Helper method to shell out and execute a command through subprocess.
117 Allows optional retry.
119 :param cmd: Passed to subprocess.Popen.
120 :type cmd: list - will be converted if needed
121 :param process_input: Send to opened process.
122 :type proces_input: string
123 :param check_exit_code: Single bool, int, or list of allowed exit
124 codes. Defaults to [0]. Raise
125 :class:`ProcessExecutionError` unless
126 program exits with one of these code.
127 :type check_exit_code: boolean, int, or [int]
128 :param delay_on_retry: True | False. Defaults to True. If set to True,
129 wait a short amount of time before retrying.
130 :type delay_on_retry: boolean
131 :param attempts: How many times to retry cmd.
133 :param run_as_root: True | False. Defaults to False. If set to True,
134 or as_root the command is prefixed by the command specified
135 in the root_helper kwarg.
136 execute this command. Defaults to false.
137 :param shell: whether or not there should be a shell used to
139 :param loglevel: log level for execute commands.
140 :type loglevel: int. (Should be logging.DEBUG or logging.INFO)
141 :param non_blocking Execute in background.
142 :type non_blockig: boolean
143 :returns: (stdout, (stderr, returncode)) from process
145 :raises: :class:`UnknownArgumentError` on
146 receiving unknown arguments
147 :raises: :class:`ProcessExecutionError`
149 process_input = kwargs.pop('process_input', None)
150 check_exit_code = kwargs.pop('check_exit_code', [0])
151 ignore_exit_code = False
152 attempts = kwargs.pop('attempts', 1)
153 run_as_root = kwargs.pop('run_as_root', False) or kwargs.pop('as_root',
155 shell = kwargs.pop('shell', False)
156 loglevel = kwargs.pop('loglevel', LOG_LEVEL)
157 non_blocking = kwargs.pop('non_blocking', False)
159 if not isinstance(cmd, list):
166 if isinstance(check_exit_code, bool):
167 ignore_exit_code = not check_exit_code
168 check_exit_code = [0]
169 elif isinstance(check_exit_code, int):
170 check_exit_code = [check_exit_code]
173 raise Exception(('Got unknown keyword args '
174 'to utils.execute: %r') % kwargs)
179 LOG.log(loglevel, ('Running cmd (subprocess): %s'), cmd)
180 _PIPE = subprocess.PIPE # pylint: disable=E1101
186 preexec_fn = _subprocess_setup
189 obj = subprocess.Popen(cmd,
194 preexec_fn=preexec_fn,
197 if process_input is not None:
198 result = obj.communicate(process_input)
202 thread = Thread(target=enqueue_output, args=(obj.stdout,
206 # If you want to read this output later:
208 # from Queue import Queue, Empty
209 # except ImportError:
210 # from queue import Queue, Empty # python 3.x
211 # try: line = q.get_nowait() # or q.get(timeout=.1)
213 # print('no output yet')
215 # ... do something with line
217 result = obj.communicate()
218 obj.stdin.close() # pylint: disable=E1101
219 _returncode = obj.returncode # pylint: disable=E1101
220 LOG.log(loglevel, ('Result was %s') % _returncode)
221 if not ignore_exit_code and _returncode not in check_exit_code:
222 (stdout, stderr) = result
223 sanitized_stdout = mask_password(stdout)
224 sanitized_stderr = mask_password(stderr)
225 raise ProcessExecutionError(
226 exit_code=_returncode,
227 stdout=sanitized_stdout,
228 stderr=sanitized_stderr,
229 cmd=(' '.join(cmd)) if isinstance(cmd, list) else cmd)
230 (stdout, stderr) = result
231 return stdout, (stderr, _returncode)
232 except ProcessExecutionError: