feature/config_upload #5

Merged
pascal merged 7 commits from feature/config_upload into main 2025-09-18 12:34:49 +00:00
15 changed files with 121 additions and 57 deletions

11
.env
View File

@@ -1,8 +1,3 @@
# Environment variables for NetApp ONTAP clusters
CLUSTER1_HOSTNAME=172.16.57.2
CLUSTER1_USERNAME=admin
CLUSTER1_PASSWORD=Netapp12
CLUSTER2_HOSTNAME=172.16.56.2
CLUSTER2_USERNAME=admin
CLUSTER2_PASSWORD=Netapp12
cluster_inventory_path = config/inventory.yml
redis_host = '172.16.0.208'
redis_port = '6379'

View File

@@ -1,8 +1,6 @@
- 1:
hostname: '172.16.57.2'
username: 'admin'
password: 'Netapp12'
- 2:
hostname: '172.16.56.2'
username: 'admin'
password: 'Netapp12'
- hostname: "172.16.57.2"
username: "admin"
password: "Netapp12"
- hostname: "172.16.56.2"
username: "admin"
password: "Netapp12"

View File

@@ -1,4 +1,5 @@
from src.example.router import router as example_router
from .aggregate_router import router as aggregate_router
from src.aggregate.aggregate_router import router as aggregate_router
__all__ = ["example_router", "aggregate_router"]

View File

@@ -3,9 +3,9 @@
from typing import List
from fastapi import Request
from .aggregate_schema import AggregateSchema, MetricEnum
from src.aggregate.aggregate_schema import AggregateSchema, MetricEnum
from logging import getLogger
from ..utils import round_bytes, get_data_from_ontap
from src.utils import round_bytes, get_data_from_ontap
logger = getLogger("uvicorn")
logger.setLevel("DEBUG")

3
src/config/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from src.config.router import router as config_router
__all__ = ["config_router"]

14
src/config/config.http Normal file
View File

@@ -0,0 +1,14 @@
POST http://127.0.0.1:8000/config
Content-Type: application/json
{
"cluster_list": [
{
"hostname": "cluster1.demo.netapp.com",
"username": "admin",
"password": "Netapp1!"
}
]
}
###

22
src/config/router.py Normal file
View File

@@ -0,0 +1,22 @@
import logging
from fastapi import APIRouter
from .schema import ConfigReturnSchema, ConfigSchema
logger = logging.getLogger("uvicorn")
router = APIRouter(tags=["config"])
@router.post(
"/config", summary="Upload a configuration", response_model=ConfigReturnSchema
)
async def create_config(config: ConfigSchema) -> ConfigSchema:
"""
Endpoint to receive and store configuration data.
⚠️ at this time the configuration is not stored anywhere. It's like logging to /dev/null
"""
logger.info("Received configuration data")
return config

21
src/config/schema.py Normal file
View File

@@ -0,0 +1,21 @@
# contains the schema definitions for the config service
from pydantic import BaseModel
class ConfigEntrySchema(BaseModel):
hostname: str
username: str
password: str
class ConfigOutSchema(BaseModel):
hostname: str
username: str
class ConfigReturnSchema(BaseModel):
cluster_list: list[ConfigOutSchema]
class ConfigSchema(BaseModel):
cluster_list: list[ConfigEntrySchema]

2
src/config/service.py Normal file
View File

@@ -0,0 +1,2 @@
# contains the business logic for the config service
async def save_config() -> None: ...

View File

@@ -3,7 +3,7 @@ import logging
from redis import Redis, ConnectionError
from typing import List
from pydantic import TypeAdapter
from schema import ConfigSchema
from src.schema import ConfigSchema
def setup_db_conn(redishost, redisport: str):

View File

@@ -1,6 +1,7 @@
# contains the router for the aggregate endpoint
from fastapi import APIRouter
from .schema import ExampleSchema
from src.example.schema import ExampleSchema
router = APIRouter(tags=["aggregate"])

View File

@@ -1,15 +1,18 @@
# contains the schema definitions for the aggregate service
from pydantic import BaseModel
from pathlib import Path
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
username: str
password: str
hostname: str = None
cert_filepath: Path = None
key_filepath: Path = None
key_filepath: Path = None

View File

@@ -5,40 +5,41 @@ import yaml
from pathlib import Path
from dotenv import load_dotenv
from database import setup_db_conn
from schema import ConfigSchema
from src.database import setup_db_conn
from src.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 = 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:
with open(ENV_INVENTORYPATH, "r") as f:
inv = yaml.safe_load(f)
inventory = json.dumps(inv)
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...')
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.hset("cluster_inventory", mapping={"inventory": inventory})
redis_conn.close()
log.info("Configuration has been loaded.")
log.info("Configuration has been loaded.")
return True
except Exception as e:
except Exception as e:
print(f"FATAL: Redis DB error: {e}")
return False
return False

View File

@@ -1,30 +1,32 @@
import os
import json
import logging
import yaml
from pathlib import Path
from dotenv import load_dotenv
from redis import Redis
from contextlib import asynccontextmanager
from pydantic import BaseModel, ValidationError, SecretStr, AnyHttpUrl
from typing import Optional, Literal, List, Union
from fastapi import FastAPI
from src.aggregate import aggregate_router
from src.config import config_router
from contextlib import asynccontextmanager
from .database import setup_db_conn, get_config_from_db
from src.initialize import initialize_config
from .utils import setup_logging
logger = logging.getLogger("uvicorn")
logger.info("Starting application")
app = FastAPI()
app.include_router(aggregate_router)
app.include_router(config_router)
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):
''' make loading it async'''
log = logging.getLogger('uvicorn')
"""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'))
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)
@@ -40,7 +42,7 @@ async def lifespan(app: FastAPI):
setup_logging()
log = logging.getLogger('uvicorn')
log = logging.getLogger("uvicorn")
log.info("Starting FastAPI app...")
app = FastAPI(lifespan=lifespan)

View File

@@ -1,6 +1,7 @@
from dotenv import dotenv_values
import logging
from dotenv import dotenv_values
from src.schema import ConfigSchema
logger = logging.getLogger("uvicorn")