new classes based on json descriptor
This commit is contained in:
@ -56,4 +56,5 @@ secure_api: false
|
|||||||
push_redis: true
|
push_redis: true
|
||||||
run_background : true
|
run_background : true
|
||||||
log_output: true
|
log_output: true
|
||||||
|
update_frequency: "1"
|
||||||
...
|
...
|
||||||
@ -1,223 +1,255 @@
|
|||||||
# this class file is for the cosmostat service
|
# this class file is for the cosmostat service
|
||||||
import subprocess
|
import subprocess
|
||||||
from LinkedList import *
|
import json
|
||||||
|
from typing import Dict, Any, List
|
||||||
|
|
||||||
|
# Global Class Vars
|
||||||
global_max_length = 500
|
global_max_length = 500
|
||||||
|
debug_output = False
|
||||||
|
|
||||||
|
# import the component descriptor
|
||||||
|
# this outlines how the component class works
|
||||||
|
# each type of component has a "type"
|
||||||
|
try:
|
||||||
|
with open("component_descriptors.json", encoding="utf-8") as f:
|
||||||
|
component_class_tree: List[Dict] = json.load(f)
|
||||||
|
except FileNotFoundError as exc:
|
||||||
|
raise RuntimeError("Descriptor file not found") from exc
|
||||||
|
|
||||||
|
component_types = [{"name": entry["name"], "multi_check": entry["multi_check"] == "True"} for entry in component_class_tree]
|
||||||
|
|
||||||
class Component:
|
class Component:
|
||||||
##########################################################################################
|
|
||||||
# Base class for all system components. All instantiated objects need a child class
|
|
||||||
# Class data:
|
|
||||||
### name - name of the type of component, declared in the parent class
|
|
||||||
### status
|
|
||||||
### model_string - string with device info, declared in parent class
|
|
||||||
### metric_name - name of the value being measured
|
|
||||||
### current_value
|
|
||||||
### historical_data - This will be a linked list used to generate a json when calling get_historical_data
|
|
||||||
### for this to work, the function using these classes needs to update the values periodically
|
|
||||||
#### historical_data = [
|
|
||||||
#### {
|
|
||||||
#### "timestamp": timestamp, # seconds since epoch
|
|
||||||
#### "value": value
|
|
||||||
#### },
|
|
||||||
#### {
|
|
||||||
#### "timestamp": timestamp,
|
|
||||||
#### "value": value
|
|
||||||
#### }
|
|
||||||
#### ]
|
|
||||||
|
|
||||||
def __init__(self, name: str, model_string: str = None):
|
def __init__(self, name: str, comp_type: str ):
|
||||||
# fail instantiation if critical data is missing
|
|
||||||
if self.model_string is None:
|
|
||||||
raise TypeError("Error - missing component model_string")
|
|
||||||
if self.metric_name is None:
|
|
||||||
raise TypeError("Error - missing component metric_name")
|
|
||||||
if self.metric_value_command is None:
|
|
||||||
raise TypeError("Error - missing component metric_value_command")
|
|
||||||
if self.type is None:
|
|
||||||
raise TypeError("Error - missing component type")
|
|
||||||
if self.has_temp is None:
|
|
||||||
raise TypeError("Error - missing temp data check")
|
|
||||||
|
|
||||||
# set up history list
|
|
||||||
self.history_max_length = global_max_length
|
|
||||||
self.historical_data = ValueHistory(self.history_max_length)
|
|
||||||
self.history_start = self.historical_data.get_first_timestamp()
|
|
||||||
self.update_value()
|
|
||||||
if self.current_value is None:
|
|
||||||
raise TypeError("Error - failed to read value")
|
|
||||||
|
|
||||||
# if temp data exists, handle it
|
|
||||||
if self.has_temp:
|
|
||||||
self.temp_history_data = ValueHistory(self.history_max_length)
|
|
||||||
self.temp_history_start = self.temp_history_data.get_first_timestamp()
|
|
||||||
self.current_temp = self.temp_history_data.get_current_value()
|
|
||||||
else:
|
|
||||||
self.temp_history_data = None
|
|
||||||
|
|
||||||
# instantiate other shared class variables
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.current_value = self.historical_data.get_current_value()
|
self.type = comp_type
|
||||||
if self.has_temp:
|
for component in component_class_tree:
|
||||||
self.current_temp = self.temp_history_data.get_current_value()
|
if component["name"] == self.type:
|
||||||
else:
|
COMPONENT_DESCRIPTORS = component
|
||||||
self.current_temp = None
|
# Load component type descriptor from class tree
|
||||||
self.comment = f"This is a {self.type}, so we are measuring {self.metric_name}, currently at {self.current_value}"
|
# COMPONENT_DESCRIPTORS = {d['type']: d for d in component_class_tree}
|
||||||
|
descriptor = COMPONENT_DESCRIPTORS
|
||||||
# if nothing failed, the object is ready
|
self._descriptor = descriptor
|
||||||
self.status = "ready"
|
if descriptor is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Component type '{comp_type}' is not defined in the "
|
||||||
|
f"component descriptor tree."
|
||||||
|
)
|
||||||
|
# store static properties
|
||||||
|
self.multi_check = self.is_multi()
|
||||||
|
self._properties: Dict[str, str] = {}
|
||||||
|
for key, command in descriptor.get('properties', {}).items():
|
||||||
|
self._properties[key] = run_command(command, True)
|
||||||
|
# build the description string
|
||||||
|
self._description_template: str | None = descriptor.get("description")
|
||||||
|
self.description = self._description_template.format(**self._properties)
|
||||||
|
# initialize metrics
|
||||||
|
self._metrics: Dict[str, str] = {}
|
||||||
|
self.update_metrics()
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (f"{self.__class__.__name__}: {self.name} "
|
self_string = (f"Component name: {self.name}, type: {self.type} - "
|
||||||
f"{self.model_string}")
|
f"{self.description}")
|
||||||
|
return self_string
|
||||||
def __del__(self):
|
|
||||||
print(f"Deleting {self.type} component - {self.model_string}")
|
def __repr__(self):
|
||||||
|
self_string = (f"Component name: {self.name}, type {self.type} - "
|
||||||
|
f"{self.description}")
|
||||||
|
return self_string
|
||||||
|
|
||||||
def get_info_key(self):
|
def update_metrics(self):
|
||||||
|
for key, command in self._descriptor.get('metrics', {}).items():
|
||||||
|
self._metrics[key] = run_command(command, True)
|
||||||
|
|
||||||
|
# complex data type return
|
||||||
|
def get_metrics(self, type = None):
|
||||||
|
these_metrics = []
|
||||||
|
if type == None:
|
||||||
|
for name, value in self._metrics:
|
||||||
|
these_metrics.append({"name": name, "value": value})
|
||||||
|
else:
|
||||||
|
for name, value in self._metrics:
|
||||||
|
if name == type:
|
||||||
|
these_metrics.append({"name": name, "value": value})
|
||||||
result = {
|
result = {
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"type": self.type,
|
"type": self.type,
|
||||||
"model_string": self.model_string,
|
"metrics": these_metrics
|
||||||
"metric_name": self.metric_name
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_summary_key(self):
|
# complex data type return
|
||||||
|
def get_properties(self, type = None):
|
||||||
|
these_properties = []
|
||||||
|
if type == None:
|
||||||
|
for name, value in self._properties.items():
|
||||||
|
these_properties.append({"name": name, "value": value})
|
||||||
|
else:
|
||||||
|
for name, value in self._properties.items():
|
||||||
|
if name == type:
|
||||||
|
these_properties.append({"name": name, "value": value})
|
||||||
result = {
|
result = {
|
||||||
|
"name": self.name,
|
||||||
"type": self.type,
|
"type": self.type,
|
||||||
"current_value": self.current_value,
|
"properties": these_properties
|
||||||
"metric_name": self.metric_name,
|
|
||||||
"model_string": self.model_string
|
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def update_value(self):
|
# this gets the value of a specified property, type required
|
||||||
#try:
|
def get_property(self, type):
|
||||||
self.current_value = run_command(self.metric_value_command, True)
|
return self._properties[type]
|
||||||
self.historical_data.add(self.current_value)
|
|
||||||
#except:
|
|
||||||
|
|
||||||
def update_temp_value(self):
|
|
||||||
if has_temp:
|
|
||||||
#try:
|
|
||||||
self.current_temp = run_command(self.temp_value_command, True)
|
|
||||||
self.temp_history_data.add(self.current_value)
|
|
||||||
#except:
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
# returns array of dicts for redis
|
||||||
def get_history(self, count: int = global_max_length):
|
def get_metrics_keys(self):
|
||||||
if self.has_temp:
|
result = []
|
||||||
result = {
|
for name, value in self._metrics.items():
|
||||||
"value_metric": self.metric_name,
|
this_metric = {
|
||||||
"history_count": count,
|
"name": self.name,
|
||||||
"history_data": self.historical_data.get_history(count), # reminder this is a LinkedList get_history
|
"type": name,
|
||||||
"history_temp_data": self.temp_history_data.get_history(count)
|
"metric": value
|
||||||
}
|
}
|
||||||
else:
|
result.append(this_metric)
|
||||||
result = {
|
|
||||||
"value_metric": self.metric_name,
|
|
||||||
"history_count": count,
|
|
||||||
"history_data": self.historical_data.get_history(count) # same reminder here
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def get_properties_keys(self):
|
||||||
|
result = []
|
||||||
|
for name, value in self._properties.items():
|
||||||
|
this_property = {
|
||||||
|
"name": self.name,
|
||||||
|
"property": name,
|
||||||
|
"value": value
|
||||||
|
}
|
||||||
|
result.append(this_property)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# full data return
|
||||||
|
def get_description(self):
|
||||||
|
these_properties = []
|
||||||
|
for name, value in self._metrics.items():
|
||||||
|
these_properties.append({"name": name, "value": value})
|
||||||
|
these_metrics = []
|
||||||
|
for name, value in self._metrics.items():
|
||||||
|
these_metrics.append({"name": name, "value": value})
|
||||||
|
result = {
|
||||||
|
"name": self.name,
|
||||||
|
"type": self.type,
|
||||||
|
"properties": these_properties,
|
||||||
|
"metrics": these_metrics
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def is_multi(self):
|
||||||
|
for component_type in component_types:
|
||||||
|
if self.type == component_type["name"]:
|
||||||
|
return component_type["multi_check"]
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
|
||||||
# Component Class Types
|
|
||||||
# There needs to be one of these for each monitored thing
|
|
||||||
############################################################
|
|
||||||
# Need to add:
|
|
||||||
### temperatures
|
|
||||||
### network + VPN
|
|
||||||
### storage + ZFS
|
|
||||||
### video cards
|
|
||||||
### virtual machines
|
|
||||||
|
|
||||||
# CPU component class.
|
|
||||||
class CPU(Component):
|
|
||||||
|
|
||||||
def __init__(self, name: str, is_virtual: bool = False):
|
|
||||||
# Declare component type
|
|
||||||
self.type = "CPU"
|
|
||||||
# deal with temp later
|
|
||||||
self.has_temp = False
|
|
||||||
# no temp if VM
|
|
||||||
#self.has_temp = not is_virtual
|
|
||||||
#self.temp_value_command = "acpi -V | jc --acpi -p | jq '.[] | select(.type==\"Thermal\") | .temperature '"
|
|
||||||
self.model_string = self.get_model_string()
|
|
||||||
|
|
||||||
# Initialize value
|
|
||||||
self.metric_name = "1m_load"
|
|
||||||
self.metric_value_command = "cat /proc/loadavg | awk '{print $1}'"
|
|
||||||
self.current_value = run_command(self.metric_value_command, True)
|
|
||||||
|
|
||||||
# Complete instantiation
|
|
||||||
super().__init__(name, self.model_string)
|
|
||||||
|
|
||||||
def get_model_string(self):
|
|
||||||
# Get CPU Info
|
|
||||||
model_string_command = "lscpu --json | jq -r '.lscpu[] | select(.field==\"Model name:\") | .data'"
|
|
||||||
return run_command(model_string_command, True)
|
|
||||||
|
|
||||||
# RAM component class.
|
|
||||||
class RAM(Component):
|
|
||||||
|
|
||||||
def __init__(self, name: str):
|
|
||||||
# Declare component type
|
|
||||||
self.type = "RAM"
|
|
||||||
self.has_temp = False
|
|
||||||
self.model_string = self.get_model_string()
|
|
||||||
|
|
||||||
# Initialize Value
|
|
||||||
self.metric_name = "used_capacity_mb"
|
|
||||||
self.metric_value_command = "free -m | grep Mem | awk '{print $3}'"
|
|
||||||
self.current_value = run_command(self.metric_value_command, True)
|
|
||||||
|
|
||||||
# Complete instantiation
|
|
||||||
super().__init__(name, self.model_string)
|
|
||||||
|
|
||||||
def get_model_string(self):
|
|
||||||
# Check total system RAM
|
|
||||||
bytes_total_command = "sudo lshw -json -c memory | jq -r '.[] | select(.description==\"System Memory\").size' "
|
|
||||||
bytes_total = float(run_command(bytes_total_command, True))
|
|
||||||
gb_total = round(bytes_total / 1073741824, 2)
|
|
||||||
return f"Total Capacity: {gb_total}GB"
|
|
||||||
|
|
||||||
############################################################
|
############################################################
|
||||||
# System Class
|
# System Class
|
||||||
# A system is build from components
|
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
class System:
|
class System:
|
||||||
|
# system variable declarations
|
||||||
|
# keys to add: model and serial number
|
||||||
|
static_key_variables = [
|
||||||
|
{"name": "hostname", "command": "hostname"},
|
||||||
|
{"name": "virt_string", "command": "systemd-detect-virt"}
|
||||||
|
]
|
||||||
|
dynamic_key_variables = [
|
||||||
|
{"name": "uptime", "command": "uptime -p"},
|
||||||
|
{"name": "timestamp", "command": "date '+%D %r'"},
|
||||||
|
]
|
||||||
|
# add components based on the class tree
|
||||||
|
# component_types = [{"name": entry["name"], "multi_check": entry["multi_check"] == "True"} for entry in component_class_tree]
|
||||||
|
|
||||||
# instantiate new system
|
# instantiate new system
|
||||||
def __init__(self, name: str):
|
def __init__(self, name: str):
|
||||||
# the system needs a name
|
# the system needs a name
|
||||||
self.name = name
|
self.name = name
|
||||||
# system is built of other component objects
|
if debug_output:
|
||||||
|
print(f"System initializing, name {self.name}")
|
||||||
|
# system contains an array of component objects
|
||||||
self.components = []
|
self.components = []
|
||||||
# other system properties
|
# initialize system properties and metrics dicts
|
||||||
self.sysvars = {}
|
self._properties: Dict[str, str] = {}
|
||||||
# either i do it here or i do it twice
|
self._metrics: Dict[str, str] = {}
|
||||||
self.sysvars["is_virtual"] = self.check_for_virtual()
|
# load static keys
|
||||||
# Let's build a system
|
for static_key in self.static_key_variables:
|
||||||
self.add_component(CPU("CPU", self.sysvars["is_virtual"]))
|
command = static_key["command"]
|
||||||
self.add_component(RAM("RAM"))
|
result = run_command(command, True)
|
||||||
|
if debug_output:
|
||||||
|
print(f"Static key [{static_key["name"]}] - command [{command}] - output [{result}]")
|
||||||
|
self._properties[static_key["name"]] = result
|
||||||
|
# initialize live keys
|
||||||
|
self.update_live_keys()
|
||||||
|
# initialze components
|
||||||
|
self.load_components()
|
||||||
|
|
||||||
# let's build system values
|
# update only system dynamic keys
|
||||||
self.check_values()
|
def update_live_keys(self):
|
||||||
|
for live_key in self.dynamic_key_variables:
|
||||||
|
if live_key['command'] is not None:
|
||||||
|
command = live_key['command']
|
||||||
|
result = run_command(command, True)
|
||||||
|
self._metrics[live_key['name']] = result
|
||||||
|
if debug_output:
|
||||||
|
print(f"Command {live_key["name"]} - [{command}] Result - [{result}]")
|
||||||
|
|
||||||
|
# update all dynamic keys, including components
|
||||||
|
def update_system_state(self):
|
||||||
|
self.update_live_keys()
|
||||||
|
for component in self.components:
|
||||||
|
component.update_metrics()
|
||||||
|
|
||||||
|
# check for components
|
||||||
|
def load_components(self):
|
||||||
|
for component in component_types:
|
||||||
|
component_name = component["name"]
|
||||||
|
multi_check = component["multi_check"]
|
||||||
|
if multi_check:
|
||||||
|
print("placeholder...")
|
||||||
|
else:
|
||||||
|
if debug_output:
|
||||||
|
print(f"Creating component {component["name"]}")
|
||||||
|
self.add_components(Component(component_name, component_name))
|
||||||
|
|
||||||
# Add a component to the system
|
# Add a component to the system
|
||||||
def add_component(self, component: Component):
|
def add_components(self, component: Component):
|
||||||
|
if debug_output:
|
||||||
|
print(f"Component description: {component.description}")
|
||||||
self.components.append(component)
|
self.components.append(component)
|
||||||
|
|
||||||
# Get all components, optionally filtered by type
|
# Get all components, optionally filtered by type
|
||||||
def get_components(self, component_type: type = None):
|
def get_components(self, component_type: type = None):
|
||||||
if component_type is None:
|
if component_type is None:
|
||||||
return self.components
|
return self.components
|
||||||
return [c for c in self.components if isinstance(c, component_type)]
|
else:
|
||||||
|
result = []
|
||||||
|
for component in self.components:
|
||||||
|
if component.type == component_type:
|
||||||
|
result.append(component)
|
||||||
|
if component.is_multi():
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return result[0]
|
||||||
|
|
||||||
|
def get_component_strings(self, component_type: type = None):
|
||||||
|
if component_type is None:
|
||||||
|
result = []
|
||||||
|
for component in self.components:
|
||||||
|
result.append(component.description)
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
result = []
|
||||||
|
for component in self.components:
|
||||||
|
if component.type == component_type:
|
||||||
|
result.append(component.description)
|
||||||
|
if component.is_multi():
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return result[0]
|
||||||
|
|
||||||
# get component count
|
# get component count
|
||||||
def get_component_count(self):
|
def get_component_count(self):
|
||||||
result = int(len(self.components))
|
result = int(len(self.components))
|
||||||
@ -225,64 +257,93 @@ class System:
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
components_str = "\n".join(f" - {c}" for c in self.components)
|
components_str = "\n".join(f" - {c}" for c in self.components)
|
||||||
return f"System: {self.name}\n{components_str}"
|
return f"System hostname: {self.name}\nComponent Count: {self.get_component_count()}\n{components_str}"
|
||||||
|
|
||||||
# update metrics for all components
|
|
||||||
def update_values(self):
|
|
||||||
self.check_values()
|
|
||||||
for component in self.components:
|
|
||||||
component.update_value()
|
|
||||||
|
|
||||||
def check_for_virtual(self):
|
|
||||||
check_if_vm_command = "systemd-detect-virt"
|
|
||||||
check_if_vm = run_command(check_if_vm_command, True)
|
|
||||||
if check_if_vm != "none":
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def check_uptime(self):
|
|
||||||
check_uptime_command = "uptime -p"
|
|
||||||
system_uptime = run_command(check_uptime_command, True)
|
|
||||||
return system_uptime
|
|
||||||
|
|
||||||
def check_timestamp(self):
|
|
||||||
check_timestamp_command = "date '+%D %r'"
|
|
||||||
system_timestamp = run_command(check_timestamp_command, True)
|
|
||||||
return system_timestamp
|
|
||||||
|
|
||||||
def check_values(self):
|
|
||||||
self.sysvars["uptime"] = self.check_uptime()
|
|
||||||
self.sysvars["name"] = self.name
|
|
||||||
self.sysvars["component_count"] = self.get_component_count()
|
|
||||||
self.sysvars["timestamp"] = self.check_timestamp()
|
|
||||||
|
|
||||||
def get_sysvars(self):
|
|
||||||
result = {}
|
|
||||||
for sysvar in self.sysvars:
|
|
||||||
result[f"{sysvar}"] = self.sysvars[f"{sysvar}"]
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
# return both static and dynamic data
|
||||||
def get_sysvars_summary_keys(self):
|
def get_sysvars_summary_keys(self):
|
||||||
result = []
|
result = []
|
||||||
for sysvar in self.sysvars:
|
for name, value in self._properties.items():
|
||||||
system_type_string = f"sysvar['{sysvar}']"
|
|
||||||
thisvar = {
|
thisvar = {
|
||||||
"type": "System Class Variable",
|
"name": "System Class Property",
|
||||||
"current_value": sysvar,
|
"type": name,
|
||||||
"metric_name": system_type_string,
|
"value": value
|
||||||
"model_string": self.sysvars[sysvar]
|
}
|
||||||
|
result.append(thisvar)
|
||||||
|
for name, value in self._metrics.items():
|
||||||
|
thisvar = {
|
||||||
|
"name": "System Class Metric",
|
||||||
|
"type": name,
|
||||||
|
"value": value
|
||||||
}
|
}
|
||||||
result.append(thisvar)
|
result.append(thisvar)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# return list of all live metrics from system and properties
|
||||||
|
def get_live_metrics(self):
|
||||||
|
result = []
|
||||||
|
for component_metric in self.get_component_metrics():
|
||||||
|
result.append(component_metric)
|
||||||
|
for system_metric in self.get_system_metrics():
|
||||||
|
result.append(system_metric)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# return array of all component metrics
|
||||||
|
def get_component_metrics(self):
|
||||||
|
result = []
|
||||||
|
for component in self.components:
|
||||||
|
for metric in component.get_metrics_keys():
|
||||||
|
result.append(metric)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# return array of all component metrics
|
||||||
|
def get_component_properties(self):
|
||||||
|
result = []
|
||||||
|
for component in self.components:
|
||||||
|
for metric in component.get_properties_keys():
|
||||||
|
result.append(metric)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# return array of all system metrics
|
||||||
|
def get_system_metrics(self):
|
||||||
|
result = []
|
||||||
|
for name, value in self._metrics.items():
|
||||||
|
thisvar = {
|
||||||
|
"name": "System",
|
||||||
|
"type": name,
|
||||||
|
"metric": value
|
||||||
|
}
|
||||||
|
result.append(thisvar)
|
||||||
|
# add component count
|
||||||
|
result.append({
|
||||||
|
"name": "System",
|
||||||
|
"type": "component_count",
|
||||||
|
"metric": self.get_component_count()
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_system_properties(self):
|
||||||
|
result = []
|
||||||
|
for name, value in self._properties.items():
|
||||||
|
if name == "virt_string":
|
||||||
|
thisvar = {
|
||||||
|
"name": "System",
|
||||||
|
"property": name,
|
||||||
|
"value": value == "none"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
thisvar = {
|
||||||
|
"name": "System",
|
||||||
|
"property": name,
|
||||||
|
"value": value
|
||||||
|
}
|
||||||
|
result.append(thisvar)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
############################################################
|
||||||
# Helper Functions
|
# Helper Functions
|
||||||
############################################################
|
############################################################
|
||||||
|
|
||||||
|
|
||||||
# subroutine to run a command, return stdout as array unless zero_only then return [0]
|
# subroutine to run a command, return stdout as array unless zero_only then return [0]
|
||||||
def run_command(cmd, zero_only=False):
|
def run_command(cmd, zero_only=False):
|
||||||
# Run the command and capture the output
|
# Run the command and capture the output
|
||||||
|
|||||||
@ -1,98 +0,0 @@
|
|||||||
|
|
||||||
##############################
|
|
||||||
# linked list classes
|
|
||||||
# written by the intern
|
|
||||||
##############################
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
# single node in a singly linked list
|
|
||||||
class Node:
|
|
||||||
__slots__ = ("value", "next", "timestamp")
|
|
||||||
|
|
||||||
def __init__(self, value):
|
|
||||||
self.value = value
|
|
||||||
self.timestamp = time.time()
|
|
||||||
self.next = None
|
|
||||||
|
|
||||||
# small, bounded history implemented with a singly linked list
|
|
||||||
class ValueHistory:
|
|
||||||
def __init__(self, maxlen: int):
|
|
||||||
if maxlen <= 0:
|
|
||||||
raise ValueError("maxlen must be a positive integer")
|
|
||||||
self.maxlen = maxlen
|
|
||||||
self.head: Node | None = None # oldest entry
|
|
||||||
self.tail: Node | None = None # newest entry
|
|
||||||
self.size = 0
|
|
||||||
|
|
||||||
# Append a new value to the history, dropping the oldest if needed
|
|
||||||
def add(self, value):
|
|
||||||
new_node = Node(value)
|
|
||||||
|
|
||||||
# link it after the current tail
|
|
||||||
if self.tail is None: # empty list
|
|
||||||
self.head = self.tail = new_node
|
|
||||||
else:
|
|
||||||
self.tail.next = new_node
|
|
||||||
self.tail = new_node
|
|
||||||
|
|
||||||
self.size += 1
|
|
||||||
|
|
||||||
# 2. enforce the size bound
|
|
||||||
if self.size > self.maxlen:
|
|
||||||
# drop the head (oldest item)
|
|
||||||
assert self.head is not None # for the type checker
|
|
||||||
self.head = self.head.next
|
|
||||||
self.size -= 1
|
|
||||||
|
|
||||||
# If the list became empty, also reset tail
|
|
||||||
if self.head is None:
|
|
||||||
self.tail = None
|
|
||||||
|
|
||||||
# Return the history as a Python dict list (oldest → newest)
|
|
||||||
def get_history(self, count: int | None = None):
|
|
||||||
if count is None:
|
|
||||||
count = self.maxlen
|
|
||||||
out = []
|
|
||||||
cur = self.head
|
|
||||||
counter = 0
|
|
||||||
while cur is not None and counter < count:
|
|
||||||
counter += 1
|
|
||||||
out.append(
|
|
||||||
{
|
|
||||||
"timestamp": cur.timestamp,
|
|
||||||
"value": cur.value
|
|
||||||
}
|
|
||||||
)
|
|
||||||
cur = cur.next
|
|
||||||
return out
|
|
||||||
|
|
||||||
# Return oldest timestamp
|
|
||||||
def get_first_timestamp(self):
|
|
||||||
if self.head is not None:
|
|
||||||
return self.head.timestamp
|
|
||||||
else:
|
|
||||||
return time.time()
|
|
||||||
|
|
||||||
# Return current data
|
|
||||||
def get_current_value(self):
|
|
||||||
if self.tail is not None:
|
|
||||||
return self.tail.value
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Convenience methods
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
def __len__(self):
|
|
||||||
return self.size
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Iterate over values from oldest to newest."""
|
|
||||||
cur = self.head
|
|
||||||
while cur is not None:
|
|
||||||
yield cur.value
|
|
||||||
cur = cur.next
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return f"BoundedHistory(maxlen={self.maxlen}, data={self.get()!r})"
|
|
||||||
205
files/api/app.py
205
files/api/app.py
@ -16,11 +16,12 @@ scheduler = APScheduler()
|
|||||||
# default application setting variables
|
# default application setting variables
|
||||||
app_settings = {
|
app_settings = {
|
||||||
"noisy_test" : False,
|
"noisy_test" : False,
|
||||||
"debug_output" : False,
|
"debug_output" : True,
|
||||||
"log_output" : False,
|
"log_output" : True,
|
||||||
"secure_api" : True,
|
"secure_api" : True,
|
||||||
"push_redis" : False,
|
"push_redis" : False,
|
||||||
"run_background" : True
|
"run_background" : True,
|
||||||
|
"update_frequency": 1
|
||||||
}
|
}
|
||||||
|
|
||||||
with open('cosmostat_settings.yaml', 'r') as f:
|
with open('cosmostat_settings.yaml', 'r') as f:
|
||||||
@ -75,93 +76,29 @@ def update_redis_channel(redis_channel, data):
|
|||||||
|
|
||||||
def update_redis_server():
|
def update_redis_server():
|
||||||
# Update Stats Redis Channel
|
# Update Stats Redis Channel
|
||||||
update_redis_channel("host_stats", get_full_summary())
|
update_redis_channel("host_metrics", get_redis_data())
|
||||||
# Update history_stats Redis Channel
|
# Update history_stats Redis Channel
|
||||||
update_redis_channel("history_stats", get_component_list())
|
# update_redis_channel("history_stats", get_component_list())
|
||||||
|
|
||||||
#######################################################################
|
|
||||||
### Other Functions
|
|
||||||
#######################################################################
|
|
||||||
|
|
||||||
def get_component_summary():
|
|
||||||
result = []
|
|
||||||
for component in cosmostat_system.components:
|
|
||||||
result.append(component.get_summary_key())
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_full_summary():
|
|
||||||
result = []
|
|
||||||
|
|
||||||
for component in cosmostat_system.components:
|
|
||||||
result.append(component.get_summary_key())
|
|
||||||
|
|
||||||
for sysvar in cosmostat_system.get_sysvars_summary_keys():
|
|
||||||
result.append(sysvar)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
# This will instantiate a System object
|
|
||||||
def new_cosmos_system():
|
|
||||||
new_system = System(f"{jenkins_hostname_settings()}")
|
|
||||||
if app_settings["log_output"]:
|
|
||||||
print(f"New system object name: {new_system.name}")
|
|
||||||
for component in new_system.components:
|
|
||||||
print(component)
|
|
||||||
return new_system
|
|
||||||
|
|
||||||
def get_component_list(history_count = None):
|
|
||||||
result = []
|
|
||||||
for component in cosmostat_system.components:
|
|
||||||
if history_count is not None:
|
|
||||||
history = component.get_history(history_count)
|
|
||||||
else:
|
|
||||||
history = component.get_history()
|
|
||||||
result.append(
|
|
||||||
{
|
|
||||||
"info": component.get_info_key(),
|
|
||||||
"history": history
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_info():
|
|
||||||
device_summary = []
|
|
||||||
for component in cosmostat_system.components:
|
|
||||||
device_summary.append(
|
|
||||||
{
|
|
||||||
"info": component.get_info_key(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
result = {
|
|
||||||
"system_info":
|
|
||||||
{
|
|
||||||
"user": jenkins_user_settings(),
|
|
||||||
"hostname": jenkins_hostname_settings(),
|
|
||||||
"timestamp": jenkins_inventory_generation_timestamp_settings(),
|
|
||||||
"component_count:": len(cosmostat_system.components),
|
|
||||||
"object_name": cosmostat_system.name,
|
|
||||||
"docker_gateway": docker_gateway_settings()
|
|
||||||
},
|
|
||||||
"device_summary": device_summary
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
|
|
||||||
#def get_history_summary():
|
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
### Flask Routes
|
### Flask Routes
|
||||||
#######################################################################
|
#######################################################################
|
||||||
|
|
||||||
# full component list
|
# dynamic data
|
||||||
@app.route('/component_list', methods=['GET'])
|
# this will go to the redis server
|
||||||
def component_list():
|
@app.route('/dynamic_data', methods=['GET'])
|
||||||
count = request.args.get('count', type=int)
|
def dynamic_data():
|
||||||
return jsonify(get_component_list(count))
|
return jsonify(get_dynamic_data())
|
||||||
|
|
||||||
# component summary
|
# static data
|
||||||
@app.route('/component_summary', methods=['GET'])
|
@app.route('/static_data', methods=['GET'])
|
||||||
def component_summary():
|
def static_data():
|
||||||
return jsonify(get_component_summary())
|
return jsonify(get_static_data())
|
||||||
|
|
||||||
|
# redis data
|
||||||
|
@app.route('/redis_data', methods=['GET'])
|
||||||
|
def redis_data():
|
||||||
|
return jsonify(get_redis_data())
|
||||||
|
|
||||||
# full summary
|
# full summary
|
||||||
@app.route('/full_summary', methods=['GET'])
|
@app.route('/full_summary', methods=['GET'])
|
||||||
@ -180,27 +117,103 @@ def test():
|
|||||||
{
|
{
|
||||||
"component_count:": len(cosmostat_system.components),
|
"component_count:": len(cosmostat_system.components),
|
||||||
"user": jenkins_user_settings(),
|
"user": jenkins_user_settings(),
|
||||||
"hostname": jenkins_hostname_settings()
|
"hostname": jenkins_hostname_settings(),
|
||||||
|
"cpu_model": cosmostat_system.get_components(component_type="CPU").description
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#######################################################################
|
||||||
|
### 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():
|
||||||
|
return cosmostat_system.get_live_metrics()
|
||||||
|
|
||||||
|
def get_static_data():
|
||||||
|
result = []
|
||||||
|
for metric in cosmostat_system.get_system_properties():
|
||||||
|
result.append(metric)
|
||||||
|
for metric in cosmostat_system.get_component_properties():
|
||||||
|
result.append(metric)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_redis_data():
|
||||||
|
result = []
|
||||||
|
for metric in get_dynamic_data():
|
||||||
|
result.append(metric)
|
||||||
|
for metric in get_static_data():
|
||||||
|
result.append(metric)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_full_summary():
|
||||||
|
live_metrics = cosmostat_system.get_live_metrics()
|
||||||
|
system_components = cosmostat_system.get_component_strings()
|
||||||
|
system_info = get_info()
|
||||||
|
result = {
|
||||||
|
"system_settings":
|
||||||
|
{
|
||||||
|
"user": jenkins_user_settings(),
|
||||||
|
"hostname": jenkins_hostname_settings(),
|
||||||
|
"timestamp": jenkins_inventory_generation_timestamp_settings(),
|
||||||
|
"component_count:": len(cosmostat_system.components),
|
||||||
|
"object_name": cosmostat_system.name,
|
||||||
|
"docker_gateway": docker_gateway_settings()
|
||||||
|
},
|
||||||
|
"live_metrics": live_metrics,
|
||||||
|
"system_components": system_components,
|
||||||
|
"system_info": system_info
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_info():
|
||||||
|
component_strings = []
|
||||||
|
for component in cosmostat_system.get_components():
|
||||||
|
component_strings.append({"name": component.name, "description": component.description})
|
||||||
|
system_strings = []
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"hostname": jenkins_hostname_settings(),
|
||||||
|
"component_strings": component_strings
|
||||||
|
}
|
||||||
|
#for component_string in component_strings:
|
||||||
|
# for name, description in component_string.items():
|
||||||
|
# result[name] = description
|
||||||
|
return result
|
||||||
|
|
||||||
|
#######################################################################
|
||||||
|
### Other Functions
|
||||||
|
#######################################################################
|
||||||
|
|
||||||
|
# instantiate and return the System object
|
||||||
|
def new_cosmos_system():
|
||||||
|
new_system = System(f"{jenkins_hostname_settings()}")
|
||||||
|
if app_settings["log_output"]:
|
||||||
|
print(f"New system object name: {new_system.name} - {new_system.get_component_count()} components:")
|
||||||
|
for component in new_system.components:
|
||||||
|
print(component.description)
|
||||||
|
return new_system
|
||||||
|
|
||||||
|
|
||||||
|
# Background Loop Function
|
||||||
|
def background_loop():
|
||||||
|
# Update all data on the System object
|
||||||
|
cosmostat_system.update_system_state()
|
||||||
|
|
||||||
|
if app_settings["push_redis"]:
|
||||||
|
update_redis_server()
|
||||||
|
|
||||||
|
if app_settings["noisy_test"]:
|
||||||
|
print("Sorry about the mess...")
|
||||||
|
print(f"Blame {jenkins_user_settings()}")
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
### Main Subroutine
|
### Main Subroutine
|
||||||
#######################################################################
|
#######################################################################
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
||||||
# Background Loop Function
|
|
||||||
def background_loop():
|
|
||||||
# Update all data on the System object
|
|
||||||
cosmostat_system.update_values()
|
|
||||||
|
|
||||||
if app_settings["push_redis"]:
|
|
||||||
update_redis_server()
|
|
||||||
|
|
||||||
if app_settings["noisy_test"]:
|
|
||||||
print("Sorry about the mess...")
|
|
||||||
print(f"Blame {jenkins_user_settings()}")
|
|
||||||
|
|
||||||
# instantiate system
|
# instantiate system
|
||||||
cosmostat_system = new_cosmos_system()
|
cosmostat_system = new_cosmos_system()
|
||||||
@ -217,7 +230,7 @@ if __name__ == '__main__':
|
|||||||
scheduler.add_job(id='background_loop',
|
scheduler.add_job(id='background_loop',
|
||||||
func=background_loop,
|
func=background_loop,
|
||||||
trigger='interval',
|
trigger='interval',
|
||||||
seconds=1)
|
seconds=app_settings["update_frequency"])
|
||||||
scheduler.init_app(app)
|
scheduler.init_app(app)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|
||||||
|
|||||||
29
files/api/component_descriptors.json
Normal file
29
files/api/component_descriptors.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "CPU",
|
||||||
|
"description": "{model_name} with {core_count} cores.",
|
||||||
|
"multi_check": "False",
|
||||||
|
"properties": {
|
||||||
|
"core_count": "lscpu --json | jq -r '.lscpu[] | select(.field==\"CPU(s):\") | .data'",
|
||||||
|
"model_name": "lscpu --json | jq -r '.lscpu[] | select(.field==\"Model name:\") | .data'"
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"1m_load": "cat /proc/loadavg | awk '{print $1}'",
|
||||||
|
"5m_load": "cat /proc/loadavg | awk '{print $2}'",
|
||||||
|
"15m_load": "cat /proc/loadavg | awk '{print $3}'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RAM",
|
||||||
|
"description": "Total {bytes_total}GB in {module_count} modules.",
|
||||||
|
"multi_check": "False",
|
||||||
|
"properties": {
|
||||||
|
"bytes_total": "sudo lshw -json -c memory | jq -r '.[] | select(.description==\"System Memory\").size' | awk '{printf \"%.2f\\n\", $1/1073741824}'",
|
||||||
|
"module_count": "sudo lshw -json -c memory | jq -r '.[] | select(.id | contains(\"bank\")) | .id ' | wc -l"
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"used_capacity_mb": "free -m | grep Mem | awk '{print $3}'",
|
||||||
|
"free_capacity_mb": "free -m | grep Mem | awk '{print $4}'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -13,8 +13,8 @@
|
|||||||
This dashboard shows the local Matt-Cloud system stats.<p>
|
This dashboard shows the local Matt-Cloud system stats.<p>
|
||||||
</div>
|
</div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2>System Stats</h2>
|
<h2>Live System Metrics</h2>
|
||||||
<div id="host_stats" class="column">Connecting…</div>
|
<div id="host_metrics" class="column">Connecting…</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@ -1,126 +1,126 @@
|
|||||||
/* -------------------------------------------------------------
|
/* ------------------------------------------------------------
|
||||||
1. Socket‑IO connection & helper functions (unchanged)
|
1. Socket-IO connection & helper functions (unchanged)
|
||||||
------------------------------------------------------------- */
|
------------------------------------------------------------ */
|
||||||
const socket = io();
|
const socket = io();
|
||||||
|
|
||||||
socket.on('host_stats', renderStatsTable);
|
socket.on('host_metrics', renderStatsTable);
|
||||||
socket.on('connect_error', err => {
|
socket.on('connect_error', err => {
|
||||||
safeSetText('host_stats', `Could not connect to server - ${err.message}`);
|
safeSetText('host_metrics', `Could not connect to server - ${err.message}`);
|
||||||
});
|
});
|
||||||
socket.on('reconnect', attempt => {
|
socket.on('reconnect', attempt => {
|
||||||
safeSetText('host_stats', `Re‑connected (attempt ${attempt})`);
|
safeSetText('host_metrics', `Re-connected (attempt ${attempt})`);
|
||||||
});
|
});
|
||||||
|
|
||||||
function safeSetText(id, txt) {
|
function safeSetText(id, txt) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.textContent = txt;
|
if (el) el.textContent = txt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
2. Table rendering - the table remains a <table>
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function renderStatsTable(data) {
|
||||||
|
renderGenericTable('host_metrics', data, 'No Stats available');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGenericTable(containerId, data, emptyMsg) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (!Array.isArray(data) || !data.length) {
|
||||||
|
container.textContent = emptyMsg;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
/* Merge rows by name (new logic) */
|
||||||
2. Table rendering – the table remains a <table>
|
const mergedData = mergeRowsByName(data);
|
||||||
------------------------------------------------------------- */
|
|
||||||
function renderStatsTable(data) {
|
|
||||||
renderGenericTable('host_stats', data, 'No Stats available');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGenericTable(containerId, data, emptyMsg) {
|
/* Build the table from the merged data */
|
||||||
const container = document.getElementById(containerId);
|
const table = buildTable(mergedData);
|
||||||
if (!Array.isArray(data) || !data.length) {
|
table.id = 'host_metrics_table';
|
||||||
container.textContent = emptyMsg;
|
container.innerHTML = '';
|
||||||
return;
|
container.appendChild(table);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------
|
||||||
|
3. Merge rows by name (new logic)
|
||||||
|
------------------------------------------------------------ */
|
||||||
|
function mergeRowsByName(data) {
|
||||||
|
const groups = {}; // { name: { types: [], metrics: [], props: [], values: [] } }
|
||||||
|
|
||||||
|
data.forEach(row => {
|
||||||
|
const name = row.name;
|
||||||
|
if (!name) return; // ignore rows without a name
|
||||||
|
|
||||||
|
if (!groups[name]) {
|
||||||
|
groups[name] = { types: [], metrics: [], props: [], values: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 2️⃣ Merge “System Class Variable” rows first */
|
// Metric rows - contain type + metric
|
||||||
const mergedData = mergeSystemClassVariableRows(data);
|
if ('type' in row && 'metric' in row) {
|
||||||
|
groups[name].types.push(row.type);
|
||||||
/* 3️⃣ Build the table from the merged data */
|
groups[name].metrics.push(row.metric);
|
||||||
const table = buildTable(mergedData);
|
|
||||||
|
|
||||||
container.innerHTML = '';
|
|
||||||
container.appendChild(table);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
|
||||||
3. Merge consecutive rows whose type === "System Class Variable"
|
|
||||||
------------------------------------------------------------- */
|
|
||||||
function mergeSystemClassVariableRows(data) {
|
|
||||||
const result = [];
|
|
||||||
let i = 0;
|
|
||||||
|
|
||||||
while (i < data.length) {
|
|
||||||
const cur = data[i];
|
|
||||||
|
|
||||||
if (cur.type && cur.type.trim() === 'System Class Variable') {
|
|
||||||
const group = [];
|
|
||||||
while (
|
|
||||||
i < data.length &&
|
|
||||||
data[i].type &&
|
|
||||||
data[i].type.trim() === 'System Class Variable'
|
|
||||||
) {
|
|
||||||
group.push(data[i]);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Build one merged object – keep each column as an array */
|
|
||||||
const merged = { type: 'System Class Variable' };
|
|
||||||
const cols = Object.keys(group[0]).filter(k => k !== 'type');
|
|
||||||
|
|
||||||
cols.forEach(col => {
|
|
||||||
const vals = group
|
|
||||||
.map(row => row[col])
|
|
||||||
.filter(v => v !== undefined && v !== null);
|
|
||||||
merged[col] = vals; // ← array, not joined string
|
|
||||||
});
|
|
||||||
|
|
||||||
result.push(merged);
|
|
||||||
} else {
|
|
||||||
/* Normal row – just copy it */
|
|
||||||
result.push(cur);
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// Property rows - contain property + value
|
||||||
|
else if ('property' in row && 'value' in row) {
|
||||||
|
groups[name].props.push(row.property);
|
||||||
|
groups[name].values.push(row.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return result;
|
// Convert each group into a single row object
|
||||||
}
|
const merged = [];
|
||||||
|
Object.entries(groups).forEach(([name, grp]) => {
|
||||||
|
merged.push({
|
||||||
|
name,
|
||||||
|
type: grp.types, // array of types
|
||||||
|
metric: grp.metrics, // array of metrics
|
||||||
|
property: grp.props, // array of property names
|
||||||
|
value: grp.values, // array of property values
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* -------------------------------------------------------------
|
return merged;
|
||||||
4. Build an HTML table from an array of objects
|
}
|
||||||
------------------------------------------------------------- */
|
|
||||||
function buildTable(data) {
|
|
||||||
const cols = Object.keys(data[0]); // column order
|
|
||||||
const table = document.createElement('table');
|
|
||||||
|
|
||||||
/* Header */
|
/* ------------------------------------------------------------
|
||||||
const thead = table.createTHead();
|
4. Build an HTML table from an array of objects
|
||||||
const headerRow = thead.insertRow();
|
------------------------------------------------------------ */
|
||||||
|
function buildTable(data) {
|
||||||
|
const cols = ['name', 'type', 'metric', 'property', 'value']; // explicit order
|
||||||
|
const table = document.createElement('table');
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const thead = table.createTHead();
|
||||||
|
const headerRow = thead.insertRow();
|
||||||
|
cols.forEach(col => {
|
||||||
|
const th = document.createElement('th');
|
||||||
|
th.textContent = col;
|
||||||
|
headerRow.appendChild(th);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Body
|
||||||
|
const tbody = table.createTBody();
|
||||||
|
data.forEach(item => {
|
||||||
|
const tr = tbody.insertRow();
|
||||||
cols.forEach(col => {
|
cols.forEach(col => {
|
||||||
const th = document.createElement('th');
|
const td = tr.insertCell();
|
||||||
th.textContent = col;
|
|
||||||
headerRow.appendChild(th);
|
const val = item[col];
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
// Create a <span> for each item
|
||||||
|
val.forEach((v, idx) => {
|
||||||
|
td.id = 'host_metrics_column';
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = v;
|
||||||
|
td.appendChild(span);
|
||||||
|
|
||||||
|
// Insert a line break after every item except the last
|
||||||
|
if (idx < val.length - 1) td.appendChild(document.createElement('br'));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
td.textContent = val !== undefined ? val : '';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/* Body */
|
return table;
|
||||||
const tbody = table.createTBody();
|
}
|
||||||
data.forEach(item => {
|
|
||||||
const tr = tbody.insertRow();
|
|
||||||
cols.forEach(col => {
|
|
||||||
const td = tr.insertCell();
|
|
||||||
const val = item[col];
|
|
||||||
|
|
||||||
/* If the value is an array → render as <ol> */
|
|
||||||
if (Array.isArray(val)) {
|
|
||||||
const ol = document.createElement('ol');
|
|
||||||
val.forEach(v => {
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = v;
|
|
||||||
ol.appendChild(li);
|
|
||||||
});
|
|
||||||
td.appendChild(ol);
|
|
||||||
} else {
|
|
||||||
td.textContent = val; // normal text
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
@ -109,12 +109,13 @@ li {
|
|||||||
background-color: #2c3e50; /* Dark background for meter */
|
background-color: #2c3e50; /* Dark background for meter */
|
||||||
}
|
}
|
||||||
|
|
||||||
#host_stats td ol {
|
#host_metrics_column td {
|
||||||
list-style: none; /* removes the numeric markers */
|
list-style: none; /* removes the numeric markers */
|
||||||
padding-left: 0; /* remove the default left indent */
|
padding-left: 0; /* remove the default left indent */
|
||||||
margin-left: 0; /* remove the default left margin */
|
margin-left: 0; /* remove the default left margin */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#host_stats td ol li:nth-child(odd) { background: #34495e; }
|
#host_metrics_table tbody tr td :nth-of-type(even) {
|
||||||
#host_stats td ol li:nth-child(even) { background: #3e5c78; }
|
background-color: #23384e;
|
||||||
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ app.use(express.static('public'));
|
|||||||
|
|
||||||
// ---------- Redis subscriber ----------
|
// ---------- Redis subscriber ----------
|
||||||
const redisClient = createClient({
|
const redisClient = createClient({
|
||||||
url: 'redis://172.17.0.1:6379'
|
url: 'redis://192.168.37.1:6379'
|
||||||
});
|
});
|
||||||
redisClient.on('error', err => console.error('Redis error', err));
|
redisClient.on('error', err => console.error('Redis error', err));
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ redisClient.on('error', err => console.error('Redis error', err));
|
|||||||
await sub.connect();
|
await sub.connect();
|
||||||
// Subscribe to the channel that sends host stats
|
// Subscribe to the channel that sends host stats
|
||||||
await sub.subscribe(
|
await sub.subscribe(
|
||||||
['host_stats'],
|
['host_metrics'],
|
||||||
(message, channel) => { // <-- single handler
|
(message, channel) => { // <-- single handler
|
||||||
let payload;
|
let payload;
|
||||||
try {
|
try {
|
||||||
@ -38,21 +38,6 @@ redisClient.on('error', err => console.error('Redis error', err));
|
|||||||
io.emit(channel, payload);
|
io.emit(channel, payload);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
// Subscribe to the channel that sends history stats
|
|
||||||
await sub.subscribe(
|
|
||||||
['history_stats'],
|
|
||||||
(message, channel) => {
|
|
||||||
let payload;
|
|
||||||
try {
|
|
||||||
payload = JSON.parse(message); // message is a JSON string
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`Failed to parse ${channel}`, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
io.emit(channel, payload);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
sub.on('error', err => console.error('Subscriber error', err));
|
sub.on('error', err => console.error('Subscriber error', err));
|
||||||
})();
|
})();
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
- name: Cosmostat - API - Stop Service
|
- name: Cosmostat - API - Stop Service
|
||||||
|
become: true
|
||||||
|
become_user: "{{ service_user }}"
|
||||||
ignore_errors: yes
|
ignore_errors: yes
|
||||||
shell: "systemctl --user -M {{ service_user }}@ stop {{ api_service_name }}"
|
systemd:
|
||||||
|
name: "{{ api_service_name }}.service"
|
||||||
|
state: stopped
|
||||||
|
scope: user
|
||||||
|
|
||||||
- name: Cosmostat - API - copy api files
|
- name: Cosmostat - API - copy api files
|
||||||
copy:
|
copy:
|
||||||
@ -35,12 +40,14 @@
|
|||||||
group: "{{ service_user }}"
|
group: "{{ service_user }}"
|
||||||
mode: 0644
|
mode: 0644
|
||||||
|
|
||||||
- name: Cosmostat - API - Daemon Reload
|
- name: Cosmostat - API - Daemon Reload, Start, Enable
|
||||||
shell: "systemctl --user -M {{ service_user }}@ daemon-reload"
|
become: true
|
||||||
|
become_user: "{{ service_user }}"
|
||||||
- name: Cosmostat - API - Start Service
|
systemd:
|
||||||
shell: "systemctl --user -M {{ service_user }}@ start {{ api_service_name }}"
|
daemon_reload: yes
|
||||||
|
name: "{{ api_service_name }}.service"
|
||||||
|
state: started
|
||||||
|
enabled: yes
|
||||||
|
scope: user
|
||||||
|
|
||||||
...
|
...
|
||||||
@ -22,7 +22,6 @@
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
ipam_config:
|
ipam_config:
|
||||||
- subnet: "{{ docker_subnet }}"
|
- subnet: "{{ docker_subnet }}"
|
||||||
# - gateway: "{{ docker_gateway }}"
|
|
||||||
|
|
||||||
# allow service_user to sudo lshw without a password
|
# allow service_user to sudo lshw without a password
|
||||||
- name: Cosmostat - Init - cosmos user sudoers file creation
|
- name: Cosmostat - Init - cosmos user sudoers file creation
|
||||||
@ -31,8 +30,12 @@
|
|||||||
content: "{{ cosmostat_sudoers_content }}"
|
content: "{{ cosmostat_sudoers_content }}"
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
mode: 0600
|
mode: "0600"
|
||||||
|
|
||||||
|
# allow user services to "linger"
|
||||||
|
- name: Cosmostat - Init - cosmos user enable linger
|
||||||
|
shell: "loginctl enable-linger {{ service_user }}"
|
||||||
|
|
||||||
# create service working folder
|
# create service working folder
|
||||||
- name: Cosmostat - Init - create cosmostat service folder
|
- name: Cosmostat - Init - create cosmostat service folder
|
||||||
file:
|
file:
|
||||||
@ -102,11 +105,4 @@
|
|||||||
labels:
|
labels:
|
||||||
ws_node: "true"
|
ws_node: "true"
|
||||||
|
|
||||||
- name: Cosmostat - Init - node.js - Prune old containers
|
|
||||||
community.docker.docker_prune:
|
|
||||||
containers: true
|
|
||||||
containers_filters:
|
|
||||||
label:
|
|
||||||
ws_node: "true"
|
|
||||||
|
|
||||||
...
|
...
|
||||||
@ -40,17 +40,25 @@
|
|||||||
- name: docker container handler
|
- name: docker container handler
|
||||||
block:
|
block:
|
||||||
|
|
||||||
- name: service_control_website - template docker-compose.yaml
|
- name: Cosmostat - Web - template docker-compose.yaml
|
||||||
template:
|
template:
|
||||||
src: docker-compose.yaml
|
src: docker-compose.yaml
|
||||||
dest: "{{ service_control_web_folder }}/docker-compose.yaml"
|
dest: "{{ service_control_web_folder }}/docker-compose.yaml"
|
||||||
mode: 0644
|
mode: 0644
|
||||||
|
|
||||||
- name: "service_control_website - Start containers"
|
- name: Cosmostat - Web - Start containers
|
||||||
shell: "docker-compose -f {{ service_control_web_folder }}/docker-compose.yaml up -d"
|
shell: "docker-compose -f {{ service_control_web_folder }}/docker-compose.yaml up -d"
|
||||||
register: docker_output
|
register: docker_output
|
||||||
- debug: |
|
- debug: |
|
||||||
msg="{{ docker_output.stdout_lines }}"
|
msg="{{ docker_output.stdout_lines }}"
|
||||||
msg="{{ docker_output.stderr_lines }}"
|
msg="{{ docker_output.stderr_lines }}"
|
||||||
|
|
||||||
|
- name: Cosmostat - Web - Prune old containers
|
||||||
|
community.docker.docker_prune:
|
||||||
|
containers: true
|
||||||
|
containers_filters:
|
||||||
|
label:
|
||||||
|
ws_node: "true"
|
||||||
|
|
||||||
|
|
||||||
...
|
...
|
||||||
@ -28,4 +28,5 @@ debug_output: {{ debug_output }}
|
|||||||
push_redis: {{ push_redis }}
|
push_redis: {{ push_redis }}
|
||||||
run_background : {{ run_background }}
|
run_background : {{ run_background }}
|
||||||
log_output: {{ log_output }}
|
log_output: {{ log_output }}
|
||||||
|
update_frequency: {{ update_frequency }}
|
||||||
...
|
...
|
||||||
@ -8,7 +8,7 @@ services:
|
|||||||
container_name: redis
|
container_name: redis
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
ports:
|
ports:
|
||||||
- {{ (docker_gateway + ':') if not secure_api else '' }}6379:6379
|
- {{ docker_gateway }}:6379:6379
|
||||||
networks:
|
networks:
|
||||||
- cosmostat_net
|
- cosmostat_net
|
||||||
restart: always
|
restart: always
|
||||||
@ -23,7 +23,7 @@ services:
|
|||||||
- {{ service_control_web_folder }}/html:/usr/src/app/public
|
- {{ service_control_web_folder }}/html:/usr/src/app/public
|
||||||
ports:
|
ports:
|
||||||
# put back to 3000 if the stack is needed
|
# put back to 3000 if the stack is needed
|
||||||
- {{ (docker_gateway + ':') if not secure_api else '' }}80:3000
|
- {{ (docker_gateway + ':') if secure_api else '' }}80:3000
|
||||||
networks:
|
networks:
|
||||||
- cosmostat_net
|
- cosmostat_net
|
||||||
restart: always
|
restart: always
|
||||||
@ -35,7 +35,7 @@ services:
|
|||||||
# container_name: web_dash
|
# container_name: web_dash
|
||||||
# image: php:8.0-apache
|
# image: php:8.0-apache
|
||||||
# ports:
|
# ports:
|
||||||
# - {{ (docker_gateway + ':') if not secure_api else '' }}8080:80
|
# - {{ (docker_gateway + ':') if secure_api else '' }}8080:80
|
||||||
# volumes:
|
# volumes:
|
||||||
# - ./html:/var/www/html/
|
# - ./html:/var/www/html/
|
||||||
# networks:
|
# networks:
|
||||||
@ -46,7 +46,7 @@ services:
|
|||||||
# container_name: nginx_proxy
|
# container_name: nginx_proxy
|
||||||
# image: nginx:latest
|
# image: nginx:latest
|
||||||
# ports:
|
# ports:
|
||||||
# - "{{ (docker_gateway + ':') if not secure_api else '' }}80:80"
|
# - "{{ (docker_gateway + ':') if secure_api else '' }}80:80"
|
||||||
# volumes:
|
# volumes:
|
||||||
# - ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf
|
# - ./proxy/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||||
# networks:
|
# networks:
|
||||||
@ -60,10 +60,5 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
cosmostat_net:
|
cosmostat_net:
|
||||||
external: true
|
external: true
|
||||||
# driver: bridge
|
|
||||||
# ipam:
|
|
||||||
# driver: default
|
|
||||||
# config:
|
|
||||||
# -
|
|
||||||
# subnet: {{ docker_subnet }}
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user