Files
openlan-cgw/utils/docker/StartMultiCGW.py
2024-12-05 14:22:21 +02:00

394 lines
15 KiB
Python

import argparse
import os
import subprocess
import shutil
from typing import Final
from jinja2 import Environment, FileSystemLoader
# CGW Docker image & container params
DEFAULT_NUMBER_OF_CGW_INSTANCES: Final[int] = 1
DOCKER_COMPOSE_TEMPLATE_FILE_NAME: Final[str] = "docker-compose-template.yml.j2"
DOCKER_COMPOSE_MULTI_CGW_FILE_NAME: Final[str] = "docker-compose-multi-cgw.yml"
CGW_IMAGE_BASE_NAME: Final[str] = "openlan-cgw-img"
CGW_CONTAINER_BASE_NAME: Final[str] = "openlan_cgw"
# CGW params
DEFAULT_CGW_BASE_ID: Final[int] = 0
DEFAULT_LOG_LEVEL: Final[str] ="debug"
# CGW groups & group infras params
DEFAULT_GROUPS_CAPACITY: Final[int] = 1000
DEFAULT_GROUPS_THRESHOLD: Final[int] = 50
DEFAULT_GROUP_INFRAS_CAPACITY: Final[int] = 2000
# GRPC params
DEFAULT_GRPC_LISTENING_IP: Final[str] = "0.0.0.0"
DEFAULT_GRPC_LISTENING_BASE_PORT: Final[int] = 50051
DEFAULT_GRPC_PUBLIC_BASE_PORT: Final[int] = 50051
DEFAULT_GRPC_PUBLIC_HOST: Final[str] ="openlan_cgw"
# WSS params
DEFAULT_WSS_IP: Final[str] = "0.0.0.0"
DEFAULT_WSS_BASE_PORT: Final[int] = 15002
DEFAULT_WSS_T_NUM: Final[int] = 4
DEFAULT_WSS_CAS: Final[str] = "cas.pem"
DEFAULT_WSS_CERT: Final[str] = "cert.pem"
DEFAULT_WSS_KEY: Final[str] = "key.pem"
# Kafka params
DEFAULT_KAFKA_HOST: Final[str] = "docker-broker-1"
DEFAULT_KAFKA_PORT: Final[int] = 9092
DEFAULT_KAFKA_CONSUME_TOPIC: Final[str] = "CnC"
DEFAULT_KAFKA_PRODUCE_TOPIC: Final[str] = "CnC_Res"
# DB params
DEFAULT_DB_HOST: Final[str] = "docker-postgresql-1"
DEFAULT_DB_PORT: Final[int] = 5432
DEFAULT_DB_NAME: Final[str] = "cgw"
DEFAULT_DB_USER: Final[str] = "cgw"
DEFAULT_DB_PASW: Final[str] = "123"
DEFAULT_DB_TLS: Final[str] = "no"
# Redis params
DEFAULT_REDIS_HOST: Final[str] = "docker-redis-1"
DEFAULT_REDIS_PORT: Final[int] = 6379
DEFAULT_REDIS_TLS: Final[str] = "no"
DEFAULT_REDIS_USERNAME: Final[str] = ""
DEFAULT_REDIS_PASSWORD: Final[str] = ""
# Metrics params
DEFAULT_METRICS_BASE_PORT: Final[int] = 8080
# TLS params: cert volumes
DEFAULT_CERTS_PATH="../cert_generator/certs/server/"
DEFAULT_CLIENT_CERTS_PATH="../cert_generator/certs/client/"
CONTAINTER_CERTS_VOLUME: Final[str] = "/etc/cgw/certs"
CONTAINTER_NB_INFRA_CERTS_VOLUME: Final[str] = "/etc/cgw/nb_infra/certs"
# Cert & key files name
DEFAULT_CERT_GENERATOR_PATH="../cert_generator"
DEFAULT_WSS_CAS="cas.pem"
DEFAULT_WSS_CERT="cert.pem"
DEFAULT_WSS_KEY="key.pem"
DEFAULT_CLIENT_CERT="base.crt"
DEFAULT_CLIENT_KEY="base.key"
# TLS params
DEFAULT_NB_INFRA_TLS: Final[str] = "no"
DEFAULT_ALLOW_CERT_MISMATCH: Final[str] = "yes"
# UCentral params
DEFAULT_UCENTRAL_AP_DATAMODEL_URI: Final[str] = "https://raw.githubusercontent.com/Telecominfraproject/wlan-ucentral-schema/main/ucentral.schema.json"
DEFAULT_UCENTRAL_SWITCH_DATAMODEL_URI: Final[str] = "https://raw.githubusercontent.com/Telecominfraproject/ols-ucentral-schema/main/ucentral.schema.json"
def get_realpath(base_path) -> str:
"""
Get absolute path from base
"""
return str(os.path.realpath(base_path))
# Certificates update
def certificates_update(certs_path: str = DEFAULT_CERTS_PATH, client_certs_path: str = DEFAULT_CLIENT_CERTS_PATH):
"""
Generate server & client certificates
"""
missing_files = any(
not os.path.isfile(os.path.join(certs_path, file))
for file in [DEFAULT_WSS_CERT, DEFAULT_WSS_KEY, DEFAULT_WSS_CAS]
) or any (
not os.path.isfile(os.path.join(client_certs_path, file))
for file in [DEFAULT_CLIENT_CERT, DEFAULT_CLIENT_KEY]
)
if missing_files:
print(f"WARNING: At specified path {certs_path}, either CAS, CERT, or KEY is missing!")
print(f"WARNING: Changing source folder for certificates to default: {client_certs_path} and generating self-signed...")
cert_gen_path = get_realpath(DEFAULT_CERT_GENERATOR_PATH)
# Clean up old certificates
cert_subfolders = ["ca", "server", "client"]
for subfolder in cert_subfolders:
cert_folder = os.path.join(cert_gen_path, "certs", subfolder)
for file in os.listdir(cert_folder):
if file.endswith((".crt", ".key")):
os.remove(os.path.join(cert_folder, file))
# Generate new certificates
try:
# Save current working directory
original_dir = os.getcwd()
os.chdir(cert_gen_path)
print(f"Changed directory to: {os.getcwd()}")
cert_gen_script = "./generate_certs.sh"
subprocess.run([cert_gen_script, "-a"], check=True)
subprocess.run([cert_gen_script, "-s"], check=True)
subprocess.run([cert_gen_script, "-c", "1", "-m", "02:00:00:00:00:00"], check=True)
# Copy generated certificates to default paths
shutil.copy(os.path.join(cert_gen_path, "certs", "ca", "ca.crt"), os.path.join(DEFAULT_CERTS_PATH, DEFAULT_WSS_CAS))
shutil.copy(os.path.join(cert_gen_path, "certs", "server", "gw.crt"), os.path.join(DEFAULT_CERTS_PATH, DEFAULT_WSS_CERT))
shutil.copy(os.path.join(cert_gen_path, "certs", "server", "gw.key"), os.path.join(DEFAULT_CERTS_PATH, DEFAULT_WSS_KEY))
for client_file in os.listdir(os.path.join(cert_gen_path, "certs", "client")):
if client_file.endswith(".crt"):
shutil.copy(
os.path.join(cert_gen_path, "certs", "client", client_file),
os.path.join(DEFAULT_CLIENT_CERTS_PATH, DEFAULT_CLIENT_CERT)
)
elif client_file.endswith(".key"):
shutil.copy(
os.path.join(cert_gen_path, "certs", "client", client_file),
os.path.join(DEFAULT_CLIENT_CERTS_PATH, DEFAULT_CLIENT_KEY)
)
print("Generating self-signed certificates done!")
except subprocess.CalledProcessError as e:
print(f"Error while generating certificates: {e}")
finally:
# Change back to the original directory
os.chdir(original_dir)
print(f"Returned to original directory: {os.getcwd()}")
# Jinga2 template generator
def get_cgw_image_base_name() -> str:
"""
Returns CGW Docker image base name
"""
return CGW_IMAGE_BASE_NAME
def get_cgw_image_tag() -> str:
"""
Returns CGW Docker image tag
"""
tag = None
try:
# Check if there are any uncommitted changes (ignoring untracked files)
status_output = subprocess.run(
["git", "status", "--porcelain", "--untracked-files=no"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
).stdout.strip()
# Get the short commit hash
commit_hash = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True
).stdout.strip()
# Append '-dirty' if there are uncommitted changes
if status_output:
tag = f"{commit_hash}-dirty"
else:
tag = commit_hash
except subprocess.CalledProcessError as e:
print(f"Error: {e.stderr.strip()}")
return tag
def get_cgw_container_base_name() -> str:
"""
Returns CGW Docker container base name
"""
return CGW_CONTAINER_BASE_NAME
def get_cgw_instances_num() -> int:
"""
Returns CGW instances number from env. variable,
or default value "DEFAULT_NUMBER_OF_CGW_INSTANCES"
"""
# Number of clients from an environment variable or fallback to default
number_of_cgw_instances = int(os.getenv("CGW_INSTANCES_NUM", DEFAULT_NUMBER_OF_CGW_INSTANCES))
return number_of_cgw_instances
def remove_docker_compose_multi_cgw_file(docker_compose_multi_cgw_file: str = DOCKER_COMPOSE_MULTI_CGW_FILE_NAME) -> int:
"""
Remove "docker-compose-multi-cgw.yml" file
"""
if os.path.isfile(docker_compose_multi_cgw_file):
try:
os.remove(docker_compose_multi_cgw_file)
except Exception as e:
print(f"Error: Filed to remove file {docker_compose_multi_cgw_file}! Error: {e}")
def generate_docker_compose_file(instances_num: int,
docker_compose_template_file: str = DOCKER_COMPOSE_TEMPLATE_FILE_NAME,
docker_compose_multi_cgw_file: str = DOCKER_COMPOSE_MULTI_CGW_FILE_NAME):
"""
Generate docker compose file based on template
"""
# 1. Get CGW image name
image_name = get_cgw_image_base_name()
# 2. Get CGW image tag
image_tag = get_cgw_image_tag()
# 3. Get CGW container name
container_name = get_cgw_container_base_name()
# 4. Get certs realpath
certs_realpath = get_realpath(DEFAULT_CERTS_PATH)
print(f'Generate Docker Compose file!')
print(f'\tNumber of CGW instances: {instances_num}')
print(f'\tCGW image name : {image_name}')
print(f'\tCGW image tag : {image_tag}')
print(f'\tCGW container name : {container_name}')
# 4. Load the Jinja2 template
env = Environment(loader=FileSystemLoader(searchpath="."))
template = env.get_template(docker_compose_template_file)
# 5. Render the template with the variable
output = template.render(cgw_instances_num = instances_num,
cgw_image_name = image_name,
cgw_image_tag = image_tag,
cgw_container_name = container_name,
cgw_base_id = DEFAULT_CGW_BASE_ID,
cgw_grpc_listening_ip = DEFAULT_GRPC_LISTENING_IP,
cgw_grpc_listening_base_port = DEFAULT_GRPC_LISTENING_BASE_PORT,
cgw_grpc_public_host = DEFAULT_GRPC_PUBLIC_HOST,
cgw_grpc_public_base_port = DEFAULT_GRPC_PUBLIC_BASE_PORT,
cgw_db_host = DEFAULT_DB_HOST,
cgw_db_port = DEFAULT_DB_PORT,
cgw_db_name = DEFAULT_DB_NAME,
cgw_db_username = DEFAULT_DB_USER,
cgw_db_password = DEFAULT_DB_PASW,
cgw_db_tls = DEFAULT_DB_TLS,
cgw_kafka_host = DEFAULT_KAFKA_HOST,
cgw_kafka_port = DEFAULT_KAFKA_PORT,
cgw_kafka_consumer_topic = DEFAULT_KAFKA_CONSUME_TOPIC,
cgw_kafka_producer_topic = DEFAULT_KAFKA_PRODUCE_TOPIC,
cgw_log_level = DEFAULT_LOG_LEVEL,
cgw_redis_host = DEFAULT_REDIS_HOST,
cgw_redis_port = DEFAULT_REDIS_PORT,
cgw_redis_tls = DEFAULT_REDIS_TLS,
cgw_redis_username = DEFAULT_REDIS_USERNAME,
cgw_redis_password = DEFAULT_REDIS_PASSWORD,
cgw_metrics_base_port = DEFAULT_METRICS_BASE_PORT,
cgw_wss_ip = DEFAULT_WSS_IP,
cgw_wss_base_port = DEFAULT_WSS_BASE_PORT,
cgw_wss_cas = DEFAULT_WSS_CAS,
cgw_wss_cert = DEFAULT_WSS_CERT,
cgw_wss_key = DEFAULT_WSS_KEY,
cgw_wss_t_num = DEFAULT_WSS_T_NUM,
cgw_ucentral_ap_datamodel_uri = DEFAULT_UCENTRAL_AP_DATAMODEL_URI,
cgw_ucentral_switch_datamodel_uri = DEFAULT_UCENTRAL_SWITCH_DATAMODEL_URI,
cgw_groups_capacity = DEFAULT_GROUPS_CAPACITY,
cgw_groups_threshold = DEFAULT_GROUPS_THRESHOLD,
cgw_group_infras_capacity = DEFAULT_GROUP_INFRAS_CAPACITY,
cgw_allow_certs_missmatch = DEFAULT_ALLOW_CERT_MISMATCH,
cgw_nb_infra_tls = DEFAULT_NB_INFRA_TLS,
container_certs_voulume = CONTAINTER_CERTS_VOLUME,
container_nb_infra_certs_voulume = CONTAINTER_NB_INFRA_CERTS_VOLUME,
default_certs_path = certs_realpath)
# 6. Save the rendered template as docker-compose.yml
with open(docker_compose_multi_cgw_file, "w") as f:
f.write(output)
def docker_compose_up(docker_compose_file: str = "docker-compose.yml"):
"""
Runs `docker compose up` with the specified docker-compose file.
:param compose_file: Path to the docker-compose file (optional).
"""
if docker_compose_file:
if not os.path.isfile(docker_compose_file):
print(f"Error: The specified compose file '{docker_compose_file}' does not exist.")
return
cmd = ["docker", "compose", "--file", docker_compose_file, "up", "-d"]
else:
cmd = ["docker", "compose", "up", "-d"]
try:
print(f"Running command: {' '.join(cmd)}")
subprocess.run(cmd, check=True)
print("Docker Compose started successfully.")
except subprocess.CalledProcessError as e:
print(f"Error: Failed to run docker compose up. {e}")
def docker_compose_down(docker_compose_file: str = "docker-compose.yml"):
"""
Runs `docker compose down` with the specified docker-compose file.
:param compose_file: Path to the docker-compose file (optional).
"""
if docker_compose_file:
if not os.path.isfile(docker_compose_file):
print(f"The specified compose file '{docker_compose_file}' does not exist.")
return
cmd = ["docker", "compose", "--file", docker_compose_file, "down"]
else:
cmd = ["docker", "compose", "down"]
try:
print(f"Running command: {' '.join(cmd)}")
subprocess.run(cmd, check=True)
print("Docker Compose stopped successfully.")
except subprocess.CalledProcessError as e:
print(f"Error: Failed to run docker compose down. {e}")
if __name__ == "__main__":
# Create the parser
parser = argparse.ArgumentParser(description="Demo application to parse arguments.")
# Add arguments
parser.add_argument("--start", action="store_true", help="Stop all Docker Composes. Clean up and generate new compose file. Start Docker Compose.")
parser.add_argument("--stop", action="store_true", help="Stop all Docker Composes.")
parser.add_argument("--generate-compose", action="store_true", help="Generate new Docker Compose file.")
# Parse the arguments
args = parser.parse_args()
if args.start or args.stop:
# 1. Try to stop default docker compose
docker_compose_down()
# 2. Try to stop multi cgw docker compose
docker_compose_down(DOCKER_COMPOSE_MULTI_CGW_FILE_NAME)
if args.start or args.generate_compose:
# 3. Remove old multi cgw docker compose file
remove_docker_compose_multi_cgw_file()
# 4. Update Certitifacates
certificates_update()
# 4. Generate new multi cgw docker compose file
generate_docker_compose_file(get_cgw_instances_num())
if args.start:
# 5. Try to start multi cgw docker compose
docker_compose_up(DOCKER_COMPOSE_MULTI_CGW_FILE_NAME)