mirror of
				https://github.com/optim-enterprises-bv/openlan-cgw.git
				synced 2025-10-31 18:27:46 +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)
 | 
