def test_strip_shell_chars(self): self.assertEqual(strip_shell_chars(None), None) self.assertEqual(strip_shell_chars("foo"), "foo") self.assertEqual(strip_shell_chars("foo\r"), "foo") self.assertEqual(strip_shell_chars("fo\ro\r"), "fo\ro") self.assertEqual(strip_shell_chars("foo\n"), "foo") self.assertEqual(strip_shell_chars("fo\no\n"), "fo\no") self.assertEqual(strip_shell_chars("foo\r\n"), "foo") self.assertEqual(strip_shell_chars("fo\no\r\n"), "fo\no") self.assertEqual(strip_shell_chars("foo\r\n\r\n"), "foo\r\n")
def test_strip_shell_chars(self): self.assertEqual(strip_shell_chars(None), None) self.assertEqual(strip_shell_chars('foo'), 'foo') self.assertEqual(strip_shell_chars('foo\r'), 'foo') self.assertEqual(strip_shell_chars('fo\ro\r'), 'fo\ro') self.assertEqual(strip_shell_chars('foo\n'), 'foo') self.assertEqual(strip_shell_chars('fo\no\n'), 'fo\no') self.assertEqual(strip_shell_chars('foo\r\n'), 'foo') self.assertEqual(strip_shell_chars('fo\no\r\n'), 'fo\no') self.assertEqual(strip_shell_chars('foo\r\n\r\n'), 'foo\r\n')
def _consume_stderr(self, chan, call_line_handler_func=False): """ Try to consume stderr data from chan if it's receive ready. """ out = bytearray() stderr = StringIO() if chan.recv_stderr_ready(): data = chan.recv_stderr(self.CHUNK_SIZE) if six.PY3 and isinstance(data, six.text_type): data = data.encode('utf-8') out += data while data: ready = chan.recv_stderr_ready() if not ready: break data = chan.recv_stderr(self.CHUNK_SIZE) if six.PY3 and isinstance(data, six.text_type): data = data.encode('utf-8') out += data stderr.write(self._get_decoded_data(out)) if self._handle_stderr_line_func and call_line_handler_func: data = strip_shell_chars(stderr.getvalue()) lines = data.split('\n') lines = [line for line in lines if line] for line in lines: # Note: If this function performs network operating no sleep is # needed, otherwise if a long blocking operating is performed, # sleep is recommended to yield and prevent from busy looping self._handle_stderr_line_func(line=line + '\n') stderr.seek(0) return stderr
def run(self, cmd, timeout=None, quote=False): """ Note: This function is based on paramiko's exec_command() method. :param timeout: How long to wait (in seconds) for the command to finish (optional). :type timeout: ``float`` """ if quote: cmd = quote_unix(cmd) extra = {"_cmd": cmd} self.logger.info("Executing command", extra=extra) # Use the system default buffer size bufsize = -1 transport = self.client.get_transport() chan = transport.open_session() start_time = time.time() if cmd.startswith("sudo"): # Note that fabric does this as well. If you set pty, stdout and stderr # streams will be combined into one. chan.get_pty() chan.exec_command(cmd) stdout = StringIO() stderr = StringIO() # Create a stdin file and immediately close it to prevent any # interactive script from hanging the process. stdin = chan.makefile("wb", bufsize) stdin.close() # Receive all the output # Note #1: This is used instead of chan.makefile approach to prevent # buffering issues and hanging if the executed command produces a lot # of output. # # Note #2: If you are going to remove "ready" checks inside the loop # you are going to have a bad time. Trying to consume from a channel # which is not ready will block for indefinitely. exit_status_ready = chan.exit_status_ready() if exit_status_ready: stdout.write(self._consume_stdout(chan).getvalue()) stderr.write(self._consume_stderr(chan).getvalue()) while not exit_status_ready: current_time = time.time() elapsed_time = current_time - start_time if timeout and (elapsed_time > timeout): # TODO: Is this the right way to clean up? chan.close() stdout = strip_shell_chars(stdout.getvalue()) stderr = strip_shell_chars(stderr.getvalue()) raise SSHCommandTimeoutError(cmd=cmd, timeout=timeout, stdout=stdout, stderr=stderr) stdout.write(self._consume_stdout(chan).getvalue()) stderr.write(self._consume_stderr(chan).getvalue()) # We need to check the exist status here, because the command could # print some output and exit during this sleep bellow. exit_status_ready = chan.exit_status_ready() if exit_status_ready: break # Short sleep to prevent busy waiting eventlet.sleep(self.SLEEP_DELAY) # print('Wait over. Channel must be ready for host: %s' % self.hostname) # Receive the exit status code of the command we ran. status = chan.recv_exit_status() stdout = strip_shell_chars(stdout.getvalue()) stderr = strip_shell_chars(stderr.getvalue()) extra = {"_status": status, "_stdout": stdout, "_stderr": stderr} self.logger.debug("Command finished", extra=extra) return [stdout, stderr, status]
def run(self, action_parameters): env_vars = self._env if not self.entry_point: script_action = False command = self.runner_parameters.get(RUNNER_COMMAND, None) action = ShellCommandAction(name=self.action_name, action_exec_id=str(self.liveaction_id), command=command, user=self._user, env_vars=env_vars, sudo=self._sudo, timeout=self._timeout, sudo_password=self._sudo_password) else: script_action = True script_local_path_abs = self.entry_point positional_args, named_args = self._get_script_args(action_parameters) named_args = self._transform_named_args(named_args) action = ShellScriptAction(name=self.action_name, action_exec_id=str(self.liveaction_id), script_local_path_abs=script_local_path_abs, named_args=named_args, positional_args=positional_args, user=self._user, env_vars=env_vars, sudo=self._sudo, timeout=self._timeout, cwd=self._cwd, sudo_password=self._sudo_password) args = action.get_full_command_string() sanitized_args = action.get_sanitized_full_command_string() # For consistency with the old Fabric based runner, make sure the file is executable if script_action: args = 'chmod +x %s ; %s' % (script_local_path_abs, args) sanitized_args = 'chmod +x %s ; %s' % (script_local_path_abs, sanitized_args) env = os.environ.copy() # Include user provided env vars (if any) env.update(env_vars) # Include common st2 env vars st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) LOG.info('Executing action via LocalRunner: %s', self.runner_id) LOG.info('[Action info] name: %s, Id: %s, command: %s, user: %s, sudo: %s' % (action.name, action.action_exec_id, sanitized_args, action.user, action.sudo)) stdout = StringIO() stderr = StringIO() store_execution_stdout_line = functools.partial(store_execution_output_data, output_type='stdout') store_execution_stderr_line = functools.partial(store_execution_output_data, output_type='stderr') read_and_store_stdout = make_read_and_store_stream_func(execution_db=self.execution, action_db=self.action, store_data_func=store_execution_stdout_line) read_and_store_stderr = make_read_and_store_stream_func(execution_db=self.execution, action_db=self.action, store_data_func=store_execution_stderr_line) # If sudo password is provided, pass it to the subprocess via stdin> # Note: We don't need to explicitly escape the argument because we pass command as a list # to subprocess.Popen and all the arguments are escaped by the function. if self._sudo_password: LOG.debug('Supplying sudo password via stdin') echo_process = subprocess.Popen(['echo', self._sudo_password + '\n'], stdout=subprocess.PIPE) stdin = echo_process.stdout else: stdin = None # Make sure os.setsid is called on each spawned process so that all processes # are in the same group. # Process is started as sudo -u {{system_user}} -- bash -c {{command}}. Introduction of the # bash means that multiple independent processes are spawned without them being # children of the process we have access to and this requires use of pkill. # Ideally os.killpg should have done the trick but for some reason that failed. # Note: pkill will set the returncode to 143 so we don't need to explicitly set # it to some non-zero value. exit_code, stdout, stderr, timed_out = shell.run_command(cmd=args, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=self._cwd, env=env, timeout=self._timeout, preexec_func=os.setsid, kill_func=kill_process, read_stdout_func=read_and_store_stdout, read_stderr_func=read_and_store_stderr, read_stdout_buffer=stdout, read_stderr_buffer=stderr) error = None if timed_out: error = 'Action failed to complete in %s seconds' % (self._timeout) exit_code = -1 * exit_code_constants.SIGKILL_EXIT_CODE # Detect if user provided an invalid sudo password or sudo is not configured for that user if self._sudo_password: if re.search('sudo: \d+ incorrect password attempts', stderr): match = re.search('\[sudo\] password for (.+?)\:', stderr) if match: username = match.groups()[0] else: username = '******' error = ('Invalid sudo password provided or sudo is not configured for this user ' '(%s)' % (username)) exit_code = -1 succeeded = (exit_code == exit_code_constants.SUCCESS_EXIT_CODE) result = { 'failed': not succeeded, 'succeeded': succeeded, 'return_code': exit_code, 'stdout': strip_shell_chars(stdout), 'stderr': strip_shell_chars(stderr) } if error: result['error'] = error status = PROC_EXIT_CODE_TO_LIVEACTION_STATUS_MAP.get( str(exit_code), action_constants.LIVEACTION_STATUS_FAILED ) return (status, jsonify.json_loads(result, LocalShellRunner.KEYS_TO_TRANSFORM), None)
def _run(self, action): env_vars = self._env if not self.entry_point: script_action = False else: script_action = True args = action.get_full_command_string() sanitized_args = action.get_sanitized_full_command_string() # For consistency with the old Fabric based runner, make sure the file is executable if script_action: script_local_path_abs = self.entry_point args = 'chmod +x %s ; %s' % (script_local_path_abs, args) sanitized_args = 'chmod +x %s ; %s' % (script_local_path_abs, sanitized_args) env = os.environ.copy() # Include user provided env vars (if any) env.update(env_vars) # Include common st2 env vars st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) LOG.info('Executing action via LocalRunner: %s', self.runner_id) LOG.info('[Action info] name: %s, Id: %s, command: %s, user: %s, sudo: %s' % (action.name, action.action_exec_id, sanitized_args, action.user, action.sudo)) stdout = StringIO() stderr = StringIO() store_execution_stdout_line = functools.partial(store_execution_output_data, output_type='stdout') store_execution_stderr_line = functools.partial(store_execution_output_data, output_type='stderr') read_and_store_stdout = make_read_and_store_stream_func(execution_db=self.execution, action_db=self.action, store_data_func=store_execution_stdout_line) read_and_store_stderr = make_read_and_store_stream_func(execution_db=self.execution, action_db=self.action, store_data_func=store_execution_stderr_line) # If sudo password is provided, pass it to the subprocess via stdin> # Note: We don't need to explicitly escape the argument because we pass command as a list # to subprocess.Popen and all the arguments are escaped by the function. if self._sudo_password: LOG.debug('Supplying sudo password via stdin') echo_process = subprocess.Popen(['echo', self._sudo_password + '\n'], stdout=subprocess.PIPE) stdin = echo_process.stdout else: stdin = None # Make sure os.setsid is called on each spawned process so that all processes # are in the same group. # Process is started as sudo -u {{system_user}} -- bash -c {{command}}. Introduction of the # bash means that multiple independent processes are spawned without them being # children of the process we have access to and this requires use of pkill. # Ideally os.killpg should have done the trick but for some reason that failed. # Note: pkill will set the returncode to 143 so we don't need to explicitly set # it to some non-zero value. exit_code, stdout, stderr, timed_out = shell.run_command(cmd=args, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=self._cwd, env=env, timeout=self._timeout, preexec_func=os.setsid, kill_func=kill_process, read_stdout_func=read_and_store_stdout, read_stderr_func=read_and_store_stderr, read_stdout_buffer=stdout, read_stderr_buffer=stderr) error = None if timed_out: error = 'Action failed to complete in %s seconds' % (self._timeout) exit_code = -1 * exit_code_constants.SIGKILL_EXIT_CODE # Detect if user provided an invalid sudo password or sudo is not configured for that user if self._sudo_password: if re.search(r'sudo: \d+ incorrect password attempts', stderr): match = re.search(r'\[sudo\] password for (.+?)\:', stderr) if match: username = match.groups()[0] else: username = '******' error = ('Invalid sudo password provided or sudo is not configured for this user ' '(%s)' % (username)) exit_code = -1 succeeded = (exit_code == exit_code_constants.SUCCESS_EXIT_CODE) result = { 'failed': not succeeded, 'succeeded': succeeded, 'return_code': exit_code, 'stdout': strip_shell_chars(stdout), 'stderr': strip_shell_chars(stderr) } if error: result['error'] = error status = PROC_EXIT_CODE_TO_LIVEACTION_STATUS_MAP.get( str(exit_code), action_constants.LIVEACTION_STATUS_FAILED ) return (status, jsonify.json_loads(result, BaseLocalShellRunner.KEYS_TO_TRANSFORM), None)
def run(self, cmd, timeout=None, quote=False): """ Note: This function is based on paramiko's exec_command() method. :param timeout: How long to wait (in seconds) for the command to finish (optional). :type timeout: ``float`` """ if quote: cmd = quote_unix(cmd) extra = {'_cmd': cmd} self.logger.info('Executing command', extra=extra) # Use the system default buffer size bufsize = -1 transport = self.client.get_transport() chan = transport.open_session() start_time = time.time() if cmd.startswith('sudo'): # Note that fabric does this as well. If you set pty, stdout and stderr # streams will be combined into one. chan.get_pty() chan.exec_command(cmd) stdout = StringIO() stderr = StringIO() # Create a stdin file and immediately close it to prevent any # interactive script from hanging the process. stdin = chan.makefile('wb', bufsize) stdin.close() # Receive all the output # Note #1: This is used instead of chan.makefile approach to prevent # buffering issues and hanging if the executed command produces a lot # of output. # # Note #2: If you are going to remove "ready" checks inside the loop # you are going to have a bad time. Trying to consume from a channel # which is not ready will block for indefinitely. exit_status_ready = chan.exit_status_ready() if exit_status_ready: stdout.write(self._consume_stdout(chan).getvalue()) stderr.write(self._consume_stderr(chan).getvalue()) while not exit_status_ready: current_time = time.time() elapsed_time = (current_time - start_time) if timeout and (elapsed_time > timeout): # TODO: Is this the right way to clean up? chan.close() stdout = strip_shell_chars(stdout.getvalue()) stderr = strip_shell_chars(stderr.getvalue()) raise SSHCommandTimeoutError(cmd=cmd, timeout=timeout, stdout=stdout, stderr=stderr) stdout.write(self._consume_stdout(chan).getvalue()) stderr.write(self._consume_stderr(chan).getvalue()) # We need to check the exist status here, because the command could # print some output and exit during this sleep bellow. exit_status_ready = chan.exit_status_ready() if exit_status_ready: break # Short sleep to prevent busy waiting eventlet.sleep(self.SLEEP_DELAY) # print('Wait over. Channel must be ready for host: %s' % self.hostname) # Receive the exit status code of the command we ran. status = chan.recv_exit_status() stdout = strip_shell_chars(stdout.getvalue()) stderr = strip_shell_chars(stderr.getvalue()) extra = {'_status': status, '_stdout': stdout, '_stderr': stderr} self.logger.debug('Command finished', extra=extra) return [stdout, stderr, status]
def run(self, action_parameters): env_vars = self._env if not self.entry_point: script_action = False command = self.runner_parameters.get(RUNNER_COMMAND, None) action = ShellCommandAction(name=self.action_name, action_exec_id=str(self.liveaction_id), command=command, user=self._user, env_vars=env_vars, sudo=self._sudo, timeout=self._timeout) else: script_action = True script_local_path_abs = self.entry_point positional_args, named_args = self._get_script_args( action_parameters) named_args = self._transform_named_args(named_args) action = ShellScriptAction( name=self.action_name, action_exec_id=str(self.liveaction_id), script_local_path_abs=script_local_path_abs, named_args=named_args, positional_args=positional_args, user=self._user, env_vars=env_vars, sudo=self._sudo, timeout=self._timeout, cwd=self._cwd) args = action.get_full_command_string() # For consistency with the old Fabric based runner, make sure the file is executable if script_action: args = 'chmod +x %s ; %s' % (script_local_path_abs, args) env = os.environ.copy() # Include user provided env vars (if any) env.update(env_vars) # Include common st2 env vars st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) LOG.info('Executing action via LocalRunner: %s', self.runner_id) LOG.info( '[Action info] name: %s, Id: %s, command: %s, user: %s, sudo: %s' % (action.name, action.action_exec_id, args, action.user, action.sudo)) # Make sure os.setsid is called on each spawned process so that all processes # are in the same group. # Process is started as sudo -u {{system_user}} -- bash -c {{command}}. Introduction of the # bash means that multiple independent processes are spawned without them being # children of the process we have access to and this requires use of pkill. # Ideally os.killpg should have done the trick but for some reason that failed. # Note: pkill will set the returncode to 143 so we don't need to explicitly set # it to some non-zero value. exit_code, stdout, stderr, timed_out = run_command( cmd=args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=self._cwd, env=env, timeout=self._timeout, preexec_func=os.setsid, kill_func=kill_process) error = None if timed_out: error = 'Action failed to complete in %s seconds' % (self._timeout) exit_code = -9 succeeded = (exit_code == 0) result = { 'failed': not succeeded, 'succeeded': succeeded, 'return_code': exit_code, 'stdout': strip_shell_chars(stdout), 'stderr': strip_shell_chars(stderr) } if error: result['error'] = error if exit_code == 0: status = LIVEACTION_STATUS_SUCCEEDED elif timed_out: status = LIVEACTION_STATUS_TIMED_OUT else: status = LIVEACTION_STATUS_FAILED return (status, jsonify.json_loads(result, LocalShellRunner.KEYS_TO_TRANSFORM), None)
def run(self, action_parameters): env_vars = self._env if not self.entry_point: script_action = False command = self.runner_parameters.get(RUNNER_COMMAND, None) action = ShellCommandAction( name=self.action_name, action_exec_id=str(self.liveaction_id), command=command, user=self._user, env_vars=env_vars, sudo=self._sudo, timeout=self._timeout, ) else: script_action = True script_local_path_abs = self.entry_point positional_args, named_args = self._get_script_args(action_parameters) named_args = self._transform_named_args(named_args) action = ShellScriptAction( name=self.action_name, action_exec_id=str(self.liveaction_id), script_local_path_abs=script_local_path_abs, named_args=named_args, positional_args=positional_args, user=self._user, env_vars=env_vars, sudo=self._sudo, timeout=self._timeout, cwd=self._cwd, ) args = action.get_full_command_string() # For consistency with the old Fabric based runner, make sure the file is executable if script_action: args = "chmod +x %s ; %s" % (script_local_path_abs, args) env = os.environ.copy() # Include user provided env vars (if any) env.update(env_vars) # Include common st2 env vars st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) LOG.info("Executing action via LocalRunner: %s", self.runner_id) LOG.info( "[Action info] name: %s, Id: %s, command: %s, user: %s, sudo: %s" % (action.name, action.action_exec_id, args, action.user, action.sudo) ) # Make sure os.setsid is called on each spawned process so that all processes # are in the same group. # Process is started as sudo -u {{system_user}} -- bash -c {{command}}. Introduction of the # bash means that multiple independent processes are spawned without them being # children of the process we have access to and this requires use of pkill. # Ideally os.killpg should have done the trick but for some reason that failed. # Note: pkill will set the returncode to 143 so we don't need to explicitly set # it to some non-zero value. exit_code, stdout, stderr, timed_out = run_command( cmd=args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=self._cwd, env=env, timeout=self._timeout, preexec_func=os.setsid, kill_func=kill_process, ) error = None if timed_out: error = "Action failed to complete in %s seconds" % (self._timeout) exit_code = -9 succeeded = exit_code == 0 result = { "failed": not succeeded, "succeeded": succeeded, "return_code": exit_code, "stdout": strip_shell_chars(stdout), "stderr": strip_shell_chars(stderr), } if error: result["error"] = error if exit_code == 0: status = LIVEACTION_STATUS_SUCCEEDED elif timed_out: status = LIVEACTION_STATUS_TIMED_OUT else: status = LIVEACTION_STATUS_FAILED return (status, jsonify.json_loads(result, LocalShellRunner.KEYS_TO_TRANSFORM), None)
def _run(self, action): env_vars = self._env if not self.entry_point: script_action = False else: script_action = True args = action.get_full_command_string() sanitized_args = action.get_sanitized_full_command_string() # For consistency with the old Fabric based runner, make sure the file is executable if script_action: script_local_path_abs = self.entry_point args = "chmod +x %s ; %s" % (script_local_path_abs, args) sanitized_args = "chmod +x %s ; %s" % ( script_local_path_abs, sanitized_args, ) env = os.environ.copy() # Include user provided env vars (if any) env.update(env_vars) # Include common st2 env vars st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) LOG.info("Executing action via LocalRunner: %s", self.runner_id) LOG.info( "[Action info] name: %s, Id: %s, command: %s, user: %s, sudo: %s" % ( action.name, action.action_exec_id, sanitized_args, action.user, action.sudo, ) ) stdout = StringIO() stderr = StringIO() store_execution_stdout_line = functools.partial( store_execution_output_data, output_type="stdout" ) store_execution_stderr_line = functools.partial( store_execution_output_data, output_type="stderr" ) read_and_store_stdout = make_read_and_store_stream_func( execution_db=self.execution, action_db=self.action, store_data_func=store_execution_stdout_line, ) read_and_store_stderr = make_read_and_store_stream_func( execution_db=self.execution, action_db=self.action, store_data_func=store_execution_stderr_line, ) subprocess = concurrency.get_subprocess_module() # If sudo password is provided, pass it to the subprocess via stdin> # Note: We don't need to explicitly escape the argument because we pass command as a list # to subprocess.Popen and all the arguments are escaped by the function. if self._sudo_password: LOG.debug("Supplying sudo password via stdin") echo_process = concurrency.subprocess_popen( ["echo", self._sudo_password + "\n"], stdout=subprocess.PIPE ) stdin = echo_process.stdout else: stdin = None # Make sure os.setsid is called on each spawned process so that all processes # are in the same group. # Process is started as sudo -u {{system_user}} -- bash -c {{command}}. Introduction of the # bash means that multiple independent processes are spawned without them being # children of the process we have access to and this requires use of pkill. # Ideally os.killpg should have done the trick but for some reason that failed. # Note: pkill will set the returncode to 143 so we don't need to explicitly set # it to some non-zero value. exit_code, stdout, stderr, timed_out = shell.run_command( cmd=args, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=self._cwd, env=env, timeout=self._timeout, preexec_func=os.setsid, kill_func=kill_process, read_stdout_func=read_and_store_stdout, read_stderr_func=read_and_store_stderr, read_stdout_buffer=stdout, read_stderr_buffer=stderr, ) error = None if timed_out: error = "Action failed to complete in %s seconds" % (self._timeout) exit_code = -1 * exit_code_constants.SIGKILL_EXIT_CODE # Detect if user provided an invalid sudo password or sudo is not configured for that user if self._sudo_password: if re.search(r"sudo: \d+ incorrect password attempts", stderr): match = re.search(r"\[sudo\] password for (.+?)\:", stderr) if match: username = match.groups()[0] else: username = "******" error = ( "Invalid sudo password provided or sudo is not configured for this user " "(%s)" % (username) ) exit_code = -1 succeeded = exit_code == exit_code_constants.SUCCESS_EXIT_CODE result = { "failed": not succeeded, "succeeded": succeeded, "return_code": exit_code, "stdout": strip_shell_chars(stdout), "stderr": strip_shell_chars(stderr), } if error: result["error"] = error status = PROC_EXIT_CODE_TO_LIVEACTION_STATUS_MAP.get( str(exit_code), action_constants.LIVEACTION_STATUS_FAILED ) return ( status, jsonify.json_loads(result, BaseLocalShellRunner.KEYS_TO_TRANSFORM), None, )