mirror of
https://github.com/optim-enterprises-bv/openlan-cgw.git
synced 2025-10-30 01:42:20 +00:00
394 lines
15 KiB
Python
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)
|