Skip to content
Merged
Show file tree
Hide file tree
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 Nov 16, 2025
8968d0c
test utils updated
8ohamed Nov 16, 2025
71a6250
added limits to compose files
8ohamed Nov 18, 2025
3e91a9e
added cpuset + resolved issues
8ohamed Nov 18, 2025
337b661
test issue
8ohamed Nov 18, 2025
ae7a39e
removed cpuset
8ohamed Nov 19, 2025
72a0dff
remove cpuset
8ohamed Nov 19, 2025
1166023
applies snake_case to tests
8ohamed Nov 19, 2025
a9311a6
resolves sonar issues
8ohamed Nov 19, 2025
a34b554
minor updates
8ohamed Nov 19, 2025
819e502
update pid limit in compose
8ohamed Nov 19, 2025
9b15119
changed pids_limit back
8ohamed Nov 20, 2025
d4e99f0
initial commit
8ohamed Nov 28, 2025
9a2c8cf
Merge branch 'INTO-CPS-Association:feature/distributed-demo' into fea…
8ohamed Nov 28, 2025
5d12713
test issue
8ohamed Nov 18, 2025
b9abf46
applies snake_case to tests
8ohamed Nov 19, 2025
ce3ff7e
resolves sonar issues
8ohamed Nov 19, 2025
f2efa23
minor updates
8ohamed Nov 19, 2025
47f3885
update pid limit in compose
8ohamed Nov 19, 2025
8e87d19
changed pids_limit back
8ohamed Nov 20, 2025
f6cc63a
initial commit
8ohamed Nov 28, 2025
5d15586
Merge branch 'feature/distributed-demo' of https://github.com/8ohamed…
8ohamed Nov 29, 2025
d76b287
minor fix
8ohamed Nov 29, 2025
6595acf
removed user arg
8ohamed Nov 30, 2025
3b4f545
refactors service_setup, updates readme
8ohamed Nov 30, 2025
0380b11
applies coding suggestions
8ohamed Nov 30, 2025
1317cc9
Handles windows OS
8ohamed Dec 1, 2025
3542676
applying suggestions
8ohamed Dec 1, 2025
4c9587f
applying suggestions
8ohamed Dec 1, 2025
dc2e4a5
minor updates
8ohamed Dec 1, 2025
ead2bb4
minor updates
8ohamed Dec 1, 2025
a85da0c
fixes bugs
8ohamed Dec 2, 2025
642a266
minor update
8ohamed Dec 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 43 additions & 58 deletions deploy/services/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,64 +23,49 @@ The following services can be installed:
## Installation steps

Please follow the steps outlined here for installation.
The `services.foo.com` website hostname is used for illustration.
Please replace the same with your server's hostname.

* Obtain the TLS certificates from letsencrypt and copy them.

```bash
cp -R /etc/letsencrypt/archive/services.foo.com certs/.
mv certs/services.foo.com/privkey1.pem certs/services.foo.com/privkey.pem
mv certs/services.foo.com/fullchain1.pem certs/services.foo.com/fullchain.pem
```

* Combine and adjust permissions of certificates for MongoDB user
in docker container.

```bash
cat certs/services.foo.com/privkey.pem \
certs/services.foo.com/fullchain.pem > certs/foo.com/combined.pem
chmod 600 certs/services.foo.com/combined.pem
chown 999:999 certs/services.foo.com/combined.pem
```

* Adjust permissions of certificates for InfluxDB user in docker container.

```bash
cp certs/services.foo.com/privkey.pem \
certs/services.foo.com/privkey-influxdb.pem
chown 1000:1000 certs/services.foo.com/privkey-influxdb.pem
```

* Adjust permissions of certificates for RabbitMQ user in docker container.

```bash
cp certs/services.foo.com/privkey.pem certs/services.foo.com/privkey-rabbitmq.pem
chown 999 certs/services.foo.com/privkey-rabbitmq.pem
```

* Note down your userid and groupid on Linux systems.

```bash
$id -u #outputs userid
$id -g #outputs groupid
```

* Use configuration template and create service configuration.
Remember to update the services.env file with the appropriate values.

```bash
cp config/services.env.template config/services.env
```

* Start or stop services.

```bash
docker compose -f compose.services.secure.yml \
--env-file config/services.env up -d
docker compose -f compose.services.secure.yml \
--env-file config/services.env down
```
`script/service_setup.py`, is provided to streamline the setup of TLS certificates
and permissions for MongoDB, InfluxDB, and RabbitMQ services.

The script has the following features:

* **Automation:** Automates all manual certificate and permission steps for
MongoDB, InfluxDB, and RabbitMQ as described above.
* **Cross-platform:** Works on Linux, macOS, and Windows.
* **Configuration-driven:** Reads all required user IDs, group IDs, and hostnames
from `config/services.env`.

### Create Config

1. Copy `config/services.env.template` into `config/services.env`.
2. Update `config/services.env` with the correct values for your environment.
3. Run the script with root privilege.

### Install

Install Python dependencies before running the script:

```bash
pip install -r script/requirements.txt
```

Run the installation script

```bash
cd deploy/services
sudo python3 script/service_setup.py
```

The script will:

* Combine and set permissions for MongoDB certificates.
* Copy and set permissions for InfluxDB and RabbitMQ certificates.
* Use the correct UID/GID values from `config/services.env`.
* Start the Docker Compose services automatically after setup.

If any required variable is missing, the script will exit with an error message.

This automation reduces manual errors and ensures your service containers have
the correct certificate files and permissions for secure operation.

## Use

Expand Down
9 changes: 9 additions & 0 deletions deploy/services/config/services.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ HOSTNAME=services.foo.com
USERID=0
GROUPID=0

# Certs path on Linux / MacOS
CERTS_SRC='/etc/letsencrypt/archive/services.foo.com'
# Certs path on Windows
#CERTS_SRC='C:/Certbot/archive/services.foo.com'

# RabbitMQ settings
RABBITMQ_PORT=8083
RABBITMQ_MANAGEMENT_PORT=8084
RABBITMQ_MQTT_PORT=8085
RABBITMQ_ADMIN_USERNAME=dtaas
RABBITMQ_ADMIN_PASSWORD=dtaas
RABBIT_UID=999

# InfluxDB settings
INFLUXDB_PORT=8086
Expand All @@ -18,11 +23,15 @@ INFLUXDB_ADMIN_USERNAME=dtaas
INFLUXDB_ADMIN_PASSWORD=dtaas1357
INFLUXDB_ORG=dtaas
INFLUXDB_BUCKET=dtaas
INFLUX_UID=1000
INFLUX_GID=1000

# MongoDB settings
MONGODB_PORT=8087
MONGODB_ADMIN_USERNAME='dtaas123'
MONGODB_ADMIN_PASSWORD='XaphrDKDTaaS2025'
MONGO_UID=999
MONGO_GID=999

# Grafana settings
GRAFANA_PORT=8088
Expand Down
1 change: 1 addition & 0 deletions deploy/services/script/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python-dotenv>=1.0.0,<2.0.0
224 changes: 224 additions & 0 deletions deploy/services/script/service_setup.py
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

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()
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")
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()


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."""
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)