-
Notifications
You must be signed in to change notification settings - Fork 70
Automating platform service setup #1374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
prasadtalasila
merged 33 commits into
INTO-CPS-Association:feature/distributed-demo
from
8ohamed:feature/distributed-demo
Dec 2, 2025
+277
−58
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
6c13134
Define resource limits, and adds tests in test_config
8ohamed 8968d0c
test utils updated
8ohamed 71a6250
added limits to compose files
8ohamed 3e91a9e
added cpuset + resolved issues
8ohamed 337b661
test issue
8ohamed ae7a39e
removed cpuset
8ohamed 72a0dff
remove cpuset
8ohamed 1166023
applies snake_case to tests
8ohamed a9311a6
resolves sonar issues
8ohamed a34b554
minor updates
8ohamed 819e502
update pid limit in compose
8ohamed 9b15119
changed pids_limit back
8ohamed d4e99f0
initial commit
8ohamed 9a2c8cf
Merge branch 'INTO-CPS-Association:feature/distributed-demo' into fea…
8ohamed 5d12713
test issue
8ohamed b9abf46
applies snake_case to tests
8ohamed ce3ff7e
resolves sonar issues
8ohamed f2efa23
minor updates
8ohamed 47f3885
update pid limit in compose
8ohamed 8e87d19
changed pids_limit back
8ohamed f6cc63a
initial commit
8ohamed 5d15586
Merge branch 'feature/distributed-demo' of https://github.com/8ohamed…
8ohamed d76b287
minor fix
8ohamed 6595acf
removed user arg
8ohamed 3b4f545
refactors service_setup, updates readme
8ohamed 0380b11
applies coding suggestions
8ohamed 1317cc9
Handles windows OS
8ohamed 3542676
applying suggestions
8ohamed 4c9587f
applying suggestions
8ohamed dc2e4a5
minor updates
8ohamed ead2bb4
minor updates
8ohamed a85da0c
fixes bugs
8ohamed 642a266
minor update
8ohamed File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| python-dotenv>=1.0.0,<2.0.0 |
8ohamed marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,224 @@ | ||
| import platform | ||
| import os | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
| from typing import Tuple | ||
| from dotenv import load_dotenv | ||
8ohamed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| class ServicesConfig: | ||
| """ | ||
| Configuration and setup utility for DTaaS platform services. | ||
|
|
||
| This class handles: | ||
| - Loading environment variables and service configuration. | ||
| - Managing TLS certificates for services (copying, normalizing, combining). | ||
| - Setting file permissions and ownership for MongoDB, InfluxDB, and RabbitMQ. | ||
| - Starting platform services using Docker Compose. | ||
| - Supporting Linux/MacOS and Windows environments. | ||
| """ | ||
|
|
||
| def __init__(self) -> None: | ||
|
|
||
| """Initialize configuration paths and files, grouped by service.""" | ||
| self.base_dir = Path(__file__).parent.resolve() | ||
8ohamed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self.env_file = self.base_dir.parent / "config" / "services.env" | ||
| self.env = self._load_env(self.env_file) | ||
|
|
||
| self.host_name = self.get_required_env("HOSTNAME") | ||
8ohamed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self.os_type = platform.system().lower() | ||
|
|
||
| self.dir_path = { | ||
| "config": self.base_dir.parent / "config", | ||
| "data": self.base_dir.parent / "data", | ||
| "certs": self.base_dir.parent / "certs", | ||
| } | ||
|
|
||
| self.certs = { | ||
| "dir": self.dir_path["certs"] / self.host_name, | ||
| "privkey": self.dir_path["certs"] / self.host_name / "privkey.pem", | ||
| "fullchain": self.dir_path["certs"] / self.host_name / "fullchain.pem", | ||
| "combined": self.dir_path["certs"] / self.host_name / "combined.pem", | ||
| "influx_key": self.dir_path["certs"] / self.host_name / "privkey-influxdb.pem", | ||
| "rabbit_key": self.dir_path["certs"] / self.host_name / "privkey-rabbitmq.pem", | ||
| } | ||
|
|
||
| self.influx = { | ||
| "uid": self.get_required_env("INFLUX_UID"), | ||
| "gid": self.get_required_env("INFLUX_GID"), | ||
| "key": self.certs["influx_key"], | ||
| } | ||
| self.mongo = { | ||
| "uid": self.get_required_env("MONGO_UID"), | ||
| "gid": self.get_required_env("MONGO_GID"), | ||
| "combined": self.certs["combined"], | ||
| } | ||
| self.rabbitmq = { | ||
| "uid": self.get_required_env("RABBIT_UID"), | ||
| "key": self.certs["rabbit_key"], | ||
| } | ||
|
|
||
| self.env_template = self.dir_path["config"] / "services.env.template" | ||
| self.compose_file = self.base_dir.parent / "compose.services.secure.yml" | ||
|
|
||
| if self.os_type in ("linux", "darwin"): | ||
| self._check_root_unix() | ||
|
|
||
8ohamed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| def _check_root_unix(self) -> None: | ||
| """Check if script is run as root on Unix systems.""" | ||
| try: | ||
| is_root = os.geteuid() == 0 | ||
| except AttributeError: | ||
| is_root = False | ||
| if not is_root: | ||
| print("This script must be run as root (Linux/MacOS).") | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| def _load_env(self, env_path: Path) -> dict: | ||
| """Load environment variables from a file into a dictionary.""" | ||
8ohamed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if not env_path.exists(): | ||
| raise FileNotFoundError(f"Environment (config/services.env) file not found: {env_path}") | ||
| load_dotenv(dotenv_path=env_path, override=True) | ||
| return dict(os.environ) | ||
|
|
||
|
|
||
| def get_required_env(self, var_name: str) -> str: | ||
| """Retrieve a required environment variable from the loaded env dict.""" | ||
| value = self.env.get(var_name) | ||
| if value is None: | ||
| raise RuntimeError( | ||
| f"Required environment variable '{var_name}' is not set. " | ||
| f"Please ensure it is defined in the services.env file.") | ||
| return value | ||
|
|
||
|
|
||
| def copy_certs(self) -> Tuple[bool, str]: | ||
| """Obtain TLS certificates for services.""" | ||
| source_dir = Path(self.get_required_env("CERTS_SRC")) | ||
| if not source_dir.exists(): | ||
| return False, f"Source directory for certs not found: {source_dir}" | ||
| self.certs["dir"].mkdir(parents=True, exist_ok=True) | ||
| try: | ||
| for path in source_dir.glob("*"): | ||
| if path.is_file(): | ||
| shutil.copy2(path, self.certs["dir"] / path.name) | ||
| self._normalize_cert_candidates("privkey") | ||
| self._normalize_cert_candidates("fullchain") | ||
| return True, f"Certificates copied and normalized in {self.certs['dir']}" | ||
| except OSError as e: | ||
| return False, f"Error copying certificates: {e}" | ||
|
|
||
|
|
||
| def _normalize_cert_candidates(self, prefix: str) -> None: | ||
| """Keep only the latest cert file for a given prefix, rename it, and remove others.""" | ||
| candidates = list(self.certs["dir"].glob(f"{prefix}*.pem")) | ||
| if not candidates: | ||
| return | ||
| latest = max(candidates, key=lambda p: p.stat().st_mtime) | ||
| target = self.certs["dir"] / f"{prefix}.pem" | ||
| if latest.resolve() != target.resolve(): | ||
| target.unlink(missing_ok=True) | ||
| latest.rename(target) | ||
| for p in candidates: | ||
| if p.resolve() != target.resolve(): | ||
| p.unlink(missing_ok=True) | ||
|
|
||
|
|
||
| def _create_combined_pem(self) -> None: | ||
| """Create combined.pem from privkey.pem and fullchain.pem.""" | ||
| privkey_path = self.certs["privkey"] | ||
| fullchain_path = self.certs["fullchain"] | ||
| if not privkey_path.exists(): | ||
| raise FileNotFoundError(f"Missing privkey.pem at {privkey_path}.") | ||
| if not fullchain_path.exists(): | ||
| raise FileNotFoundError(f"Missing fullchain.pem at {fullchain_path}.") | ||
| with open(self.certs["combined"], "wb") as out_f: | ||
| with open(privkey_path, "rb") as pk: | ||
| out_f.write(pk.read()) | ||
| with open(fullchain_path, "rb") as fc: | ||
| out_f.write(fc.read()) | ||
|
|
||
|
|
||
| def permissions_mongodb(self) -> Tuple[bool, str]: | ||
| """Creates combined.pem and sets permissions for MongoDB.""" | ||
| try: | ||
| self.certs["dir"].mkdir(parents=True, exist_ok=True) | ||
| self._create_combined_pem() | ||
| if self.os_type in ("linux", "darwin"): | ||
| self.certs["combined"].chmod(0o600) | ||
| chown_args = ["chown", f"{self.mongo['uid']}:{self.mongo['gid']}", str(self.certs["combined"])] | ||
| subprocess.run(chown_args, check=True) | ||
| return True, (f"combined.pem created with mode 600 and ownership set to " | ||
| f"{self.mongo['uid']}:{self.mongo['gid']}.") | ||
| except OSError as e: | ||
| return False, f"Error setting permissions for MongoDB: {e}" | ||
| except subprocess.CalledProcessError as e: | ||
| return False, f"Failed to set ownership: {str(e)}" | ||
|
|
||
|
|
||
| def permissions_influxdb(self) -> Tuple[bool, str]: | ||
| """Copy privkey.pem -> privkey-influxdb.pem and change owner.""" | ||
| try: | ||
| shutil.copy2(self.certs["privkey"], self.influx["key"]) | ||
| if self.os_type in ("linux", "darwin"): | ||
| chown_args = ["chown", f"{self.influx['uid']}:{self.influx['gid']}", str(self.influx["key"])] | ||
| subprocess.run(chown_args, check=True) | ||
| return True, ( | ||
| f"{self.influx['key']} created and ownership set to " | ||
| f"{self.influx['uid']}:{self.influx['gid']}.") | ||
| except OSError as e: | ||
| return False, f"Error setting permissions for InfluxDB: {e}" | ||
| except subprocess.CalledProcessError as e: | ||
| return False, f"Failed to set ownership: {str(e)}" | ||
|
|
||
|
|
||
| def permissions_rabbitmq(self) -> Tuple[bool, str]: | ||
| """Copy privkey.pem -> privkey-rabbitmq.pem and sets owner.""" | ||
| try: | ||
| shutil.copy2(self.certs["privkey"], self.rabbitmq["key"]) | ||
| if self.os_type in ("linux", "darwin"): | ||
| chown_args = ["chown", f"{self.rabbitmq['uid']}", str(self.rabbitmq["key"])] | ||
| subprocess.run(chown_args, check=True) | ||
| return True, (f"{self.rabbitmq['key']} created and ownership set to user " | ||
| f"{self.rabbitmq['uid']}.") | ||
| except OSError as e: | ||
| return False, f"Error setting permissions for RabbitMQ: {e}" | ||
| except subprocess.CalledProcessError as e: | ||
| return False, f"Failed to set ownership: {str(e)}" | ||
|
|
||
|
|
||
| def start_docker_compose(self) -> Tuple[bool, str]: | ||
| """Start the platform services using docker compose.""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["docker", "compose", "-f", str(self.compose_file), "up", "-d"], | ||
| check=True, | ||
| capture_output=True, | ||
| text=True) | ||
| return True, f"Docker Compose started successfully:\n{result.stdout}" | ||
| except OSError as e: | ||
| return False, f"Error starting Docker Compose: {e}" | ||
| except subprocess.CalledProcessError as e: | ||
| return False, f"Failed to start Docker Compose: {str(e)}" | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| cfg = ServicesConfig() | ||
| steps = [ | ||
| cfg.copy_certs, | ||
| cfg.permissions_mongodb, | ||
| cfg.permissions_influxdb, | ||
| cfg.permissions_rabbitmq, | ||
| cfg.start_docker_compose, | ||
| ] | ||
| for step in steps: | ||
| ok, msg = step() | ||
| if not ok: | ||
| print(f"ERROR: {msg}", file=sys.stderr) | ||
| sys.exit(1) | ||
| else: | ||
| print(f"OK: {msg}") | ||
| sys.exit(0) | ||
8ohamed marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.