def run(self, cmd, timeout=None, cwd=None): """ Run a command on remote hosts. Returns a dict containing results of execution from all hosts. :param cmd: Command to run. Must be shlex quoted. :type cmd: ``str`` :param timeout: Optional Timeout for the command. :type timeout: ``int`` :param cwd: Optional Current working directory. Must be shlex quoted. :type cwd: ``str`` :rtype: ``dict`` of ``str`` to ``dict`` """ # Note that doing a chdir using sftp client in ssh_client doesn't really # set the session working directory. So we have to do this hack. if cwd: cmd = 'cd %s && %s' % (cwd, cmd) options = { 'cmd': cmd, 'timeout': timeout } results = self._execute_in_pool(self._run_command, **options) return jsonify.json_loads(results, ParallelSSHClient.KEYS_TO_TRANSFORM)
def _translate_response(self, response): # check exit status for errors succeeded = (response.status_code == exit_code_constants.SUCCESS_EXIT_CODE) status = action_constants.LIVEACTION_STATUS_SUCCEEDED status_code = response.status_code if response.timeout: status = action_constants.LIVEACTION_STATUS_TIMED_OUT status_code = WINRM_TIMEOUT_EXIT_CODE elif not succeeded: status = action_constants.LIVEACTION_STATUS_FAILED # create result result = { 'failed': not succeeded, 'succeeded': succeeded, 'return_code': status_code, 'stdout': response.std_out, 'stderr': response.std_err } # Ensure stdout and stderr is always a string if isinstance(result['stdout'], six.binary_type): result['stdout'] = result['stdout'].decode('utf-8') if isinstance(result['stderr'], six.binary_type): result['stderr'] = result['stderr'].decode('utf-8') # automatically convert result stdout/stderr from JSON strings to # objects so they can be used natively return (status, jsonify.json_loads(result, RESULT_KEYS_TO_TRANSFORM), None)
def _sudo(self): fabric_env_vars = self.env_vars fabric_settings = self._get_settings() try: with shell_env(**fabric_env_vars), settings(**fabric_settings): output = sudo(self.command, combine_stderr=False, pty=True, quiet=True) except Exception: LOG.exception('Failed executing remote action.') result = self._get_error_result() else: result = { 'stdout': output.stdout, 'stderr': output.stderr, 'return_code': output.return_code, 'succeeded': output.succeeded, 'failed': output.failed } finally: self._cleanup(settings=fabric_settings) # XXX: For sudo, fabric requires to set pty=True. This basically combines stdout and # stderr into a single stdout stream. So if the command fails, we explictly set stderr # to stdout and stdout to ''. if result['failed'] and result.get('stdout', None): result['stderr'] = result['stdout'] result['stdout'] = '' return jsonify.json_loads(result, FabricRemoteAction.KEYS_TO_TRANSFORM)
def run(self, action_parameters): result = { 'failed': False, 'succeeded': True, 'return_code': 0, } status = LIVEACTION_STATUS_SUCCEEDED return (status, jsonify.json_loads(result, MockRunner.KEYS_TO_TRANSFORM), None)
def run(self, action_parameters): LOG.info('Executing action via NoopRunner: %s', self.runner_id) LOG.info('[Action info] name: %s, Id: %s', self.action_name, str(self.execution_id)) result = { 'failed': False, 'succeeded': True, 'return_code': 0, } status = LIVEACTION_STATUS_SUCCEEDED return (status, jsonify.json_loads(result, NoopRunner.KEYS_TO_TRANSFORM), None)
def _run_command(self, host, cmd, results, timeout=None): try: LOG.debug('Running command: %s on host: %s.', cmd, host) client = self._hosts_client[host] (stdout, stderr, exit_code) = client.run(cmd, timeout=timeout) is_succeeded = (exit_code == 0) result_dict = {'stdout': stdout, 'stderr': stderr, 'return_code': exit_code, 'succeeded': is_succeeded, 'failed': not is_succeeded} results[host] = jsonify.json_loads(result_dict, ParallelSSHClient.KEYS_TO_TRANSFORM) except: error = 'Failed executing command %s on host %s' % (cmd, host) LOG.exception(error) _, _, tb = sys.exc_info() results[host] = self._generate_error_result(error, tb)
def _run_command(self, host, cmd, results, timeout=None): try: LOG.debug('Running command: %s on host: %s.', cmd, host) client = self._hosts_client[host] (stdout, stderr, exit_code) = client.run(cmd, timeout=timeout) is_succeeded = (exit_code == 0) result_dict = {'stdout': stdout, 'stderr': stderr, 'return_code': exit_code, 'succeeded': is_succeeded, 'failed': not is_succeeded} results[host] = jsonify.json_loads(result_dict, ParallelSSHClient.KEYS_TO_TRANSFORM) except Exception as ex: cmd = self._sanitize_command_string(cmd=cmd) error = 'Failed executing command "%s" on host "%s"' % (cmd, host) LOG.exception(error) results[host] = self._generate_error_result(exc=ex, message=error)
def _handle_command_result(self, stdout, stderr, 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)) raise ValueError(error) is_succeeded = (exit_code == 0) result_dict = {'stdout': stdout, 'stderr': stderr, 'return_code': exit_code, 'succeeded': is_succeeded, 'failed': not is_succeeded} result = jsonify.json_loads(result_dict, ParallelSSHClient.KEYS_TO_TRANSFORM) return result
def run(self, action_parameters): # Note: "inputs" runner parameter has precedence over action parameters if self._inputs: inputs = self._inputs elif action_parameters: inputs = action_parameters else: inputs = None inputs_file_path = self._write_inputs_to_a_temp_file(inputs=inputs) has_inputs = (inputs_file_path is not None) try: command = self._prepare_command(has_inputs=has_inputs, inputs_file_path=inputs_file_path) result, status = self._run_cli_command(command) return (status, jsonify.json_loads(result, CloudSlangRunner.KEYS_TO_TRANSFORM), None) finally: if inputs_file_path and os.path.isfile(inputs_file_path): os.remove(inputs_file_path)
def _run(self): fabric_env_vars = self.env_vars fabric_settings = self._get_settings() try: with shell_env(**fabric_env_vars), settings(**fabric_settings): output = run(self.command, combine_stderr=False, pty=False, quiet=True) except Exception: LOG.exception('Failed executing remote action.') result = self._get_error_result() else: result = { 'stdout': output.stdout, 'stderr': output.stderr, 'return_code': output.return_code, 'succeeded': output.succeeded, 'failed': output.failed } finally: self._cleanup(settings=fabric_settings) return jsonify.json_loads(result, FabricRemoteAction.KEYS_TO_TRANSFORM)
def test_no_keys(self): obj = {'foo': '{"bar": "baz"}'} transformed_obj = jsonify.json_loads(obj) self.assertTrue(transformed_obj['foo']['bar'] == 'baz')
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_last_newline_char(stdout), 'stderr': strip_last_newline_char(stderr) } if error: result['error'] = error status = LIVEACTION_STATUS_SUCCEEDED if exit_code == 0 else LIVEACTION_STATUS_FAILED return (status, jsonify.json_loads(result, LocalShellRunner.KEYS_TO_TRANSFORM), None)
def test_no_json_value(self): obj = {'foo': 'bar'} transformed_obj = jsonify.json_loads(obj) self.assertTrue(transformed_obj['foo'] == 'bar')
def test_happy_case(self): obj = {'foo': '{"bar": "baz"}', 'yo': 'bibimbao'} transformed_obj = jsonify.json_loads(obj, ['yo']) self.assertTrue(transformed_obj['yo'] == 'bibimbao')
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 test_none_object(self): obj = None self.assertTrue(jsonify.json_loads(obj) is None)
def run(self, action_parameters): LOG.debug(' action_parameters = %s', 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) 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 = subprocess.Popen(args=args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, cwd=self._cwd, env=env, preexec_fn=os.setsid) error_holder = {} def on_timeout_expired(timeout): try: process.wait(timeout=self._timeout) except subprocess.TimeoutExpired: # Set the error prior to kill the process else the error is not picked up due # to eventlet scheduling. error_holder['error'] = 'Action failed to complete in %s seconds' % (self._timeout) # Action has timed out, kill the process and propagate the error. The 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. try: killcommand = shlex.split('sudo pkill -TERM -s %s' % process.pid) subprocess.call(killcommand) except: LOG.exception('Unable to pkill.') timeout_expiry = eventlet.spawn(on_timeout_expired, self._timeout) stdout, stderr = process.communicate() timeout_expiry.cancel() error = error_holder.get('error', None) exit_code = process.returncode succeeded = (exit_code == 0) result = { 'failed': not succeeded, 'succeeded': succeeded, 'return_code': exit_code, 'stdout': stdout, 'stderr': stderr } if error: result['error'] = error status = LIVEACTION_STATUS_SUCCEEDED if exit_code == 0 else LIVEACTION_STATUS_FAILED self._log_action_completion(logger=LOG, result=result, status=status, exit_code=exit_code) 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, 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': stdout, 'stderr': stderr } if error: result['error'] = error status = LIVEACTION_STATUS_SUCCEEDED if exit_code == 0 else LIVEACTION_STATUS_FAILED return (status, jsonify.json_loads(result, LocalShellRunner.KEYS_TO_TRANSFORM), None)