diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..15be366 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM python:latest + +WORKDIR /usr/local/bin +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + +COPY src/start.py . + +CMD ["src/start.py"] \ No newline at end of file diff --git a/config/inventory.yml b/config/inventory.yml new file mode 100644 index 0000000..6433930 --- /dev/null +++ b/config/inventory.yml @@ -0,0 +1,8 @@ +- 1: + hostname: '172.16.57.2' + username: 'admin' + password: 'Netapp12' +- 2: + hostname: '172.16.56.2' + username: 'admin' + password: 'Netapp12' diff --git a/initialize.py b/initialize.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt index 8af5e42..5935683 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ fastapi[standard]>=0.116.2 httpx>=0.28.1 redis>=6.4.0 -python-dotenv>=1.1.1 \ No newline at end of file +python-dotenv>=1.1.1 +pydantic +redis[hiredis] +dotenv diff --git a/src/database.py b/src/database.py new file mode 100644 index 0000000..a278d98 --- /dev/null +++ b/src/database.py @@ -0,0 +1,40 @@ +import json +import logging +from redis import Redis, ConnectionError +from typing import List +from pydantic import TypeAdapter +from schema import ConfigSchema + + +def setup_db_conn(redishost, redisport: str): + ''' Setup Redis connection and return it open''' + log = logging.getLogger('uvicorn') + try: + redisclient = Redis(host=redishost, port=redisport, decode_responses=True) + if redisclient.ping(): + log.info(f"Connected to Redis DB {redishost} on port {redisport}") + else: + log.error(f"Cannot connect to Redis DB {redishost} on port {redisport}") + exit(1) + return redisclient + except ConnectionError as e: + print(f"FATAL: Redis DB {redishost} is unreachable on port {redisport}. Err: {e}") + return None + except Exception as e: + print(f"FATAL: {e}") + return None + +def get_inventory_from_redis(redisclient: Redis): + ''' Read inventory from Redis ''' + cluster_inv = redisclient.hgetall('cluster_inventory') + if 'inventory' in cluster_inv: + return json.loads(cluster_inv['inventory']) + return {} + +def get_config_from_db(redisclient: Redis) -> ConfigSchema: + ''' Load inventory to global vars''' + GLOBAL_INVENTORY = get_inventory_from_redis(redisclient) + + GLOBAL_INVENTORY_VALID = TypeAdapter(List[ConfigSchema]).validate_python(GLOBAL_INVENTORY) + + return GLOBAL_INVENTORY_VALID diff --git a/src/example/schema.py b/src/example/schema.py index 47392d6..d0ae38a 100644 --- a/src/example/schema.py +++ b/src/example/schema.py @@ -5,3 +5,11 @@ from pydantic import BaseModel class ExampleSchema(BaseModel): example_field: str another_field: int + +class ClusterCreds(BaseModel): + """A structure to hold basic auth cluster credentials for a cluster""" + username: str + password: str + hostname: str = None + cert_filepath: Path = None + key_filepath: Path = None diff --git a/src/initialize.py b/src/initialize.py new file mode 100644 index 0000000..dcdd84d --- /dev/null +++ b/src/initialize.py @@ -0,0 +1,44 @@ +import os +import json +import logging +import yaml + +from pathlib import Path +from dotenv import load_dotenv +from database import setup_db_conn +from schema import ConfigSchema +from typing import List +from pydantic import TypeAdapter + +def initialize_config(): + load_dotenv() + log = logging.getLogger('uvicorn') + ENV_INVENTORYPATH = os.getenv('cluster_inventory_path') + ENV_REDISHOST = os.getenv('redis_host') + ENV_REDISPORT = os.getenv('redis_port') + + log.info(f"Found Cluster Inventory file at: {ENV_INVENTORYPATH}") + if not ENV_INVENTORYPATH or not Path(ENV_INVENTORYPATH).is_file(): + print(f"FATAL: Inventory file {ENV_INVENTORYPATH} is missing or not a file.") + return False + try: + with open(ENV_INVENTORYPATH, 'r') as f: + inv = yaml.safe_load(f) + inventory = json.dumps(inv) + except Exception as e: + print(f"FATAL: Cannot read inventory file {ENV_INVENTORYPATH}. Err: {e}") + return False + + print(f'[INFO] Importing configuration to DB...') + try: + GLOBAL_INVENTORY_VALID = TypeAdapter(List[ConfigSchema]).validate_python(inv) + redis_conn = setup_db_conn(ENV_REDISHOST, ENV_REDISPORT) + redis_conn.hset('cluster_inventory', mapping={'inventory': inventory}) + redis_conn.close() + + log.info("Configuration has been loaded.") + return True + + except Exception as e: + print(f"FATAL: Redis DB error: {e}") + return False \ No newline at end of file diff --git a/src/main.py b/src/main.py index ea4abdf..bec035d 100644 --- a/src/main.py +++ b/src/main.py @@ -1,35 +1,46 @@ +import os +import json import logging +import yaml -from fastapi import FastAPI +from pathlib import Path +from dotenv import load_dotenv +from redis import Redis from contextlib import asynccontextmanager -import httpx -from src.aggregate import aggregate_router -from src.service import load_config +from pydantic import BaseModel, ValidationError, SecretStr, AnyHttpUrl +from typing import Optional, Literal, List, Union +from fastapi import FastAPI -logger = logging.getLogger("uvicorn") - -logger.info("Starting application") -config = load_config() +from database import setup_db_conn, get_inventory_from_redis, get_config_from_db +from src.initialize import initialize_config +from utils import setup_logging + @asynccontextmanager async def lifespan(app: FastAPI): - app.requests_client = httpx.AsyncClient(verify=False) + ''' make loading it async''' + log = logging.getLogger('uvicorn') + cfg_init_result = initialize_config() + + shared_redis_conn = setup_db_conn(os.getenv('redis_host'), os.getenv('redis_port')) + if not shared_redis_conn: + log.error("Cannot connect to Redis DB. Exiting...") + exit(1) + + inv_check = get_config_from_db(shared_redis_conn) + log.info(f"[DEBUG] Data validity healthcheck (DEVELOPER MODE): {inv_check}") + if not cfg_init_result: + log.error("Configuration initialization failed. Exiting...") + # exit(1) + yield - await app.requests_client.aclose() + log.info("Shutting down FastAPI app...") +setup_logging() +log = logging.getLogger('uvicorn') + +log.info("Starting FastAPI app...") app = FastAPI(lifespan=lifespan) -app.include_router(aggregate_router) - - -@app.get("/") -async def main(): - return {"Hello": "World"} - - -@app.get("/config") -async def get_config(): - """Endpoint to get the current configuration.""" - return config.model_dump() diff --git a/src/utils.py b/src/utils.py index febeea5..8ab9e69 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,3 +1,4 @@ +import logging import httpx def round_bytes(size_in_bytes: int) -> str: @@ -22,3 +23,11 @@ async def get_data_from_ontap(client, logger, hostname: str, username: str, pass except httpx.HTTPError as e: logger.error(f"HTTP error occurred: {e}") return None + +def setup_logging() -> None: + """Configure logging for the application""" + logging.basicConfig( + level=logging.DEBUG, + format="[%(asctime)s] [%(levelname)5s] %(message)s" + ) + print(f"Logger is initialized.")