splight-runner 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ from splight_runner.runner import runner
2
+
3
+ __all__ = [runner]
File without changes
@@ -0,0 +1,55 @@
1
+ import requests
2
+
3
+ from splight_runner.api.settings import settings
4
+
5
+
6
+ class StatusReporter:
7
+ """Class responsible for updating the component status each time the
8
+ healthcheck method is called.
9
+ """
10
+
11
+ _SPL_PREFIX = "Splight"
12
+ _BASE_PATH = "v2/engine/component/components"
13
+
14
+ def __init__(
15
+ self, api_host: str, access_id: str, secret_key: str, component_id: str
16
+ ):
17
+ self._api_host = api_host.rstrip("/")
18
+ self._component_id = component_id
19
+ self._auth_header = {
20
+ "Authorization": f"{self._SPL_PREFIX} {access_id} {secret_key}"
21
+ }
22
+ self._prev_status: str = "Unknown"
23
+
24
+ def report_status(self, status: str) -> None:
25
+ """Updates the status of the component in the Splight Platform making
26
+ POST request to the API.
27
+
28
+ Parameters
29
+ ----------
30
+ status : str
31
+ The status of the component.
32
+ It can be "Running", "Stopped" or "Succeeded".
33
+ """
34
+ if self._prev_status == status:
35
+ return None
36
+ url = f"{self._api_host}/{self._BASE_PATH}"
37
+ url = f"{url}/{self._component_id}/update-status/"
38
+ response = requests.post(
39
+ url, headers=self._auth_header, data={"deployment_status": status}
40
+ )
41
+ try:
42
+ response.raise_for_status()
43
+ except requests.exceptions.HTTPError as exc:
44
+ print("Unable to update component status")
45
+ print(exc)
46
+ self._prev_status = status
47
+ return None
48
+
49
+
50
+ reporter = StatusReporter(
51
+ api_host=settings.splight_platform_api_host,
52
+ access_id=settings.access_id,
53
+ secret_key=settings.secret_key,
54
+ component_id=settings.process_id,
55
+ )
@@ -0,0 +1,85 @@
1
+ import traceback
2
+ from logging import LogRecord
3
+
4
+ from splight_runner.api.settings import settings
5
+ from splight_runner.log_streamer.log_streamer import ComponentLogsStreamer
6
+ from splight_runner.logging import log
7
+
8
+
9
+ class ApplicationLogInterceptor:
10
+ """Class responsible for intercept logs records from the logging module
11
+ and append them into a queue for further processing. In particular for
12
+ sending the logs events to Splight GRPC API.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ host: str,
18
+ access_id: str,
19
+ secret_key: str,
20
+ process_id: str,
21
+ sender_type: str,
22
+ ):
23
+ self._streamer = ComponentLogsStreamer(
24
+ host=host,
25
+ access_id=access_id,
26
+ secret_key=secret_key,
27
+ process_id=process_id,
28
+ )
29
+ self._sender_type = sender_type
30
+ self._streamer.start()
31
+
32
+ def save_record(self, record: LogRecord) -> None:
33
+ """Saves the logging record to be sent.
34
+
35
+ Parameters
36
+ ----------
37
+ record: LogRecord
38
+ The log record.
39
+ """
40
+ exc_info = None
41
+ try:
42
+ message = str(record.msg) % record.args
43
+ except Exception as exc:
44
+ log(exc)
45
+ message = str(record.msg)
46
+
47
+ if record.exc_info:
48
+ exc_info = "".join(
49
+ traceback.format_exception(*record.exc_info)
50
+ ).replace('"', "'")
51
+ message = str(record.msg)
52
+ else:
53
+ exc_info = ""
54
+
55
+ # tags attribute is not default in logger
56
+ tags = None
57
+ if hasattr(record, "tags"):
58
+ tags = getattr(record, "tags")
59
+ event = {
60
+ "sender_type": self._sender_type,
61
+ "name": record.name,
62
+ "message": message,
63
+ "loglevel": record.levelname,
64
+ "filename": record.filename,
65
+ "traceback": exc_info,
66
+ "tags": tags,
67
+ }
68
+ self._streamer.insert_message(event)
69
+
70
+ def flush(self) -> None:
71
+ """Flushes the log records."""
72
+ self._streamer.flush()
73
+
74
+ def stop(self) -> None:
75
+ """Stops the thread."""
76
+ self._streamer.stop()
77
+
78
+
79
+ interceptor = ApplicationLogInterceptor(
80
+ host=settings.splight_platform_api_host,
81
+ access_id=settings.access_id,
82
+ secret_key=settings.secret_key,
83
+ process_id=settings.process_id,
84
+ sender_type=settings.process_type,
85
+ )
@@ -0,0 +1,63 @@
1
+ import os
2
+ from dataclasses import dataclass, field
3
+ from functools import partial
4
+ from typing import List
5
+
6
+ SECRET_DIR = "/etc/config"
7
+
8
+
9
+ class MissingEnvVariable(Exception):
10
+ ...
11
+
12
+
13
+ def load_variable(var_name: str) -> str:
14
+ secret_file = os.path.join(SECRET_DIR, var_name)
15
+ if os.path.exists(secret_file):
16
+ with open(secret_file) as f:
17
+ variable = f.read().strip()
18
+ elif os.getenv(var_name):
19
+ variable = os.getenv(var_name)
20
+ else:
21
+ raise MissingEnvVariable(f"{var_name} is missing")
22
+ return variable
23
+
24
+
25
+ def load_multiple_variable(var_names: List[str]) -> str:
26
+ value = None
27
+ for var_name in var_names:
28
+ try:
29
+ value = load_variable(var_name)
30
+ break
31
+ except MissingEnvVariable:
32
+ continue
33
+
34
+ if value is None:
35
+ raise MissingEnvVariable(f"On of {var_names} is missing")
36
+ return value
37
+
38
+
39
+ @dataclass
40
+ class SplightSettings:
41
+ """Class for holding Splight Runner settings."""
42
+
43
+ access_id: str = field(
44
+ default_factory=partial(load_variable, "SPLIGHT_ACCESS_ID")
45
+ )
46
+ secret_key: str = field(
47
+ default_factory=partial(load_variable, "SPLIGHT_SECRET_KEY")
48
+ )
49
+ splight_platform_api_host: str = field(
50
+ default_factory=partial(load_variable, "SPLIGHT_PLATFORM_API_HOST")
51
+ )
52
+ process_id: str = field(
53
+ default_factory=partial(
54
+ load_multiple_variable,
55
+ ["COMPONENT_ID", "COMPUTE_NODE_ID", "AGENT_ID"],
56
+ )
57
+ )
58
+ process_type: str = field(
59
+ default_factory=partial(load_variable, "PROCESS_TYPE")
60
+ )
61
+
62
+
63
+ settings = SplightSettings()
File without changes
@@ -0,0 +1,39 @@
1
+ # This file is used for configuring splight runner in runtime. In particular
2
+ # loads some env vars and activate hooks for sending logs.
3
+ # All hooks or configurations for the Splight Runner should be loaded in
4
+ # this file.
5
+
6
+ import ast
7
+ import os
8
+ import sys
9
+
10
+ runner_active = ast.literal_eval(os.getenv("SPLIGHT_RUNNER_ACTIVE", "False"))
11
+
12
+ boot_directory = os.path.dirname(__file__)
13
+ root_directory = os.path.dirname(os.path.dirname(boot_directory))
14
+
15
+ path = list(sys.path)
16
+
17
+ if boot_directory in path:
18
+ del path[path.index(boot_directory)]
19
+
20
+ try:
21
+ from importlib.machinery import PathFinder
22
+
23
+ module_spec = PathFinder.find_spec("sitecustomize", path=path)
24
+ except ImportError as exc:
25
+ sys.stdout.write("Unable to import Splight Runner sitecustomie", exc)
26
+ sys.stdout.flush()
27
+ else:
28
+ if module_spec is not None:
29
+ module_spec.loader.load_module("sitecustomize")
30
+
31
+ if runner_active:
32
+ do_insert = root_directory not in sys.path
33
+ if do_insert:
34
+ sys.path.insert(0, root_directory)
35
+
36
+ import splight_runner.config
37
+
38
+ if do_insert:
39
+ del sys.path[sys.path.index(root_directory)]
File without changes
@@ -0,0 +1,136 @@
1
+ import inspect
2
+ import sys
3
+ from typing import Any, Callable, Dict, List, Optional
4
+
5
+
6
+ class MissingCommandError(Exception):
7
+ """Exception"""
8
+
9
+ ...
10
+
11
+
12
+ class MissingArgument(Exception):
13
+ """Exception raises when there is a missing argument for a command"""
14
+
15
+ ...
16
+
17
+
18
+ class CommandAdmin:
19
+ """Class respobsible for taking registry of all the commands and callbaks"""
20
+
21
+ def __init__(self, name: str, version: str):
22
+ """Constructor
23
+
24
+ Parameters
25
+ ----------
26
+ name : str
27
+ Name of the application
28
+ version : str
29
+ The application version
30
+ """
31
+ self._name = name
32
+ self._version = version
33
+ self._commands: Dict[str, Callable] = {}
34
+ self._callback: Dict[str, Callable] = {}
35
+
36
+ def command(self, name: Optional[str] = None) -> Callable:
37
+ """Decorator to register a command
38
+
39
+ Parameters
40
+ ----------
41
+ name: Optional[str]
42
+ The name for the command
43
+
44
+ Returns
45
+ -------
46
+ Callable
47
+ The decorated function
48
+ """
49
+
50
+ def decorator(func: Callable) -> Callable:
51
+ func_name = name or func.__name__
52
+ self._commands.update({func_name: func})
53
+ return func
54
+
55
+ return decorator
56
+
57
+ def callback(self, name: Optional[str] = None) -> Callable:
58
+ """Decorator to register a callback
59
+
60
+ Parameters
61
+ ----------
62
+ name: Optional[str]
63
+ The name for the command
64
+
65
+ Returns
66
+ -------
67
+ Callable
68
+ The decorated function
69
+ """
70
+
71
+ def decorator(func: Callable) -> Callable:
72
+ func_name = name or func.__name__
73
+ self._callback.update({func_name: func})
74
+ return func
75
+
76
+ return decorator
77
+
78
+ def print_commands(self) -> None:
79
+ """Prints the commands and callbacks"""
80
+ title = f"{self._name} - {self._version}\n"
81
+ sys.stdout.write(title)
82
+ sys.stdout.write("{:=<{}}\n".format("", len(title)))
83
+ sys.stdout.write("Commands:\n")
84
+ for command in self._commands:
85
+ sys.stdout.write(f"{command: >20}\n")
86
+ sys.stdout.write("Callbacks:\n")
87
+ for callback in self._callback:
88
+ name = f"--{callback}"
89
+ sys.stdout.write(f"{name: >20}\n")
90
+
91
+ def __call__(self, *args: Any, **kwargs: Any):
92
+ """Dunder method to run a given command."""
93
+ if len(sys.argv) == 1:
94
+ raise MissingCommandError("No command provided")
95
+
96
+ command_name = sys.argv[1]
97
+ raw_args = sys.argv[2:]
98
+ if command_name.startswith("--"):
99
+ command = self._retrieve_callback(command_name.lstrip("--"))
100
+ else:
101
+ command = self._retrieve_command(command_name)
102
+
103
+ signature = inspect.signature(command)
104
+ command_args = self._parse_arguments(raw_args, signature=signature)
105
+ command(**command_args)
106
+
107
+ def _parse_arguments(
108
+ self, raw_args: List, signature: inspect.Signature
109
+ ) -> Dict:
110
+ arguments = {}
111
+ for idx, (name, parameter) in enumerate(signature.parameters.items()):
112
+ try:
113
+ raw_value = raw_args[idx]
114
+ except IndexError as exc:
115
+ raise MissingArgument(f"Missing argument {name}") from exc
116
+
117
+ argument = (
118
+ raw_value.split("=")[-1] if "=" in raw_value else raw_value
119
+ )
120
+ arg_value = parameter.annotation(argument)
121
+ arguments.update({name: arg_value})
122
+ return arguments
123
+
124
+ def _retrieve_command(self, command_name: str) -> Callable:
125
+ try:
126
+ command = self._commands[command_name]
127
+ except KeyError as exc:
128
+ sys.stderr.write(f"Command {command_name} not found: {exc}")
129
+ return command
130
+
131
+ def _retrieve_callback(self, command_name: str) -> Callable:
132
+ try:
133
+ command = self._callback[command_name]
134
+ except KeyError as exc:
135
+ sys.stderr.write(f"Callback {command_name} not found: {exc}")
136
+ return command
@@ -0,0 +1,43 @@
1
+ import os
2
+
3
+ from splight_runner import __file__ as root_file
4
+ from splight_runner.api.settings import settings
5
+ from splight_runner.logging import log
6
+
7
+
8
+ def execute_agent() -> None:
9
+ """Executes an splight agent after configuring splight runner hooks."""
10
+
11
+ root_dir = os.path.dirname(root_file)
12
+ boot_dir = os.path.join(root_dir, "bootstrap")
13
+
14
+ python_path = boot_dir
15
+ if "PYTHONPATH" in os.environ:
16
+ all_paths = os.getenv("PYTHONPATH").split(os.path.pathsep)
17
+ if boot_dir not in all_paths:
18
+ all_paths.insert(0, boot_dir)
19
+ python_path = os.path.pathsep.join(all_paths)
20
+
21
+ log("Configuring PYTHONPATH env var")
22
+ os.environ["PYTHONPATH"] = python_path
23
+ # The following variable is used for activate hooks
24
+ os.environ["SPLIGHT_RUNNER_ACTIVE"] = "True"
25
+
26
+ # Configure env variables for running Splight Agent
27
+ if "SPLIGHT_ACCESS_ID" not in os.environ:
28
+ os.environ["SPLIGHT_ACCESS_ID"] = settings.access_id
29
+ if "SPLIGHT_SECRET_KEY" not in os.environ:
30
+ os.environ["SPLIGHT_SECRET_KEY"] = settings.secret_key
31
+ if "SPLIGHT_PLATFORM_API_HOST" not in os.environ:
32
+ os.environ[
33
+ "SPLIGHT_PLATFORM_API_HOST"
34
+ ] = settings.splight_platform_api_host
35
+ if "COMPUTE_NODE_ID" not in os.environ:
36
+ os.environ["COMPUTE_NODE_ID"] = settings.process_id
37
+
38
+ log("Executing Splight Agent")
39
+
40
+ command = ["/bin/bash", "-c", "splight-agent"]
41
+ exec_path = "/bin/bash"
42
+
43
+ os.execl(exec_path, *command)
@@ -0,0 +1,84 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ from splight_runner import __file__ as root_file
6
+ from splight_runner.api.settings import settings
7
+
8
+
9
+ class InvalidComponentLanguage(Exception):
10
+ ...
11
+
12
+
13
+ class ComponentDoesNotExists(Exception):
14
+ ...
15
+
16
+
17
+ def execute_component(component_file: Path, component_id: str) -> None:
18
+ """Executes a component after configuring splight runner hooks.
19
+
20
+ Parameters
21
+ ----------
22
+ component_file: Path
23
+ The path to the main file for the component.
24
+ component_id: str
25
+ The ID for the component to be executed.
26
+
27
+ Returns
28
+ -------
29
+ None
30
+
31
+ Raises
32
+ ------
33
+ ComponentDoesNotExists
34
+ If the component file does not exists.
35
+ InvalidComponentLanguage
36
+ If the component is written in a non-supported language.
37
+ """
38
+ # TODO: Add some logs messages for debugging
39
+ main_file = component_file.name
40
+ component_path = component_file.parent.resolve()
41
+ if not component_file.exists():
42
+ raise ComponentDoesNotExists("Component does not exists")
43
+ if main_file.endswith(".py"):
44
+ base_cmd = "python3"
45
+ else:
46
+ raise InvalidComponentLanguage(
47
+ "Component is not written in a supported language"
48
+ )
49
+
50
+ root_dir = os.path.dirname(root_file)
51
+ boot_dir = os.path.join(root_dir, "bootstrap")
52
+
53
+ python_path = boot_dir
54
+ if "PYTHONPATH" in os.environ:
55
+ all_paths = os.getenv("PYTHONPATH").split(os.path.pathsep)
56
+ if boot_dir not in all_paths:
57
+ all_paths.insert(0, boot_dir)
58
+ python_path = os.path.pathsep.join(all_paths)
59
+
60
+ os.environ["PYTHONPATH"] = python_path
61
+ # The following variable is used for activate hooks
62
+ os.environ["SPLIGHT_RUNNER_ACTIVE"] = "True"
63
+
64
+ # TODO: improve how env variables are configured
65
+ if "SPLIGHT_ACCESS_ID" not in os.environ:
66
+ os.environ["SPLIGHT_ACCESS_ID"] = settings.access_id
67
+ if "SPLIGHT_SECRET_KEY" not in os.environ:
68
+ os.environ["SPLIGHT_SECRET_KEY"] = settings.secret_key
69
+ if "SPLIGHT_PLATFORM_API_HOST" not in os.environ:
70
+ os.environ[
71
+ "SPLIGHT_PLATFORM_API_HOST"
72
+ ] = settings.splight_platform_api_host
73
+ if "COMPONENT_ID" not in os.environ:
74
+ os.environ["COMPONENT_ID"] = settings.process_id
75
+
76
+ os.chdir(component_path)
77
+ exec_path = sys.executable
78
+ command = [
79
+ base_cmd,
80
+ main_file,
81
+ "--component-id",
82
+ component_id,
83
+ ]
84
+ os.execl(exec_path, *command)
@@ -0,0 +1,72 @@
1
+ import logging
2
+ from types import ModuleType
3
+
4
+ try:
5
+ import splight_lib
6
+
7
+ SPL_LIB_INSTALLED = True
8
+ except Exception as exc:
9
+ print("Error importing splight_lib", exc)
10
+ SPL_LIB_INSTALLED = False
11
+
12
+ from splight_runner.hooks import copy_module, on_import, reload_module
13
+ from splight_runner.logging import log
14
+ from splight_runner.wrapper.healthcheck import healthcheck_wrapper
15
+ from splight_runner.wrapper.hooks import finish_execution
16
+ from splight_runner.wrapper.logging import call_handlers_wrapper
17
+
18
+
19
+ @on_import("logging")
20
+ def on_logging_import(module: ModuleType) -> ModuleType:
21
+ """Hook for modifying default behavior for the logging module.
22
+
23
+ Parameters
24
+ ----------
25
+ module: ModuleType
26
+ The built-in logging module
27
+
28
+ Returns
29
+ -------
30
+ ModuleType: The logging module updated
31
+ """
32
+ original = getattr(module.Logger, "callHandlers", None)
33
+ if original:
34
+ wrapped = call_handlers_wrapper(original)
35
+ new_module = copy_module(logging)
36
+ setattr(new_module.Logger, "callHandlers", wrapped)
37
+ return new_module
38
+ return module
39
+
40
+
41
+ @on_import("splight_lib")
42
+ def on_splight_lib_import(module: ModuleType) -> ModuleType:
43
+ if hasattr(module.execution, "ExecutionClient"):
44
+ original = getattr(
45
+ module.execution.ExecutionClient, "healthcheck", None
46
+ )
47
+ engine_name = "ExecutionClient"
48
+ elif hasattr(module.execution, "ExecutionEngine"):
49
+ original = getattr(
50
+ module.execution.ExecutionEngine, "healthcheck", None
51
+ )
52
+ engine_name = "ExecutionEngine"
53
+ else:
54
+ log("Unable to find Execution Engine in splight_lib")
55
+ return module
56
+ if original and SPL_LIB_INSTALLED:
57
+ wrapped = healthcheck_wrapper(original)
58
+ new_module = copy_module(splight_lib)
59
+ engine = getattr(module.execution, engine_name)
60
+ setattr(engine, "healthcheck", wrapped)
61
+ return new_module
62
+ return module
63
+
64
+
65
+ # Reload logging module to update everywhere
66
+ reload_module(logging)
67
+
68
+
69
+ if SPL_LIB_INSTALLED:
70
+ reload_module(splight_lib)
71
+
72
+ finish_execution()