from flask import Flask, jsonify, request from flask_apscheduler import APScheduler from typing import Dict, Union import json, time, redis, yaml import base64, hashlib from Components import * from Cosmos_Settings import * # declare flask apps app = Flask(__name__) scheduler = APScheduler() ####################################################################### ### Redis Functions ####################################################################### # Redis client - will publish updates r = redis.Redis(host=service_gateway_ip(), port=6379) def update_redis_channel(redis_channel, data): # Publish to the specified Redis channel r.publish(redis_channel, json.dumps(data)) log_data(log_output = data, log_level = "noisy_test") def update_redis_server(): # Client Redis Tree if not run_cosmostat_server(): if cosmostat_client.check_system_timer(): update_redis_channel("host_metrics", get_client_redis_data(human_readable = False)) if run_cosmostat_server(): update_redis_channel("client_summary", get_server_redis_data()) # Server Redis Tree # Update history_stats Redis Channel # update_redis_channel("history_stats", get_component_list()) def get_client_redis_data(human_readable = False): result = [] for metric in get_dynamic_data(human_readable): result.append(metric) #for metric in get_static_data(human_readable): # result.append(metric) return result def get_server_redis_data(): result = [] for client in cosmostat_server.systems: this_client_key = { "uuid": client["uuid"], "short_id": client["short_id"], "redis_data": client["redis_data"] } result.append(this_client_key) return result ####################################################################### ### Client Flask Routes ####################################################################### # dynamic data # this will go to the redis server @app.route('/dynamic_data', methods=['GET']) def dynamic_data(): return jsonify(get_dynamic_data()) # static data @app.route('/static_data', methods=['GET']) def static_data(): return jsonify(get_static_data()) # redis data @app.route('/redis_data', methods=['GET']) def redis_data(): return jsonify(get_client_redis_data(human_readable = False)) # redis strings @app.route('/redis_strings', methods=['GET']) def redis_strings(): return jsonify(get_client_redis_data(human_readable = True)) # php summary @app.route('/php_summary', methods=['GET']) def php_summary(): return jsonify(get_php_summary()) # return full descriptor @app.route('/descriptor', methods=['GET']) def descriptor(): return jsonify(get_descriptor()) # socket timer handler @app.route('/start_timer', methods=['GET']) def start_timer(): current_timestamp = int(time.time()) cosmostat_client.recent_check = current_timestamp log_data(log_output = f"Timestamp updated to {cosmostat_client.recent_check}", log_level = "noisy_test") return jsonify( { "message": "websocket timer reset", "new_timestamp": cosmostat_client.recent_check } ) # socket timer data @app.route('/timer_data', methods=['GET']) def timer_data(): time_now = time.time() time_lapsed = time_now - float(cosmostat_client.recent_check) result = { "Time Lapsed": time_lapsed, "Current Time Value": time_now, "Last Update Value": float(cosmostat_client.recent_check), "System Updating": cosmostat_client.check_system_timer() } return jsonify(result) # test route @app.route('/test', methods=['GET']) def test(): this_cpu = cosmostat_client.get_components(component_type="CPU") return jsonify( { "component_count:": len(cosmostat_client.components), "user": jenkins_user_settings(), "hostname": jenkins_hostname_settings(), "cpu_model": this_cpu[0].description } ) ####################################################################### ### Client Flask Helpers ####################################################################### # needs to return array of {name: name, type: type, metrics: metrics} # for redis table generation, includes system and component metrics def get_dynamic_data(human_readable = False): return cosmostat_client.get_live_metrics(human_readable) def get_static_data(human_readable = False): result = [] return cosmostat_client.get_static_metrics(human_readable) def get_php_summary(): system_properties = cosmostat_client.get_system_properties(human_readable = True) system_components = [] for component in cosmostat_client.get_components(): this_component = { "component_name": component.name, "info_strings": component.get_properties_strings(return_simple = True) } system_components.append(this_component) result = [{ "system_properties": system_properties, "system_components": system_components }] return result def get_descriptor(): return cosmostat_client.get_component_class_tree() def generate_state_definition(): result = { "uuid": cosmostat_client.uuid, "state_definition": get_php_summary() } return result ####################################################################### ### Server Flask Routes ####################################################################### # update client on server @app.route('/update_client', methods=['GET']) def update_client(): result = {} # check the request and return payload if all good payload = client_submit_check(request = request, dict_name = "redis_data") this_client = cosmostat_server.get_system(uuid = payload["uuid"]) result = run_update_client(this_client) return jsonify(result), 200 # create client on server @app.route('/create_client', methods=['GET']) def create_client(): result = {} # check the request and return payload if all good payload = client_submit_check(request = request, dict_name = "client_properties") this_client = cosmostat_server.get_system(uuid = payload["uuid"]) result = run_create_client(this_client) return jsonify(result), 200 # api to validate Cosmostat Class @app.route('/client_summary', methods=['GET']) def client_summary(): client_summary = get_client_summary() return jsonify() ####################################################################### ### Server Flask Helpers ####################################################################### # update client on server def run_update_client(this_client): if this_client == {}: return { "message": "client not found" } update_status = f"updated client {this_client.short_id}" timestamp_update = cosmostat_server.update_system(system_state = payload, system_uuid = payload["uuid"]) return { "status": update_status, "uuid": payload["uuid"], "timestamp": timestamp_update } # create client on server def run_create_client(this_client): update_status = f"created client {this_client.short_id}" timestamp_update = cosmostat_server.create_system(system_state = payload, system_uuid = payload["uuid"]) return { "status": update_status, "uuid": payload["uuid"], "timestamp": timestamp_update } # flask submission check fucntion def client_submit_check(request, dict_name: str): required_keys = {"uuid", "short_id", "data_timestamp", dict_name} if not request.is_json: logging.warning("Received non-JSON request") return jsonify({"error": "Content-type must be application/json"}), 400 payload = request.get_json(silent=True) if payload is None: logging.warning("Malformed JSON body") return jsonify({"error": "Malformed JSON"}), 400 missing = required_keys - payload.keys() if missing: raise ValueError(f"Missing required keys: {', '.join(sorted(missing))}") return payload # generate cosmostat server summary def get_client_summary(): result = [] for client in cosmostat_server.systems: this_client_properties = client.get_system_properties(human_readable = True) this_client_components = [] for component in client.get_components(): this_component = { "component_name": component.name, "info_strings": component.get_properties_strings(return_simple = True) } this_client_components.append(this_component) this_client = { "client_properties": this_client_properties, "client_components": this_client_components } result.append(this_client) return result ####################################################################### ### Cosmostat Client Subroutines ####################################################################### # Cosmostat Client Reporter def client_update(this_client: dict, api_endpoint = "update_client"): # set variables for API call this_uuid = cosmostat_client.uuid this_short_id = cosmostat_client.short_id this_timestamp = time.time() api_url = f"{cosmostat_server_api()}{api_endpoint}" # generate payload payload = { "uuid": this_uuid, "short_id": this_short_id, "data_timestamp": this_timestamp, # Unix epoch float "redis_data": get_client_redis_data(human_readable = False), } # execute API call result = client_submission_handler() if ( isinstance(result, dict) and result.get("message", "").lower() == "client not found" ): # if client not found, create client if api_endpoint == "update_client": client_initialize() raise RuntimeError("Client not found - initializing") return result # Cosmostat Client Initializer def client_initialize(): # set variables for API call this_uuid = cosmostat_client.uuid this_short_id = cosmostat_client.short_id this_timestamp = time.time() api_url = f"{cosmostat_server_api()}create_client" # generate payload payload = { "uuid": this_uuid, "short_id": this_short_id, "data_timestamp": this_timestamp, # Unix epoch float "client_properties": get_php_summary(), } # execute API call result = client_submission_handler() return result # Cosmostat Client API Reporting Handler def client_submission_handler(): result = None try: # `json=` automatically sets Content-Type to application/json response: Response = requests.post(api_url, json=payload, timeout=timeout) response.raise_for_status() # raise HTTPError for 4xx/5xx except RequestException as exc: # Wrap the low-level exception in a more descriptive one raise RuntimeError( f"Failed to POST to {url!r}: {exc}" ) from exc # process reply from API try: result = response.json() except ValueError as exc: raise RuntimeError( f"Server responded with non-JSON payload: {response.text!r}" ) from exc return result ####################################################################### ####################################################################### ### Main Subroutine ####################################################################### ####################################################################### if __name__ == '__main__': ###################################### ### Main Functions ###################################### # instantiate and return the Client System object def new_cosmos_client(): new_client = System(f"{jenkins_hostname_settings()}") log_data(log_output = f"New System object name: {new_client.name} - {new_client.get_component_count()} components:", log_level = "log_output") for component in new_client.components: log_data(log_output = component.description, log_level = "log_output") return new_client # instantiate and return the Cosmoserver System object def new_cosmos_server(): new_server = Cosmoserver(cosmostat_client.uuid) log_data(log_output = f"New Cosmostat object name: {new_server.name}", log_level = "log_output") return new_server # Background Loop Function def background_loop(): # Update all data on the System object if cosmostat_client.check_system_timer(): cosmostat_client.update_system_state() if app_settings["push_redis"]: update_redis_server() if app_settings["cosmostat_server_reporter"]: client_update() ###################################### # instantiate client ###################################### cosmostat_client = new_cosmos_client() if app_settings["cosmostat_server_reporter"]: client_initialize() ###################################### # instantiate server ###################################### cosmostat_server = None if run_cosmostat_server(): cosmostat_server = new_cosmos_server() ###################################### # send initial stats update to redis ###################################### if app_settings["push_redis"]: update_redis_server() ###################################### # Flask scheduler for scanner ###################################### if app_settings["run_background"]: log_data(log_output = "Loading flask background subroutine...", log_level = "log_output") scheduler.add_job(id='background_loop', func=background_loop, trigger='interval', seconds=app_settings["update_frequency"]) scheduler.init_app(app) scheduler.start() log_data(log_output = "...Done", log_level = "log_output") else: log_data(log_output = "Skipping flask background task", log_level = "log_output") ###################################### # Flask API ###################################### app.run(debug=False, host=service_gateway_ip(), port=service_api_port())