Ejemplo n.º 1
0
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
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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.")
Ejemplo n.º 4
0
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.
Ejemplo n.º 5
0
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.