def test_get_sandbox_python_path(self, mock_get_python_lib): # No inheritence python_path = get_sandbox_python_path(inherit_from_parent=False, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':') # Inherit python path from current process # Mock the current process python path old_python_path = os.environ.get('PYTHONPATH', '') os.environ['PYTHONPATH'] = ':/data/test1:/data/test2' python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':/data/test1:/data/test2') # Inherit from current process and from virtualenv (not running inside virtualenv) old_real_prefix = sys.real_prefix del sys.real_prefix python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':/data/test1:/data/test2') # Inherit from current process and from virtualenv (running inside virtualenv) sys.real_prefix = '/usr' mock_get_python_lib.return_value = sys.prefix + '/virtualenvtest' python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) self.assertEqual(python_path, ':/data/test1:/data/test2:%s/virtualenvtest' % (sys.prefix)) os.environ['PYTHONPATH'] = old_python_path sys.real_prefix = old_real_prefix
def test_get_sandbox_python_path_for_python_action_python2_used_for_venv(self, mock_get_python_lib): # No inheritance python_path = get_sandbox_python_path_for_python_action(pack='dummy_pack', inherit_from_parent=False, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':') # Inherit python path from current process # Mock the current process python path os.environ['PYTHONPATH'] = ':/data/test1:/data/test2' python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':/data/test1:/data/test2') # Inherit from current process and from virtualenv (not running inside virtualenv) del sys.real_prefix python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':/data/test1:/data/test2') # Inherit from current process and from virtualenv (running inside virtualenv) sys.real_prefix = '/usr' mock_get_python_lib.return_value = sys.prefix + '/virtualenvtest' python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) self.assertEqual(python_path, ':/data/test1:/data/test2:%s/virtualenvtest' % (sys.prefix))
def test_get_sandbox_python_path(self, mock_get_python_lib): # No inheritance python_path = get_sandbox_python_path(inherit_from_parent=False, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':') # Inherit python path from current process # Mock the current process python path os.environ['PYTHONPATH'] = ':/data/test1:/data/test2' python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':/data/test1:/data/test2') # Inherit from current process and from virtualenv (not running inside virtualenv) del sys.real_prefix python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':/data/test1:/data/test2') # Inherit from current process and from virtualenv (running inside virtualenv) sys.real_prefix = '/usr' mock_get_python_lib.return_value = sys.prefix + '/virtualenvtest' python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) self.assertEqual(python_path, ':/data/test1:/data/test2:%s/virtualenvtest' % (sys.prefix))
def test_get_sandbox_python_path(self, mock_get_python_lib): # No inheritence python_path = get_sandbox_python_path(inherit_from_parent=False, inherit_parent_virtualenv=False) self.assertEqual(python_path, ":") # Inherit python path from current process # Mock the current process python path old_python_path = os.environ.get("PYTHONPATH", "") os.environ["PYTHONPATH"] = ":/data/test1:/data/test2" python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ":/data/test1:/data/test2") # Inherit from current process and from virtualenv (not running inside virtualenv) old_real_prefix = sys.real_prefix del sys.real_prefix python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ":/data/test1:/data/test2") # Inherit from current process and from virtualenv (running inside virtualenv) sys.real_prefix = "/usr" mock_get_python_lib.return_value = sys.prefix + "/virtualenvtest" python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) self.assertEqual(python_path, ":/data/test1:/data/test2:%s/virtualenvtest" % (sys.prefix)) os.environ["PYTHONPATH"] = old_python_path sys.real_prefix = old_real_prefix
def test_get_sandbox_python_path_for_python_action_inherit_from_parent_process_only( self, mock_get_python_lib): # Inherit python path from current process # Mock the current process python path with mock.patch.dict(os.environ, {"PYTHONPATH": ":/data/test1:/data/test2"}): python_path = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ":/data/test1:/data/test2") python_path = get_sandbox_python_path_for_python_action( pack="dummy_pack", inherit_from_parent=True, inherit_parent_virtualenv=False, ) actual_path = python_path.strip(":").split(":") self.assertEqual(len(actual_path), 6) # First entry should be lib/python3 dir from venv self.assertEndsWith(actual_path[0], "virtualenvs/dummy_pack/lib/python3.6") # Second entry should be python3 site-packages dir from venv self.assertEndsWith( actual_path[1], "virtualenvs/dummy_pack/lib/python3.6/site-packages") # Third entry should be actions/lib dir from pack root directory self.assertEndsWith(actual_path[2], "packs/dummy_pack/actions/lib") # And the rest of the paths from get_sandbox_python_path self.assertEqual(actual_path[3], "") self.assertEqual(actual_path[4], "/data/test1") self.assertEqual(actual_path[5], "/data/test2")
def test_get_sandbox_python_path_for_python_action_python3_used_for_venv( self, mock_get_python_lib): self.assertTrue(is_pack_virtualenv_using_python3(pack='dummy_pack')[0]) # No inheritance python_path = get_sandbox_python_path_for_python_action( pack='dummy_pack', inherit_from_parent=False, inherit_parent_virtualenv=False) split = python_path.strip(':').split(':') self.assertEqual(len(split), 3) # First entry should be lib/python3 dir from venv self.assertTrue('virtualenvs/dummy_pack/lib/python3.6' in split[0]) # Second entry should be python3 site-packages dir from venv self.assertTrue( 'virtualenvs/dummy_pack/lib/python3.6/site-packages' in split[1]) # Third entry should be actions/lib dir from pack root directory self.assertTrue('packs/dummy_pack/actions/lib/' in split[2]) # Inherit python path from current process # Mock the current process python path os.environ['PYTHONPATH'] = ':/data/test1:/data/test2' python_path = get_sandbox_python_path_for_python_action( pack='dummy_pack', inherit_from_parent=True, inherit_parent_virtualenv=False) expected = ( '/tmp/virtualenvs/dummy_pack/lib/python3.6:' '/tmp/virtualenvs/dummy_pack/lib/python3.6/site-packages:' '/tmp/packs/dummy_pack/actions/lib/::/data/test1:/data/test2') self.assertEqual(python_path, expected) # Inherit from current process and from virtualenv (not running inside virtualenv) del sys.real_prefix python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':/data/test1:/data/test2') # Inherit from current process and from virtualenv (running inside virtualenv) sys.real_prefix = '/usr' mock_get_python_lib.return_value = sys.prefix + '/virtualenvtest' python_path = get_sandbox_python_path_for_python_action( pack='dummy_pack', inherit_from_parent=True, inherit_parent_virtualenv=True) expected = ( '/tmp/virtualenvs/dummy_pack/lib/python3.6:' '/tmp/virtualenvs/dummy_pack/lib/python3.6/site-packages:' '/tmp/packs/dummy_pack/actions/lib/::/data/test1:/data/test2:' '%s/virtualenvtest' % (sys.prefix)) self.assertEqual(python_path, expected)
def test_get_sandbox_python_path_for_python_action_python3_used_for_venv(self, mock_get_python_lib): self.assertTrue(is_pack_virtualenv_using_python3(pack='dummy_pack')[0]) # No inheritance python_path = get_sandbox_python_path_for_python_action(pack='dummy_pack', inherit_from_parent=False, inherit_parent_virtualenv=False) split = python_path.strip(':').split(':') self.assertEqual(len(split), 3) # First entry should be lib/python3 dir from venv self.assertTrue('virtualenvs/dummy_pack/lib/python3.6' in split[0]) # Second entry should be python3 site-packages dir from venv self.assertTrue('virtualenvs/dummy_pack/lib/python3.6/site-packages' in split[1]) # Third entry should be actions/lib dir from pack root directory self.assertTrue('packs/dummy_pack/actions/lib/' in split[2]) # Inherit python path from current process # Mock the current process python path os.environ['PYTHONPATH'] = ':/data/test1:/data/test2' python_path = get_sandbox_python_path_for_python_action(pack='dummy_pack', inherit_from_parent=True, inherit_parent_virtualenv=False) expected = ('/tmp/virtualenvs/dummy_pack/lib/python3.6:' '/tmp/virtualenvs/dummy_pack/lib/python3.6/site-packages:' '/tmp/packs/dummy_pack/actions/lib/::/data/test1:/data/test2') self.assertEqual(python_path, expected) # Inherit from current process and from virtualenv (not running inside virtualenv) del sys.real_prefix python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ':/data/test1:/data/test2') # Inherit from current process and from virtualenv (running inside virtualenv) sys.real_prefix = '/usr' mock_get_python_lib.return_value = sys.prefix + '/virtualenvtest' python_path = get_sandbox_python_path_for_python_action(pack='dummy_pack', inherit_from_parent=True, inherit_parent_virtualenv=True) expected = ('/tmp/virtualenvs/dummy_pack/lib/python3.6:' '/tmp/virtualenvs/dummy_pack/lib/python3.6/site-packages:' '/tmp/packs/dummy_pack/actions/lib/::/data/test1:/data/test2:' '%s/virtualenvtest' % (sys.prefix)) self.assertEqual(python_path, expected)
def run(self, action_parameters): pack = self.get_pack_name() user = self.get_user() serialized_parameters = json.dumps( action_parameters) if action_parameters else '' virtualenv_path = get_sandbox_virtualenv_path(pack=pack) python_path = get_sandbox_python_binary_path(pack=pack) if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = {'pack': pack, 'virtualenv_path': virtualenv_path} msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values raise Exception(msg) if not self.entry_point: raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters), '--user=%s' % (user), '--parent-args=%s' % (json.dumps(sys.argv[1:])) ] # We need to ensure all the st2 dependencies are also available to the # subprocess env = os.environ.copy() env['PATH'] = get_sandbox_path(virtualenv_path=virtualenv_path) env['PYTHONPATH'] = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=True) # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) # Include common st2 environment variables st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) datastore_env_vars = self._get_datastore_access_env_vars() env.update(datastore_env_vars) exit_code, stdout, stderr, timed_out = run_command( cmd=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env, timeout=self._timeout) return self._get_output_values(exit_code, stdout, stderr, timed_out)
def test_get_sandbox_python_path(self, mock_get_python_lib): # No inheritance python_path = get_sandbox_python_path(inherit_from_parent=False, inherit_parent_virtualenv=False) self.assertEqual(python_path, ":") # Inherit python path from current process # Mock the current process python path with mock.patch.dict(os.environ, {"PYTHONPATH": ":/data/test1:/data/test2"}): python_path = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ":/data/test1:/data/test2") # Inherit from current process and from virtualenv (not running inside virtualenv) clear_virtualenv_prefix() with mock.patch.dict(os.environ, {"PYTHONPATH": ":/data/test1:/data/test2"}): python_path = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ":/data/test1:/data/test2") # Inherit from current process and from virtualenv (running inside virtualenv) sys.real_prefix = "/usr" mock_get_python_lib.return_value = f"{sys.prefix}/virtualenvtest" with mock.patch.dict(os.environ, {"PYTHONPATH": ":/data/test1:/data/test2"}): python_path = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=True) self.assertEqual( python_path, f":/data/test1:/data/test2:{sys.prefix}/virtualenvtest")
def run(self, action_parameters): pack = self.get_pack_name() user = self.get_user() serialized_parameters = json.dumps(action_parameters) if action_parameters else '' virtualenv_path = get_sandbox_virtualenv_path(pack=pack) python_path = get_sandbox_python_binary_path(pack=pack) if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = {'pack': pack, 'virtualenv_path': virtualenv_path} msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values raise Exception(msg) if not self.entry_point: raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters), '--user=%s' % (user), '--parent-args=%s' % (json.dumps(sys.argv[1:])) ] # We need to ensure all the st2 dependencies are also available to the # subprocess env = os.environ.copy() env['PATH'] = get_sandbox_path(virtualenv_path=virtualenv_path) env['PYTHONPATH'] = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) # Include common st2 environment variables st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) datastore_env_vars = self._get_datastore_access_env_vars() env.update(datastore_env_vars) exit_code, stdout, stderr, timed_out = run_command(cmd=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env, timeout=self._timeout) return self._get_output_values(exit_code, stdout, stderr, timed_out)
def _spawn_sensor_process(self, sensor): """ Spawn a new process for the provided sensor. New process uses isolated Python binary from a virtual environment belonging to the sensor pack. """ sensor_id = self._get_sensor_id(sensor=sensor) python_path = get_sandbox_python_binary_path(pack=sensor['pack']) trigger_type_refs = sensor['trigger_types'] or [] trigger_type_refs = ','.join(trigger_type_refs) parent_args = json.dumps(sys.argv[1:]) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (sensor['pack']), '--file-path=%s' % (sensor['file_path']), '--class-name=%s' % (sensor['class_name']), '--trigger-type-refs=%s' % (trigger_type_refs), '--parent-args=%s' % (parent_args) ] if sensor['poll_interval']: args.append('--poll-interval=%s' % (sensor['poll_interval'])) env = os.environ.copy() env['PYTHONPATH'] = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) LOG.debug('Running sensor subprocess (cmd="%s")', ' '.join(args)) # TODO: Intercept stdout and stderr for aggregated logging purposes try: process = subprocess.Popen(args=args, stdin=None, stdout=None, stderr=None, shell=False, env=env) except Exception as e: cmd = ' '.join(args) message = ('Failed to spawn process for sensor %s ("%s"): %s' % (sensor_id, cmd, str(e))) raise Exception(message) self._processes[sensor_id] = process return process
def _spawn_sensor_process(self, sensor): """ Spawn a new process for the provided sensor. New process uses isolated Python binary from a virtual environment belonging to the sensor pack. """ sensor_id = self._get_sensor_id(sensor=sensor) virtualenv_path = get_sandbox_virtualenv_path(pack=sensor['pack']) python_path = get_sandbox_python_binary_path(pack=sensor['pack']) if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = {'pack': sensor['pack'], 'virtualenv_path': virtualenv_path} msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values raise Exception(msg) trigger_type_refs = sensor['trigger_types'] or [] trigger_type_refs = ','.join(trigger_type_refs) parent_args = json.dumps(sys.argv[1:]) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (sensor['pack']), '--file-path=%s' % (sensor['file_path']), '--class-name=%s' % (sensor['class_name']), '--trigger-type-refs=%s' % (trigger_type_refs), '--parent-args=%s' % (parent_args) ] if sensor['poll_interval']: args.append('--poll-interval=%s' % (sensor['poll_interval'])) env = os.environ.copy() env['PYTHONPATH'] = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) # Include full api URL and API token specific to that sensor ttl = (24 * 60 * 60) temporary_token = create_token(username='******', ttl=ttl) env[API_URL_ENV_VARIABLE_NAME] = get_full_public_api_url() env[AUTH_TOKEN_ENV_VARIABLE_NAME] = temporary_token.token # TODO 1: Purge temporary token when service stops or sensor process dies # TODO 2: Store metadata (wrapper process id) with the token and delete # tokens for old, dead processes on startup cmd = ' '.join(args) LOG.debug('Running sensor subprocess (cmd="%s")', cmd) # TODO: Intercept stdout and stderr for aggregated logging purposes try: process = subprocess.Popen(args=args, stdin=None, stdout=None, stderr=None, shell=False, env=env, preexec_fn=on_parent_exit('SIGTERM')) except Exception as e: cmd = ' '.join(args) message = ('Failed to spawn process for sensor %s ("%s"): %s' % (sensor_id, cmd, str(e))) raise Exception(message) self._processes[sensor_id] = process self._sensors[sensor_id] = sensor self._sensor_start_times[sensor_id] = int(time.time()) self._dispatch_trigger_for_sensor_spawn(sensor=sensor, process=process, cmd=cmd) return process
def run(self, action_parameters): pack = self.action.pack if self.action else DEFAULT_PACK_NAME serialized_parameters = json.dumps(action_parameters) if action_parameters else '' virtualenv_path = get_sandbox_virtualenv_path(pack=pack) python_path = get_sandbox_python_binary_path(pack=pack) if virtualenv_path and not os.path.isdir(virtualenv_path): msg = PACK_VIRTUALENV_DOESNT_EXIST % (pack, pack) raise Exception(msg) if not self.entry_point: raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters), '--parent-args=%s' % (json.dumps(sys.argv[1:])) ] # We need to ensure all the st2 dependencies are also available to the # subprocess env = os.environ.copy() env['PYTHONPATH'] = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) exit_code, stdout, stderr, timed_out = run_command(cmd=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env, timeout=self._timeout) if timed_out: error = 'Action failed to complete in %s seconds' % (self._timeout) else: error = None if ACTION_OUTPUT_RESULT_DELIMITER in stdout: split = stdout.split(ACTION_OUTPUT_RESULT_DELIMITER) assert len(split) == 3 result = split[1].strip() stdout = split[0] + split[2] else: result = None try: result = json.loads(result) except: pass output = { 'stdout': stdout, 'stderr': stderr, 'exit_code': exit_code, 'result': result } if error: output['error'] = error status = LIVEACTION_STATUS_SUCCEEDED if exit_code == 0 else LIVEACTION_STATUS_FAILED self._log_action_completion(logger=LOG, result=output, status=status, exit_code=exit_code) return (status, output, None)
def _spawn_sensor_process(self, sensor): """ Spawn a new process for the provided sensor. New process uses isolated Python binary from a virtual environment belonging to the sensor pack. """ sensor_id = self._get_sensor_id(sensor=sensor) virtualenv_path = get_sandbox_virtualenv_path(pack=sensor['pack']) python_path = get_sandbox_python_binary_path(pack=sensor['pack']) if virtualenv_path and not os.path.isdir(virtualenv_path): msg = PACK_VIRTUALENV_DOESNT_EXIST % (sensor['pack'], sensor['pack']) raise Exception(msg) trigger_type_refs = sensor['trigger_types'] or [] trigger_type_refs = ','.join(trigger_type_refs) parent_args = json.dumps(sys.argv[1:]) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (sensor['pack']), '--file-path=%s' % (sensor['file_path']), '--class-name=%s' % (sensor['class_name']), '--trigger-type-refs=%s' % (trigger_type_refs), '--parent-args=%s' % (parent_args) ] if sensor['poll_interval']: args.append('--poll-interval=%s' % (sensor['poll_interval'])) env = os.environ.copy() env['PYTHONPATH'] = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=True) # Include full api URL and API token specific to that sensor ttl = (24 * 60 * 60) temporary_token = create_token(username='******', ttl=ttl) env[API_URL_ENV_VARIABLE_NAME] = get_full_api_url() env[AUTH_TOKEN_ENV_VARIABLE_NAME] = temporary_token.token # TODO 1: Purge temporary token when service stops or sensor process dies # TODO 2: Store metadata (wrapper process id) with the token and delete # tokens for old, dead processes on startup LOG.debug('Running sensor subprocess (cmd="%s")', ' '.join(args)) # TODO: Intercept stdout and stderr for aggregated logging purposes try: process = subprocess.Popen(args=args, stdin=None, stdout=None, stderr=None, shell=False, env=env) except Exception as e: cmd = ' '.join(args) message = ('Failed to spawn process for sensor %s ("%s"): %s' % (sensor_id, cmd, str(e))) raise Exception(message) self._processes[sensor_id] = process return process
def run(self, action_parameters): pack = self.action.pack if self.action else DEFAULT_PACK_NAME serialized_parameters = json.dumps( action_parameters) if action_parameters else '' virtualenv_path = get_sandbox_virtualenv_path(pack=pack) python_path = get_sandbox_python_binary_path(pack=pack) if virtualenv_path and not os.path.isdir(virtualenv_path): msg = PACK_VIRTUALENV_DOESNT_EXIST % (pack, pack) raise Exception(msg) if not self.entry_point: raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters), '--parent-args=%s' % (json.dumps(sys.argv[1:])) ] # We need to ensure all the st2 dependencies are also available to the # subprocess env = os.environ.copy() env['PYTHONPATH'] = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=True) # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) # Note: We are using eventlet friendly implementation of subprocess # which uses GreenPipe so it doesn't block process = subprocess.Popen(args=args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env) try: exit_code = process.wait(timeout=self._timeout) except subprocess.TimeoutExpired: # Action has timed out, kill the process and propagate the error # Note: process.kill() will set the returncode to -9 so we don't # need to explicitly set it to some non-zero value process.kill() error = 'Action failed to complete in %s seconds' % (self._timeout) else: error = None stdout, stderr = process.communicate() exit_code = process.returncode if ACTION_OUTPUT_RESULT_DELIMITER in stdout: split = stdout.split(ACTION_OUTPUT_RESULT_DELIMITER) assert len(split) == 3 result = split[1].strip() stdout = split[0] + split[2] else: result = None try: result = json.loads(result) except: pass output = { 'stdout': stdout, 'stderr': stderr, 'exit_code': exit_code, 'result': result } if error: output['error'] = error status = LIVEACTION_STATUS_SUCCEEDED if exit_code == 0 else LIVEACTION_STATUS_FAILED LOG.debug('Action output : %s. exit_code : %s. status : %s', str(output), exit_code, status) return (status, output, None)
def run(self, action_parameters): LOG.debug('Running pythonrunner.') LOG.debug('Getting pack name.') pack = self.get_pack_name() LOG.debug('Getting user.') user = self.get_user() LOG.debug('Serializing parameters.') serialized_parameters = json.dumps(action_parameters) if action_parameters else '' LOG.debug('Getting virtualenv_path.') virtualenv_path = get_sandbox_virtualenv_path(pack=pack) LOG.debug('Getting python path.') python_path = get_sandbox_python_binary_path(pack=pack) LOG.debug('Checking virtualenv path.') if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = {'pack': pack, 'virtualenv_path': virtualenv_path} msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values LOG.error('virtualenv_path set but not a directory: %s', msg) raise Exception(msg) LOG.debug('Checking entry_point.') if not self.entry_point: LOG.error('Action "%s" is missing entry_point attribute' % (self.action.name)) raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) LOG.debug('Setting args.') args = [ python_path, '-u', WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters), '--user=%s' % (user), '--parent-args=%s' % (json.dumps(sys.argv[1:])) ] # We need to ensure all the st2 dependencies are also available to the # subprocess LOG.debug('Setting env.') env = os.environ.copy() env['PATH'] = get_sandbox_path(virtualenv_path=virtualenv_path) env['PYTHONPATH'] = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) # Include common st2 environment variables st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) datastore_env_vars = self._get_datastore_access_env_vars() env.update(datastore_env_vars) 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) command_string = list2cmdline(args) LOG.debug('Running command: PATH=%s PYTHONPATH=%s %s' % (env['PATH'], env['PYTHONPATH'], command_string)) exit_code, stdout, stderr, timed_out = run_command(cmd=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env, timeout=self._timeout, read_stdout_func=read_and_store_stdout, read_stderr_func=read_and_store_stderr, read_stdout_buffer=stdout, read_stderr_buffer=stderr) LOG.debug('Returning values: %s, %s, %s, %s' % (exit_code, stdout, stderr, timed_out)) LOG.debug('Returning.') return self._get_output_values(exit_code, stdout, stderr, timed_out)
def run(self, action_parameters): LOG.debug('Running pythonrunner.') LOG.debug('Getting pack name.') pack = self.get_pack_ref() LOG.debug('Getting user.') user = self.get_user() LOG.debug('Serializing parameters.') serialized_parameters = json.dumps(action_parameters if action_parameters else {}) LOG.debug('Getting virtualenv_path.') virtualenv_path = get_sandbox_virtualenv_path(pack=pack) LOG.debug('Getting python path.') if self._sandbox: python_path = get_sandbox_python_binary_path(pack=pack) else: python_path = sys.executable LOG.debug('Checking virtualenv path.') if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = {'pack': pack, 'virtualenv_path': virtualenv_path} msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values LOG.error('virtualenv_path set but not a directory: %s', msg) raise Exception(msg) LOG.debug('Checking entry_point.') if not self.entry_point: LOG.error('Action "%s" is missing entry_point attribute' % (self.action.name)) raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) # Note: We pass config as command line args so the actual wrapper process is standalone # and doesn't need access to db LOG.debug('Setting args.') if self._use_parent_args: parent_args = json.dumps(sys.argv[1:]) else: parent_args = json.dumps([]) args = [ python_path, '-u', # unbuffered mode so streaming mode works as expected WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--user=%s' % (user), '--parent-args=%s' % (parent_args), ] # If parameter size is larger than the maximum allowed by Linux kernel # we need to swap to stdin to communicate parameters. This avoids a # failure to fork the wrapper process when using large parameters. stdin = None stdin_params = None if len(serialized_parameters) >= MAX_PARAM_LENGTH: stdin = subprocess.PIPE LOG.debug('Parameters are too big...changing to stdin') stdin_params = '{"parameters": %s}\n' % (serialized_parameters) args.append('--stdin-parameters') else: LOG.debug('Parameters are just right...adding them to arguments') args.append('--parameters=%s' % (serialized_parameters)) if self._config: args.append('--config=%s' % (json.dumps(self._config))) if self._log_level != PYTHON_RUNNER_DEFAULT_LOG_LEVEL: # We only pass --log-level parameter if non default log level value is specified args.append('--log-level=%s' % (self._log_level)) # We need to ensure all the st2 dependencies are also available to the # subprocess LOG.debug('Setting env.') env = os.environ.copy() env['PATH'] = get_sandbox_path(virtualenv_path=virtualenv_path) sandbox_python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) if self._enable_common_pack_libs: try: pack_common_libs_path = self._get_pack_common_libs_path(pack_ref=pack) except Exception as e: LOG.debug('Failed to retrieve pack common lib path: %s' % (str(e))) print(e) # There is no MongoDB connection available in Lambda and pack common lib # functionality is not also mandatory for Lambda so we simply ignore those errors. # Note: We should eventually refactor this code to make runner standalone and not # depend on a db connection (as it was in the past) - this param should be passed # to the runner by the action runner container pack_common_libs_path = None else: pack_common_libs_path = None # Remove leading : (if any) if sandbox_python_path.startswith(':'): sandbox_python_path = sandbox_python_path[1:] if self._enable_common_pack_libs and pack_common_libs_path: env['PYTHONPATH'] = pack_common_libs_path + ':' + sandbox_python_path else: env['PYTHONPATH'] = sandbox_python_path # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) # Include common st2 environment variables st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) datastore_env_vars = self._get_datastore_access_env_vars() env.update(datastore_env_vars) 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) command_string = list2cmdline(args) if stdin_params: command_string = 'echo %s | %s' % (quote_unix(stdin_params), command_string) LOG.debug('Running command: PATH=%s PYTHONPATH=%s %s' % (env['PATH'], env['PYTHONPATH'], command_string)) exit_code, stdout, stderr, timed_out = run_command(cmd=args, stdin=stdin, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env, timeout=self._timeout, read_stdout_func=read_and_store_stdout, read_stderr_func=read_and_store_stderr, read_stdout_buffer=stdout, read_stderr_buffer=stderr, stdin_value=stdin_params) LOG.debug('Returning values: %s, %s, %s, %s', exit_code, stdout, stderr, timed_out) LOG.debug('Returning.') return self._get_output_values(exit_code, stdout, stderr, timed_out)
def run(self, action_parameters): LOG.debug('Running pythonrunner.') LOG.debug('Getting pack name.') pack = self.get_pack_name() LOG.debug('Getting user.') user = self.get_user() LOG.debug('Serializing parameters.') serialized_parameters = json.dumps(action_parameters) if action_parameters else '' LOG.debug('Getting virtualenv_path.') virtualenv_path = get_sandbox_virtualenv_path(pack=pack) LOG.debug('Getting python path.') python_path = get_sandbox_python_binary_path(pack=pack) LOG.debug('Checking virtualenv path.') if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = {'pack': pack, 'virtualenv_path': virtualenv_path} msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values LOG.error('virtualenv_path set but not a directory: %s', msg) raise Exception(msg) LOG.debug('Checking entry_point.') if not self.entry_point: LOG.error('Action "%s" is missing entry_point attribute' % (self.action.name)) raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) LOG.debug('Setting args.') args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters), '--user=%s' % (user), '--parent-args=%s' % (json.dumps(sys.argv[1:])) ] # We need to ensure all the st2 dependencies are also available to the # subprocess LOG.debug('Setting env.') env = os.environ.copy() env['PATH'] = get_sandbox_path(virtualenv_path=virtualenv_path) env['PYTHONPATH'] = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) # Include common st2 environment variables st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) datastore_env_vars = self._get_datastore_access_env_vars() env.update(datastore_env_vars) command_string = list2cmdline(args) LOG.debug('Running command: PATH=%s PYTHONPATH=%s %s' % (env['PATH'], env['PYTHONPATH'], command_string)) exit_code, stdout, stderr, timed_out = run_command(cmd=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env, timeout=self._timeout) LOG.debug('Returning values: %s, %s, %s, %s' % (exit_code, stdout, stderr, timed_out)) LOG.debug('Returning.') return self._get_output_values(exit_code, stdout, stderr, timed_out)
def test_get_sandbox_python_path_for_python_action_inherit_from_parent_process_and_venv( self, mock_get_python_lib): # Inherit from current process and from virtualenv (not running inside virtualenv) clear_virtualenv_prefix() # Inherit python path from current process # Mock the current process python path with mock.patch.dict(os.environ, {"PYTHONPATH": ":/data/test1:/data/test2"}): python_path = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=False) self.assertEqual(python_path, ":/data/test1:/data/test2") python_path = get_sandbox_python_path_for_python_action( pack="dummy_pack", inherit_from_parent=True, inherit_parent_virtualenv=True, ) actual_path = python_path.strip(":").split(":") self.assertEqual(len(actual_path), 6) # First entry should be lib/python3 dir from venv self.assertEndsWith(actual_path[0], "virtualenvs/dummy_pack/lib/python3.6") # Second entry should be python3 site-packages dir from venv self.assertEndsWith( actual_path[1], "virtualenvs/dummy_pack/lib/python3.6/site-packages") # Third entry should be actions/lib dir from pack root directory self.assertEndsWith(actual_path[2], "packs/dummy_pack/actions/lib") # And the rest of the paths from get_sandbox_python_path self.assertEqual(actual_path[3], "") self.assertEqual(actual_path[4], "/data/test1") self.assertEqual(actual_path[5], "/data/test2") # Inherit from current process and from virtualenv (running inside virtualenv) sys.real_prefix = "/usr" mock_get_python_lib.return_value = f"{sys.prefix}/virtualenvtest" # Inherit python path from current process # Mock the current process python path with mock.patch.dict(os.environ, {"PYTHONPATH": ":/data/test1:/data/test2"}): python_path = get_sandbox_python_path_for_python_action( pack="dummy_pack", inherit_from_parent=True, inherit_parent_virtualenv=True, ) actual_path = python_path.strip(":").split(":") self.assertEqual(len(actual_path), 7) # First entry should be lib/python3 dir from venv self.assertEndsWith(actual_path[0], "virtualenvs/dummy_pack/lib/python3.6") # Second entry should be python3 site-packages dir from venv self.assertEndsWith( actual_path[1], "virtualenvs/dummy_pack/lib/python3.6/site-packages") # Third entry should be actions/lib dir from pack root directory self.assertEndsWith(actual_path[2], "packs/dummy_pack/actions/lib") # The paths from get_sandbox_python_path self.assertEqual(actual_path[3], "") self.assertEqual(actual_path[4], "/data/test1") self.assertEqual(actual_path[5], "/data/test2") # And the parent virtualenv self.assertEqual(actual_path[6], f"{sys.prefix}/virtualenvtest")
def run(self, action_parameters): pack = self.action.pack if self.action else DEFAULT_PACK_NAME serialized_parameters = json.dumps(action_parameters) if action_parameters else '' python_path = get_sandbox_python_binary_path(pack=pack) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters) ] # We need to ensure all the st2 dependencies are also available to the # subprocess env = os.environ.copy() env['PYTHONPATH'] = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) # Note: We are using eventlet friendly implementation of subprocess # which uses GreenPipe so it doesn't block process = subprocess.Popen(args=args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env) try: exit_code = process.wait(timeout=self._timeout) except subprocess.TimeoutExpired: # Action has timed out, kill the process and propagate the error # Note: process.kill() will set the returncode to -9 so we don't # need to explicitly set it to some non-zero value process.kill() error = 'Action failed to complete in %s seconds' % (self._timeout) else: error = None stdout, stderr = process.communicate() exit_code = process.returncode if ACTION_OUTPUT_RESULT_DELIMITER in stdout: split = stdout.split(ACTION_OUTPUT_RESULT_DELIMITER) assert len(split) == 3 result = split[1].strip() stdout = split[0] = split[2] else: result = None output = { 'stdout': stdout, 'stderr': stderr, 'exit_code': exit_code, 'result': result } if error: output['error'] = error output = json.dumps(output) status = ACTIONEXEC_STATUS_SUCCEEDED if exit_code == 0 else ACTIONEXEC_STATUS_FAILED self.container_service.report_result(output) self.container_service.report_status(status) LOG.info('Action output : %s. exit_code : %s. status : %s', str(output), exit_code, status) return output is not None
def run(self, action_parameters): pack = self.get_pack_name() serialized_parameters = json.dumps(action_parameters) if action_parameters else '' virtualenv_path = get_sandbox_virtualenv_path(pack=pack) python_path = get_sandbox_python_binary_path(pack=pack) if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = {'pack': pack, 'virtualenv_path': virtualenv_path} msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values raise Exception(msg) if not self.entry_point: raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters), '--parent-args=%s' % (json.dumps(sys.argv[1:])) ] # We need to ensure all the st2 dependencies are also available to the # subprocess env = os.environ.copy() env['PATH'] = get_sandbox_path(virtualenv_path=virtualenv_path) env['PYTHONPATH'] = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) # Include common st2 environment variables st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) exit_code, stdout, stderr, timed_out = run_command(cmd=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env, timeout=self._timeout) if timed_out: error = 'Action failed to complete in %s seconds' % (self._timeout) else: error = None if ACTION_OUTPUT_RESULT_DELIMITER in stdout: split = stdout.split(ACTION_OUTPUT_RESULT_DELIMITER) assert len(split) == 3 result = split[1].strip() stdout = split[0] + split[2] else: result = None try: result = json.loads(result) except: pass output = { 'stdout': stdout, 'stderr': stderr, 'exit_code': exit_code, 'result': result } if error: output['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, output, None)
def run(self, action_parameters): LOG.debug('Running pythonrunner.') LOG.debug('Getting pack name.') pack = self.get_pack_ref() LOG.debug('Getting user.') user = self.get_user() LOG.debug('Serializing parameters.') serialized_parameters = json.dumps( action_parameters) if action_parameters else '' LOG.debug('Getting virtualenv_path.') virtualenv_path = get_sandbox_virtualenv_path(pack=pack) LOG.debug('Getting python path.') if self._sandbox: python_path = get_sandbox_python_binary_path(pack=pack) else: python_path = sys.executable LOG.debug('Checking virtualenv path.') if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = {'pack': pack, 'virtualenv_path': virtualenv_path} msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values LOG.error('virtualenv_path set but not a directory: %s', msg) raise Exception(msg) LOG.debug('Checking entry_point.') if not self.entry_point: LOG.error('Action "%s" is missing entry_point attribute' % (self.action.name)) raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) # Note: We pass config as command line args so the actual wrapper process is standalone # and doesn't need access to db LOG.debug('Setting args.') args = [ python_path, '-u', # unbuffered mode so streaming mode works as expected WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters), '--user=%s' % (user), '--parent-args=%s' % (json.dumps(sys.argv[1:])), ] if self._config: args.append('--config=%s' % (json.dumps(self._config))) if self._log_level != 'debug': # We only pass --log-level parameter if non default log level value is specified args.append('--log-level=%s' % (self._log_level)) # We need to ensure all the st2 dependencies are also available to the # subprocess LOG.debug('Setting env.') env = os.environ.copy() env['PATH'] = get_sandbox_path(virtualenv_path=virtualenv_path) sandbox_python_path = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=True) if self._enable_common_pack_libs: try: pack_common_libs_path = get_pack_common_libs_path_for_pack_ref( pack_ref=pack) except Exception: # There is no MongoDB connection available in Lambda and pack common lib # functionality is not also mandatory for Lambda so we simply ignore those errors. # Note: We should eventually refactor this code to make runner standalone and not # depend on a db connection (as it was in the past) - this param should be passed # to the runner by the action runner container pack_common_libs_path = None else: pack_common_libs_path = None if self._enable_common_pack_libs and pack_common_libs_path: env['PYTHONPATH'] = pack_common_libs_path + ':' + sandbox_python_path else: env['PYTHONPATH'] = sandbox_python_path # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) # Include common st2 environment variables st2_env_vars = self._get_common_action_env_variables() env.update(st2_env_vars) datastore_env_vars = self._get_datastore_access_env_vars() env.update(datastore_env_vars) 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) command_string = list2cmdline(args) LOG.debug('Running command: PATH=%s PYTHONPATH=%s %s' % (env['PATH'], env['PYTHONPATH'], command_string)) exit_code, stdout, stderr, timed_out = run_command( cmd=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env, timeout=self._timeout, read_stdout_func=read_and_store_stdout, read_stderr_func=read_and_store_stderr, read_stdout_buffer=stdout, read_stderr_buffer=stderr) LOG.debug('Returning values: %s, %s, %s, %s' % (exit_code, stdout, stderr, timed_out)) LOG.debug('Returning.') return self._get_output_values(exit_code, stdout, stderr, timed_out)
def _spawn_sensor_process(self, sensor): """ Spawn a new process for the provided sensor. New process uses isolated Python binary from a virtual environment belonging to the sensor pack. """ sensor_id = self._get_sensor_id(sensor=sensor) pack_ref = sensor["pack"] virtualenv_path = get_sandbox_virtualenv_path(pack=pack_ref) python_path = get_sandbox_python_binary_path(pack=pack_ref) if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = { "pack": sensor["pack"], "virtualenv_path": virtualenv_path } msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values raise Exception(msg) args = self._get_args_for_wrapper_script(python_binary=python_path, sensor=sensor) if self._enable_common_pack_libs: pack_common_libs_path = get_pack_common_libs_path_for_pack_ref( pack_ref=pack_ref) else: pack_common_libs_path = None env = os.environ.copy() sandbox_python_path = get_sandbox_python_path( inherit_from_parent=True, inherit_parent_virtualenv=True) if self._enable_common_pack_libs and pack_common_libs_path: env["PYTHONPATH"] = pack_common_libs_path + ":" + sandbox_python_path else: env["PYTHONPATH"] = sandbox_python_path if self._create_token: # Include full api URL and API token specific to that sensor LOG.debug("Creating temporary auth token for sensor %s" % (sensor["class_name"])) ttl = cfg.CONF.auth.service_token_ttl metadata = { "service": "sensors_container", "sensor_path": sensor["file_path"], "sensor_class": sensor["class_name"], } temporary_token = create_token(username="******", ttl=ttl, metadata=metadata, service=True) env[API_URL_ENV_VARIABLE_NAME] = get_full_public_api_url() env[AUTH_TOKEN_ENV_VARIABLE_NAME] = temporary_token.token # TODO 1: Purge temporary token when service stops or sensor process dies # TODO 2: Store metadata (wrapper process id) with the token and delete # tokens for old, dead processes on startup cmd = " ".join(args) LOG.debug('Running sensor subprocess (cmd="%s")', cmd) # TODO: Intercept stdout and stderr for aggregated logging purposes try: process = subprocess.Popen( args=args, stdin=None, stdout=None, stderr=None, shell=False, env=env, preexec_fn=on_parent_exit("SIGTERM"), ) except Exception as e: cmd = " ".join(args) message = 'Failed to spawn process for sensor %s ("%s"): %s' % ( sensor_id, cmd, six.text_type(e), ) raise Exception(message) self._processes[sensor_id] = process self._sensors[sensor_id] = sensor self._sensor_start_times[sensor_id] = int(time.time()) self._dispatch_trigger_for_sensor_spawn(sensor=sensor, process=process, cmd=cmd) return process
def _spawn_sensor_process(self, sensor): """ Spawn a new process for the provided sensor. New process uses isolated Python binary from a virtual environment belonging to the sensor pack. """ sensor_id = self._get_sensor_id(sensor=sensor) pack_ref = sensor['pack'] virtualenv_path = get_sandbox_virtualenv_path(pack=pack_ref) python_path = get_sandbox_python_binary_path(pack=pack_ref) if virtualenv_path and not os.path.isdir(virtualenv_path): format_values = {'pack': sensor['pack'], 'virtualenv_path': virtualenv_path} msg = PACK_VIRTUALENV_DOESNT_EXIST % format_values raise Exception(msg) trigger_type_refs = sensor['trigger_types'] or [] trigger_type_refs = ','.join(trigger_type_refs) parent_args = json.dumps(sys.argv[1:]) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (sensor['pack']), '--file-path=%s' % (sensor['file_path']), '--class-name=%s' % (sensor['class_name']), '--trigger-type-refs=%s' % (trigger_type_refs), '--parent-args=%s' % (parent_args) ] if sensor['poll_interval']: args.append('--poll-interval=%s' % (sensor['poll_interval'])) sandbox_python_path = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) if self._enable_common_pack_libs: pack_common_libs_path = get_pack_common_libs_path_for_pack_ref(pack_ref=pack_ref) else: pack_common_libs_path = None env = os.environ.copy() if self._enable_common_pack_libs and pack_common_libs_path: env['PYTHONPATH'] = pack_common_libs_path + ':' + sandbox_python_path else: env['PYTHONPATH'] = sandbox_python_path # Include full api URL and API token specific to that sensor ttl = cfg.CONF.auth.service_token_ttl metadata = { 'service': 'sensors_container', 'sensor_path': sensor['file_path'], 'sensor_class': sensor['class_name'] } temporary_token = create_token(username='******', ttl=ttl, metadata=metadata, service=True) env[API_URL_ENV_VARIABLE_NAME] = get_full_public_api_url() env[AUTH_TOKEN_ENV_VARIABLE_NAME] = temporary_token.token # TODO 1: Purge temporary token when service stops or sensor process dies # TODO 2: Store metadata (wrapper process id) with the token and delete # tokens for old, dead processes on startup cmd = ' '.join(args) LOG.debug('Running sensor subprocess (cmd="%s")', cmd) # TODO: Intercept stdout and stderr for aggregated logging purposes try: process = subprocess.Popen(args=args, stdin=None, stdout=None, stderr=None, shell=False, env=env, preexec_fn=on_parent_exit('SIGTERM')) except Exception as e: cmd = ' '.join(args) message = ('Failed to spawn process for sensor %s ("%s"): %s' % (sensor_id, cmd, str(e))) raise Exception(message) self._processes[sensor_id] = process self._sensors[sensor_id] = sensor self._sensor_start_times[sensor_id] = int(time.time()) self._dispatch_trigger_for_sensor_spawn(sensor=sensor, process=process, cmd=cmd) return process
def run(self, action_parameters): pack = self.action.pack if self.action else DEFAULT_PACK_NAME serialized_parameters = json.dumps(action_parameters) if action_parameters else '' virtualenv_path = get_sandbox_virtualenv_path(pack=pack) python_path = get_sandbox_python_binary_path(pack=pack) if virtualenv_path and not os.path.isdir(virtualenv_path): msg = PACK_VIRTUALENV_DOESNT_EXIST % (pack, pack) raise Exception(msg) if not self.entry_point: raise Exception('Action "%s" is missing entry_point attribute' % (self.action.name)) args = [ python_path, WRAPPER_SCRIPT_PATH, '--pack=%s' % (pack), '--file-path=%s' % (self.entry_point), '--parameters=%s' % (serialized_parameters), '--parent-args=%s' % (json.dumps(sys.argv[1:])) ] # We need to ensure all the st2 dependencies are also available to the # subprocess env = os.environ.copy() env['PYTHONPATH'] = get_sandbox_python_path(inherit_from_parent=True, inherit_parent_virtualenv=True) # Include user provided environment variables (if any) user_env_vars = self._get_env_vars() env.update(user_env_vars) # Note: We are using eventlet friendly implementation of subprocess # which uses GreenPipe so it doesn't block process = subprocess.Popen(args=args, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env=env) try: exit_code = process.wait(timeout=self._timeout) except subprocess.TimeoutExpired: # Action has timed out, kill the process and propagate the error # Note: process.kill() will set the returncode to -9 so we don't # need to explicitly set it to some non-zero value process.kill() error = 'Action failed to complete in %s seconds' % (self._timeout) else: error = None stdout, stderr = process.communicate() exit_code = process.returncode if ACTION_OUTPUT_RESULT_DELIMITER in stdout: split = stdout.split(ACTION_OUTPUT_RESULT_DELIMITER) assert len(split) == 3 result = split[1].strip() stdout = split[0] + split[2] else: result = None try: result = json.loads(result) except: pass output = { 'stdout': stdout, 'stderr': stderr, 'exit_code': exit_code, 'result': result } if error: output['error'] = error status = LIVEACTION_STATUS_SUCCEEDED if exit_code == 0 else LIVEACTION_STATUS_FAILED LOG.debug('Action output : %s. exit_code : %s. status : %s', str(output), exit_code, status) return (status, output, None)