98162c84ef6a1b3622d56b94b1257d51f791adef
[sdnvpn.git] / odl-pipeline / lib / utils / processutils.py
1 #
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
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 #
10 import utils_log as log
11 import os
12 import six
13 import re
14 import signal
15 import subprocess
16 from time import sleep
17 from threading import Thread
18 try:
19     from Queue import Queue
20 except ImportError:
21     from queue import Queue  # python 3.x
22
23 LOG = log.LOG
24 LOG_LEVEL = log.LOG_LEVEL
25
26
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)
31
32 # NOTE(flaper87): The following globals are used by `mask_password`
33 _SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
34
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 = []
41
42
43 def mask_password(message, secret="***"):
44     """Replace password with 'secret' in message.
45
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.
49
50     For example:
51
52     >>> mask_password("'adminPass' : 'aaaaa'")
53     "'adminPass' : '***'"
54     >>> mask_password("'admin_pass' : 'aaaaa'")
55     "'admin_pass' : '***'"
56     >>> mask_password('"password" : "aaaaa"')
57     '"password" : "***"'
58     >>> mask_password("'original_password' : 'aaaaa'")
59     "'original_password' : '***'"
60     >>> mask_password("u'original_password' :   u'aaaaa'")
61     "u'original_password' :   u'***'"
62     """
63     try:
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.
68         pass
69
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):
74         return message
75
76     substitute = r'\g<1>' + secret + r'\g<2>'
77     for pattern in _SANITIZE_PATTERNS_2:
78         message = re.sub(pattern, substitute, message)
79
80     substitute = r'\g<1>' + secret
81     for pattern in _SANITIZE_PATTERNS_1:
82         message = re.sub(pattern, substitute, message)
83
84     return message
85
86
87 class ProcessExecutionError(Exception):
88
89     def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
90                  description=None):
91         self.exit_code = exit_code
92         self.stderr = stderr
93         self.stdout = stdout
94         self.cmd = cmd
95         self.description = description
96
97         if description is None:
98             description = "Unexpected error while running command."
99         if exit_code is None:
100             exit_code = '-'
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)
104
105
106 def enqueue_output(out, queue):
107     for line in iter(out.readline, b''):
108         queue.put(line)
109     queue.put("##Finished##")
110     out.close()
111
112
113 def execute(cmd, **kwargs):
114     """Helper method to shell out and execute a command through subprocess.
115
116     Allows optional retry.
117
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.
131     :type attempts:         int
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
137     :type shell:            boolean
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
143                             execution
144     :raises:                :class:`UnknownArgumentError` on
145                             receiving unknown arguments
146     :raises:                :class:`ProcessExecutionError`
147     """
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',
153                                                                  False)
154     shell = kwargs.pop('shell', False)
155     loglevel = kwargs.pop('loglevel', LOG_LEVEL)
156     non_blocking = kwargs.pop('non_blocking', False)
157
158     if not isinstance(cmd, list):
159         cmd = cmd.split(' ')
160
161     if run_as_root:
162         cmd = ['sudo'] + cmd
163     if shell:
164         cmd = ' '.join(cmd)
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]
170
171     if kwargs:
172         raise Exception(('Got unknown keyword args '
173                          'to utils.execute: %r') % kwargs)
174
175     while attempts > 0:
176         attempts -= 1
177         try:
178             LOG.log(loglevel, ('Running cmd (subprocess): %s'), cmd)
179             _PIPE = subprocess.PIPE  # pylint: disable=E1101
180
181             if os.name == 'nt':
182                 preexec_fn = None
183                 close_fds = False
184             else:
185                 preexec_fn = _subprocess_setup
186                 close_fds = True
187
188             obj = subprocess.Popen(cmd,
189                                    stdin=_PIPE,
190                                    stdout=_PIPE,
191                                    stderr=_PIPE,
192                                    close_fds=close_fds,
193                                    preexec_fn=preexec_fn,
194                                    shell=shell)
195             result = None
196             if process_input is not None:
197                 result = obj.communicate(process_input)
198             else:
199                 if non_blocking:
200                     queue = Queue()
201                     thread = Thread(target=enqueue_output, args=(obj.stdout,
202                                                                  queue))
203                     thread.deamon = True
204                     thread.start()
205                     # If you want to read this output later:
206                     # try:
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)
211                     # except Empty:
212                     #     print('no output yet')
213                     # else: # got line
214                     # ... do something with line
215                     return queue
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:
232             raise
233         finally:
234             sleep(0)