class BaseTester: """Base tester API.""" __artifact_id = None __gql_response = None def __init__(self, params): """Initialize. :param params: Parameter data storage for Tester APIs. :type params: :obj:`lib.params.Params` """ self.params = params self.etos = ETOS("ETOS API", os.getenv("HOSTNAME"), "ETOS API") self.etos.config.rabbitmq_publisher_from_environment() self.etos.start_publisher() def configure_environment_provider(self, suite_id): """Configure the ETOS environment provider for a suite ID. :param suite_id: Suite ID to configure the environment provider for. :type suite_id: str """ params = { "suite_id": suite_id, "iut_provider": self.params.iut_provider, "execution_space_provider": self.params.execution_space_provider, "log_area_provider": self.params.log_area_provider, "dataset": self.params.dataset, } generator = self.etos.http.retry( "POST", "{}/configure".format(self.etos.debug.environment_provider), as_json=False, json=params, ) for response in generator: print(response) break else: raise Exception( "Could not configure the Environment Provider with %r" % params) def handle(self): # pylint: disable=duplicate-code """Handle this tester.""" raise NotImplementedError @property def gql_response(self): """GQL response for artifact created and published events.""" if self.__gql_response is None: request = "search: \"{{'data.identity': {{'$regex': '{}'}}}}\"".format( self.params.artifact_identity) wait_generator = self.etos.utils.wait(self.etos.graphql.execute, query=ARTIFACT_REQUEST % request) for response in wait_generator: self.__gql_response = response break return self.__gql_response @property def artifact_id(self): """Figure out Artifact event ID from event storage.""" if self.__artifact_id is None: try: artifact_node = self.gql_response["artifactCreated"]["edges"][ 0]["node"] self.__artifact_id = artifact_node["meta"]["id"] except (KeyError, IndexError): pass return self.__artifact_id
class ETR: """ETOS Test Runner.""" context = None def __init__(self): """Initialize ETOS library and start eiffel publisher.""" self.etos = ETOS("ETOS Test Runner", os.getenv("HOSTNAME"), "ETOS Test Runner") self.etos.config.rabbitmq_publisher_from_environment() # ETR will print the entire environment just before executing. # Hide the password. os.environ["RABBITMQ_PASSWORD"] = "******" self.etos.start_publisher() self.tests_url = os.getenv("SUB_SUITE_URL") signal.signal(signal.SIGTERM, self.graceful_shutdown) @staticmethod def graceful_shutdown(*args): """Catch sigterm.""" raise Exception("ETR has been terminated.") def download_and_load(self): """Download and load test json.""" generator = self.etos.http.wait_for_request(self.tests_url) for response in generator: json_config = response break self.etos.config.set("test_config", json_config) self.etos.config.set("context", json_config.get("context")) self.etos.config.set("artifact", json_config.get("artifact")) def _run_tests(self): """Run tests in ETOS test runner. :return: Results of test runner execution. :rtype: bool """ iut = Iut(self.etos.config.get("test_config").get("iut")) test_runner = TestRunner(iut, self.etos) return test_runner.execute() def run_etr(self): """Send activity events and run ETR. :return: Result of testrunner execution. :rtype: bool """ _LOGGER.info("Starting ETR.") self.download_and_load() try: activity_name = self.etos.config.get("test_config").get("name") triggered = self.etos.events.send_activity_triggered(activity_name) self.etos.events.send_activity_started(triggered) result = self._run_tests() except Exception as exc: # pylint:disable=broad-except self.etos.events.send_activity_finished(triggered, { "conclusion": "FAILED", "description": str(exc) }) raise self.etos.events.send_activity_finished(triggered, {"conclusion": "SUCCESSFUL"}) _LOGGER.info("ETR finished.") return result
class ESR: # pylint:disable=too-many-instance-attributes """Suite runner for ETOS main program. Run this as a daemon on your system in order to trigger test suites within the eiffel event system. """ def __init__(self): """Initialize ESR by creating a rabbitmq publisher.""" self.logger = logging.getLogger("ESR") self.etos = ETOS( "ETOS Suite Runner", os.getenv("SOURCE_HOST"), "ETOS Suite Runner" ) signal.signal(signal.SIGTERM, self.graceful_exit) self.params = ESRParameters(self.etos) FORMAT_CONFIG.identifier = self.params.tercc.meta.event_id self.etos.config.rabbitmq_publisher_from_environment() self.etos.start_publisher() self.etos.config.set( "WAIT_FOR_ENVIRONMENT_TIMEOUT", int(os.getenv("ESR_WAIT_FOR_ENVIRONMENT_TIMEOUT")), ) def _request_environment(self): """Request an environment from the environment provider. :return: Task ID and an error message. :rtype: tuple """ params = {"suite_id": self.params.tercc.meta.event_id} wait_generator = self.etos.http.retry( "POST", self.etos.debug.environment_provider, json=params ) task_id = None result = {} try: for response in wait_generator: result = response.get("result", "") if response and result and result.lower() == "success": task_id = response.get("data", {}).get("id") break continue else: return None, "Did not retrieve an environment" except ConnectionError as exception: return None, str(exception) return task_id, "" def _wait_for_environment(self, task_id): """Wait for an environment being provided. :param task_id: Task ID to wait for. :type task_id: str :return: Environment and an error message. :rtype: tuple """ timeout = self.etos.config.get("WAIT_FOR_ENVIRONMENT_TIMEOUT") wait_generator = self.etos.utils.wait( self.etos.http.wait_for_request, uri=self.etos.debug.environment_provider, timeout=timeout, params={"id": task_id}, ) environment = None result = {} response = None for generator in wait_generator: for response in generator: result = response.get("result", {}) if response and result and result.get("error") is None: environment = response break if result and result.get("error"): return None, result.get("error") if environment is not None: break else: if result and result.get("error"): return None, result.get("error") return ( None, ( "Unknown Error: Did not receive an environment " f"within {self.etos.debug.default_http_timeout}s" ), ) return environment, "" def _release_environment(self, task_id): """Release an environment from the environment provider. :param task_id: Task ID to release. :type task_id: str """ wait_generator = self.etos.http.wait_for_request( self.etos.debug.environment_provider, params={"release": task_id} ) for response in wait_generator: if response: break def _reserve_workers(self): """Reserve workers for test.""" LOGGER.info("Request environment from environment provider") task_id, msg = self._request_environment() if task_id is None: raise EnvironmentProviderException(msg, task_id) LOGGER.info("Wait for environment to become ready.") environment, msg = self._wait_for_environment(task_id) if environment is None: raise EnvironmentProviderException(msg, task_id) return environment, task_id def run_suite(self, triggered): """Trigger an activity and starts the actual test runner. Will only start the test activity if there's a 'slot' available. :param triggered: Activity triggered. :type triggered: :obj:`eiffel.events.EiffelActivityTriggeredEvent` """ context = triggered.meta.event_id LOGGER.info("Sending ESR Docker environment event.") self.etos.events.send_environment_defined( "ESR Docker", {"CONTEXT": context}, image=os.getenv("SUITE_RUNNER") ) runner = SuiteRunner(self.params, self.etos, context) task_id = None try: LOGGER.info("Wait for test environment.") environment, task_id = self._reserve_workers() self.etos.events.send_activity_started(triggered, {"CONTEXT": context}) LOGGER.info("Starting ESR.") runner.run(environment.get("result")) except EnvironmentProviderException as exception: task_id = exception.task_id raise finally: LOGGER.info("Release test environment.") if task_id is not None: self._release_environment(task_id) @staticmethod def verify_input(): """Verify that the data input to ESR are correct.""" assert os.getenv( "SUITE_RUNNER" ), "SUITE_RUNNER enviroment variable not provided." assert os.getenv( "SOURCE_HOST" ), "SOURCE_HOST environment variable not provided." assert os.getenv("TERCC"), "TERCC environment variable not provided." def run(self): """Run the ESR main loop.""" tercc_id = None try: tercc_id = self.params.tercc.meta.event_id self.etos.events.send_announcement_published( "[ESR] Launching.", "Starting up ESR. Waiting for tests to start.", "MINOR", {"CAUSE": tercc_id}, ) activity_name = "ETOS testrun" links = { "CAUSE": [ self.params.tercc.meta.event_id, self.params.artifact_created["meta"]["id"], ] } triggered = self.etos.events.send_activity_triggered( activity_name, links, executionType="AUTOMATED", triggers=[{"type": "EIFFEL_EVENT"}], ) self.verify_input() context = triggered.meta.event_id except: # noqa self.etos.events.send_announcement_published( "[ESR] Failed to start test execution", traceback.format_exc(), "CRITICAL", {"CAUSE": tercc_id}, ) raise try: self.run_suite(triggered) self.etos.events.send_activity_finished( triggered, {"conclusion": "SUCCESSFUL"}, {"CONTEXT": context} ) except Exception as exception: # pylint:disable=broad-except reason = str(exception) self.etos.events.send_activity_canceled( triggered, {"CONTEXT": context}, reason=reason ) self.etos.events.send_announcement_published( "[ESR] Test suite execution failed", traceback.format_exc(), "MAJOR", {"CONTEXT": context}, ) raise def graceful_exit(self, *_): """Attempt to gracefully exit the running job.""" self.logger.info( "Kill command received - Attempting to shut down all processes." ) raise Exception("Terminate command received - Shutting down.")
class SuiteStarter: # pylint:disable=too-many-instance-attributes """Suite starter main program.""" announcement = None def __init__(self): """Initialize SuiteStarter by creating a rabbitmq publisher and subscriber.""" self.etos = ETOS( "ETOS Suite Starter", os.getenv("HOSTNAME"), "ETOS Suite Starter" ) self._configure() self.etos.config.rabbitmq_subscriber_from_environment() self.etos.config.rabbitmq_publisher_from_environment() self.etos.start_subscriber() self.etos.start_publisher() self.etos.subscriber.subscribe( "EiffelTestExecutionRecipeCollectionCreatedEvent", self.suite_runner_callback, can_nack=True, ) def _configure(self): """Configure ETOS library.""" self.etos.config.set("suite_runner", os.getenv("SUITE_RUNNER")) def suite_runner_callback(self, event, _): """Start a suite runner on a TERCC event. :param event: EiffelTestExecutionRecipeCollectionCreatedEvent (TERCC) :type event: :obj: `eiffellib.events.base_event.EiffelTestExecutionRecipeCollectionCreatedEvent` # noqa pylint:disable=line-too-long :return: Whether event was ACK:ed or not. :rtype: bool """ suite_id = event.meta.event_id FORMAT_CONFIG.identifier = suite_id LOGGER.info("Received a TERCC event. Build data for ESR.") data = { "EiffelTestExecutionRecipeCollectionCreatedEvent": json.dumps(event.json) } data["etos_configmap"] = os.getenv("ETOS_CONFIGMAP") data["docker_image"] = self.etos.config.get("suite_runner") data["suite_id"] = suite_id with_sidecar = os.getenv("ETOS_SIDECAR_ENABLED", "false").lower() == "true" if with_sidecar: data["sidecar_image"] = os.getenv("ETOS_SIDECAR_IMAGE") job = Job(in_cluster=bool(os.getenv("DOCKER_CONTEXT"))) job_name = job.uniqueify(f"suite-runner-{suite_id}").lower() data["job_name"] = job_name LOGGER.info("Data: %r", data) try: assert data["EiffelTestExecutionRecipeCollectionCreatedEvent"] assert data["etos_configmap"], "Missing ETOS_CONFIGMAP in environment" assert data["docker_image"], "Missing SUITE_RUNNER in environment" except AssertionError as exception: LOGGER.critical("Incomplete data for ESR. %r", exception) raise if with_sidecar: body = job.load_yaml(ESR_YAML_WITH_SIDECAR.format(**data)) else: body = job.load_yaml(ESR_YAML.format(**data)) LOGGER.info("Starting new executor: %r", job_name) job.create_job(body) LOGGER.info("ESR successfully launched.") return True def run(self): """Run the SuiteStarter main loop. Checks if required data has been received within the same context and if it does, trigger a runner job within a thread. The thread is never joined and is daemonized. This means that if SuiteStarter would exit, all tests running within will also exit. """ body = ( "Suite starter is running and listening to " "events in the Eiffel context.\n" "Configmap:\n" f"ETOS Suite Runner: {os.getenv('SUITE_RUNNER')}\n" ) self.etos.monitor.keep_alive(body) # Blocking.
class SuiteStarter: # pylint:disable=too-many-instance-attributes """Suite starter main program.""" announcement = None def __init__(self): """Initialize SuiteStarter by creating a rabbitmq publisher and subscriber.""" self.etos = ETOS("ETOS Suite Starter", os.getenv("HOSTNAME"), "ETOS Suite Starter") self._configure() self.etos.config.rabbitmq_subscriber_from_environment() self.etos.config.rabbitmq_publisher_from_environment() self.etos.start_subscriber() self.etos.start_publisher() self.etos.subscriber.subscribe( "EiffelTestExecutionRecipeCollectionCreatedEvent", self.suite_runner_callback, can_nack=True, ) def _configure(self): """Configure ETOS library.""" self.etos.config.set("suite_runner", os.getenv("SUITE_RUNNER")) def suite_runner_callback(self, event, _): """Start a suite runner on a TERCC event. :param event: EiffelTestExecutionRecipeCollectionCreatedEvent (TERCC) :type event: :obj: `eiffellib.events.base_event.EiffelTestExecutionRecipeCollectionCreatedEvent` # noqa pylint:disable=line-too-long :return: Whether event was ACK:ed or not. :rtype: bool """ suite_id = event.meta.event_id data = { "EiffelTestExecutionRecipeCollectionCreatedEvent": json.dumps(event.json) } data["etos_configmap"] = os.getenv("ETOS_CONFIGMAP") data["docker_image"] = self.etos.config.get("suite_runner") data["suite_id"] = suite_id job = Job(in_cluster=bool(os.getenv("DOCKER_CONTEXT"))) job_name = job.uniqueify("suite-runner-{}".format(suite_id)).lower() data["job_name"] = job_name assert data["EiffelTestExecutionRecipeCollectionCreatedEvent"] assert data["etos_configmap"], "Missing ETOS_CONFIGMAP in environment" assert data["docker_image"], "Missing SUITE_RUNNER in environment" body = job.load_yaml(ESR_YAML.format(**data)) _logger.info("Starting new executor: %s", job_name) job.create_job(body) return True def run(self): """Run the SuiteStarter main loop. Checks if required data has been received within the same context and if it does, trigger a runner job within a thread. The thread is never joined and is daemonized. This means that if SuiteStarter would exit, all tests running within will also exit. """ body = ("Suite starter is running and listening to " "events in the Eiffel context.\n" "Configmap:\n" "ETOS Suite Runner: {}\n".format(os.getenv("SUITE_RUNNER"))) self.etos.monitor.keep_alive(body) # Blocking.