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)
32 # NOTE(flaper87): The following globals are used by `mask_password`
33 _SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
35 # NOTE(ldbragst): Let's build a list of regex objects using the list of
36 # _SANITIZE_KEYS we already have. This way, we only have to add the new key
37 # to the list of _SANITIZE_KEYS and we can generate regular expressions
38 # for XML and JSON automatically.
39 _SANITIZE_PATTERNS_2 = []
40 _SANITIZE_PATTERNS_1 = []
43 def mask_password(message, secret="***"):
44 """Replace password with 'secret' in message.
46 :param message: The string which includes security information.
47 :param secret: value with which to replace passwords.
48 :returns: The unicode value of message with the password fields masked.
52 >>> mask_password("'adminPass' : 'aaaaa'")
54 >>> mask_password("'admin_pass' : 'aaaaa'")
55 "'admin_pass' : '***'"
56 >>> mask_password('"password" : "aaaaa"')
58 >>> mask_password("'original_password' : 'aaaaa'")
59 "'original_password' : '***'"
60 >>> mask_password("u'original_password' : u'aaaaa'")
61 "u'original_password' : u'***'"
64 message = six.text_type(message)
65 except UnicodeDecodeError:
66 # NOTE(jecarey): Temporary fix to handle cases where message is a
67 # byte string. A better solution will be provided in Kilo.
70 # NOTE(ldbragst): Check to see if anything in message contains any key
71 # specified in _SANITIZE_KEYS, if not then just return the message since
72 # we don't have to mask any passwords.
73 if not any(key in message for key in _SANITIZE_KEYS):
76 substitute = r'\g<1>' + secret + r'\g<2>'
77 for pattern in _SANITIZE_PATTERNS_2:
78 message = re.sub(pattern, substitute, message)
80 substitute = r'\g<1>' + secret
81 for pattern in _SANITIZE_PATTERNS_1:
82 message = re.sub(pattern, substitute, message)
87 class ProcessExecutionError(Exception):
89 def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
91 self.exit_code = exit_code
95 self.description = description
97 if description is None:
98 description = "Unexpected error while running command."
101 message = ("%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r"
102 % (description, cmd, exit_code, stdout, stderr))
103 super(ProcessExecutionError, self).__init__(message)
106 def enqueue_output(out, queue):
107 for line in iter(out.readline, b''):
109 queue.put("##Finished##")
113 def execute(cmd, **kwargs):
114 """Helper method to shell out and execute a command through subprocess.
116 Allows optional retry.
118 :param cmd: Passed to subprocess.Popen.
119 :type cmd: list - will be converted if needed
120 :param process_input: Send to opened process.
121 :type proces_input: string
122 :param check_exit_code: Single bool, int, or list of allowed exit
123 codes. Defaults to [0]. Raise
124 :class:`ProcessExecutionError` unless
125 program exits with one of these code.
126 :type check_exit_code: boolean, int, or [int]
127 :param delay_on_retry: True | False. Defaults to True. If set to True,
128 wait a short amount of time before retrying.
129 :type delay_on_retry: boolean
130 :param attempts: How many times to retry cmd.
132 :param run_as_root: True | False. Defaults to False. If set to True,
133 or as_root the command is prefixed by the command specified
134 in the root_helper kwarg.
135 execute this command. Defaults to false.
136 :param shell: whether or not there should be a shell used to
138 :param loglevel: log level for execute commands.
139 :type loglevel: int. (Should be logging.DEBUG or logging.INFO)
140 :param non_blocking Execute in background.
141 :type non_blockig: boolean
142 :returns: (stdout, (stderr, returncode)) from process
144 :raises: :class:`UnknownArgumentError` on
145 receiving unknown arguments
146 :raises: :class:`ProcessExecutionError`
148 process_input = kwargs.pop('process_input', None)
149 check_exit_code = kwargs.pop('check_exit_code', [0])
150 ignore_exit_code = False
151 attempts = kwargs.pop('attempts', 1)
152 run_as_root = kwargs.pop('run_as_root', False) or kwargs.pop('as_root',
154 shell = kwargs.pop('shell', False)
155 loglevel = kwargs.pop('loglevel', LOG_LEVEL)
156 non_blocking = kwargs.pop('non_blocking', False)
158 if not isinstance(cmd, list):
165 if isinstance(check_exit_code, bool):
166 ignore_exit_code = not check_exit_code
167 check_exit_code = [0]
168 elif isinstance(check_exit_code, int):
169 check_exit_code = [check_exit_code]
172 raise Exception(('Got unknown keyword args '
173 'to utils.execute: %r') % kwargs)
178 LOG.log(loglevel, ('Running cmd (subprocess): %s'), cmd)
179 _PIPE = subprocess.PIPE # pylint: disable=E1101
185 preexec_fn = _subprocess_setup
188 obj = subprocess.Popen(cmd,
193 preexec_fn=preexec_fn,
196 if process_input is not None:
197 result = obj.communicate(process_input)
201 thread = Thread(target=enqueue_output, args=(obj.stdout,
205 # If you want to read this output later:
207 # from Queue import Queue, Empty
208 # except ImportError:
209 # from queue import Queue, Empty # python 3.x
210 # try: line = q.get_nowait() # or q.get(timeout=.1)
212 # print('no output yet')
214 # ... do something with line
216 result = obj.communicate()
217 obj.stdin.close() # pylint: disable=E1101
218 _returncode = obj.returncode # pylint: disable=E1101
219 LOG.log(loglevel, ('Result was %s') % _returncode)
220 if not ignore_exit_code and _returncode not in check_exit_code:
221 (stdout, stderr) = result
222 sanitized_stdout = mask_password(stdout)
223 sanitized_stderr = mask_password(stderr)
224 raise ProcessExecutionError(
225 exit_code=_returncode,
226 stdout=sanitized_stdout,
227 stderr=sanitized_stderr,
228 cmd=(' '.join(cmd)) if isinstance(cmd, list) else cmd)
229 (stdout, stderr) = result
230 return stdout, (stderr, _returncode)
231 except ProcessExecutionError: