From fd7e0aaf31bafc92910ea5f92c0ae181a0a956a4 Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 17 Mar 2026 23:56:09 -0700 Subject: [PATCH] site updated with php for static data --- files/api/Components.py | 90 +++++++++++++++++++--------- files/api/app.py | 27 ++++++++- files/api/component_descriptors.json | 13 ++-- files/api/new_descriptors.json | 24 ++++++++ files/web/html/index.php | 75 ++++++++++++++++++++++- files/web/html/src/redis.js | 9 +-- 6 files changed, 193 insertions(+), 45 deletions(-) create mode 100644 files/api/new_descriptors.json diff --git a/files/api/Components.py b/files/api/Components.py index a32c0c3..a5637f1 100644 --- a/files/api/Components.py +++ b/files/api/Components.py @@ -27,10 +27,13 @@ class Component: # instantiate new component ############################################################ - def __init__(self, name: str, comp_type: str, this_device="None"): + def __init__(self, name: str, comp_type: str, this_device="None", is_virtual = "True"): self.name = name self.type = comp_type + # this variable is set when the device can have multiples + # it indicates that the commands in the descriptor might need templating self.this_device = this_device + self.is_virtual = is_virtual print(f"This device - {self.this_device}") # build the component descriptor dictionary for component in component_class_tree: @@ -46,13 +49,17 @@ class Component: # store static properties self.multi_check = self.is_multi() self.virt_ignore = self._descriptor.get('virt_ignore', []) + if self.is_virtual: + self.virt_ignore = [] self._properties: Dict[str, str] = {} for key, command in descriptor.get('properties', {}).items(): if self.this_device != "None": + # this means this component type is a multi and the commands need templating for each device formatted_command = command.format(this_device=self.this_device) self._properties[key] = run_command(formatted_command, True) else: - self._properties[key] = run_command(command, True) + self._properties[key] = run_command(command, zero_only = True) + print(self._properties[key]) # build the description string self._description_template: str | None = descriptor.get("description") self.description = self._description_template.format(**self._properties) @@ -82,10 +89,13 @@ class Component: if this_metric is not None: self._metrics[key] = this_metric else: - self._metrics[key] = run_command(command, True) + self._metrics[key] = run_command(command, zero_only = True) - def get_property(self, type): - return self._properties[type] + def get_property(self, type = None): + if type == None: + return self._properties + else: + return self._properties[type] def is_multi(self): for component_type in component_types: @@ -97,9 +107,14 @@ class Component: # redis data functions ######################################################## - def get_properties_keys(self): + def get_properties_keys(self, component = None): result = [] - for name, value in self._properties.items(): + component_properties = [] + if component == None: + component_properties = self._properties.items() + else: + component_properties = self.get_property(component) + for name, value in component_properties: this_property = { "Source": self.name, "Property": name, @@ -109,15 +124,21 @@ class Component: result.append(this_property) return result - def get_properties_strings(self): + def get_properties_strings(self, return_simple = False): result = [] - for name, value in self._properties.items(): - this_property = { + component_properties = self._properties.items() + print(component_properties) + for name, value in component_properties: + simple_property = f"{name}: {value}" + complex_property = { "Source": self.name, - "Property": f"{name}: {value}" + "Property": simple_property } - if name not in self.virt_ignore: - result.append(this_property) + if name not in self.virt_ignore: + if return_simple: + result.append(simple_property) + else: + result.append(complex_property) return result def get_metrics_keys(self): @@ -167,7 +188,7 @@ class Component: return result # complex data type return - def get_properties(self, type = None): + def get_property_summary(self, type = None): these_properties = [] if type == None: for name, value in self._properties.items(): @@ -212,7 +233,7 @@ class System: static_key_variables = [ {"name": "Hostname", "command": "hostname"}, - {"name": "Virtual Machine", "command": "echo $([[ \"$(systemd-detect-virt)\" == none ]] && echo False || echo True)"}, + {"name": "Virtual Machine", "command": 'echo $( [ "$(systemd-detect-virt)" = none ] && echo False || echo True )', "req_check": "False"}, {"name": "CPU Architecture:", "command": "lscpu --json | jq -r '.lscpu[] | select(.field==\"Architecture:\") | .data'"}, {"name": "OS Kernel", "command": "uname -r"}, {"name": "OS Name", "command": "cat /etc/os-release | grep PRETTY | cut -d\\\" -f2"}, @@ -244,13 +265,20 @@ class System: # initialize system properties and metrics dicts self._properties: Dict[str, str] = {} self._metrics: Dict[str, str] = {} + self._virt_string = run_command('systemd-detect-virt', zero_only = True, req_check = False) + self._virt_ignore = self.virt_ignore + if self._virt_string == "none": + self._virt_ignore = [] # timekeeping for websocket - self.recent_check = int(time.time()) + self.recent_check = int(time.time()) # load static keys for static_key in self.static_key_variables: - if static_key["name"] not in self.virt_ignore: + if static_key["name"] not in self._virt_ignore: command = static_key["command"] - result = run_command(command, True) + if "req_check" in static_key: + result = run_command(command, zero_only = True, req_check = static_key["req_check"]) + else: + result = run_command(command, zero_only = True) if debug_output: print(f'Static key [{static_key["name"]}] - command [{command}] - output [{result}]') self._properties[static_key["name"]] = result @@ -272,7 +300,7 @@ class System: for live_key in self.dynamic_key_variables: if live_key['command'] is not None: command = live_key['command'] - result = run_command(command, True) + result = run_command(command, zero_only = True) self._metrics[live_key['name']] = result if debug_output: print(f'Command {live_key["name"]} - [{command}] Result - [{result}]') @@ -298,12 +326,12 @@ class System: this_component_letter = letters[component_type_device_list.index(this_device)] this_component_name = f"{component_name} {this_component_letter}" print(f"{this_component_name} - {component_name} - {this_device}") - self.add_components(Component(this_component_name, component_name, this_device)) + self.add_components(Component(name = this_component_name, comp_type = component_name, this_device = this_device)) else: if debug_output: print(f'Creating component {component["name"]}') - self.add_components(Component(component_name, component_name)) + self.add_components(Component(name = component_name, comp_type = component_name, is_virtual = self.is_virtual())) # Add a component to the system def add_components(self, component: Component,): @@ -332,9 +360,17 @@ class System: def get_component_count(self): result = int(len(self.components)) return result + + def get_property(self, property = None): + if property == None: + return None + else: + return self._properties.get(property, {}) def is_virtual(self): - virt_check = self._properties.get('virt_ignore', {}).items() + vm_check = self.get_property('Virtual Machine') + print(f'vm_check: {vm_check}') + return vm_check def check_system_timer(self): time_lapsed = time.time() - float(self.recent_check) @@ -353,14 +389,14 @@ class System: result.append(system_property) return result - def get_component_properties(self, human_readable = False): + def get_component_properties(self, human_readable = False, component = None): result = [] for component in self.components: if human_readable: - for metric in component.get_properties_strings(): + for metric in component.get_properties_strings(component = component): result.append(metric) else: - for metric in component.get_properties_keys(): + for metric in component.get_properties_keys(component = component): result.append(metric) return result @@ -486,9 +522,9 @@ class System: ############################################################ # 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, use_shell=True, req_check = True): # Run the command and capture the output - result = subprocess.run(cmd, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + result = subprocess.run(cmd, shell=use_shell, check=req_check, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # Decode the byte output to a string output = result.stdout.decode('utf-8') # Split the output into lines and store it in an array diff --git a/files/api/app.py b/files/api/app.py index 3317f7e..9d6e09d 100644 --- a/files/api/app.py +++ b/files/api/app.py @@ -111,6 +111,11 @@ def redis_strings(): def full_summary(): return jsonify(get_full_summary()) +# php summary +@app.route('/php_summary', methods=['GET']) +def php_summary(): + return jsonify(get_php_summary()) + # system info @app.route('/info', methods=['GET']) def info(): @@ -169,12 +174,13 @@ def get_static_data(human_readable = False): result = [] return cosmostat_system.get_static_metrics(human_readable) +# php is about to start rendering static data def get_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) + #for metric in get_static_data(human_readable): + # result.append(metric) return result def get_full_summary(): @@ -212,6 +218,23 @@ def get_info(): # result[name] = description return result +def get_php_summary(): + system_properties = cosmostat_system.get_system_properties(human_readable = True) + system_components = [] + for component in cosmostat_system.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 + ####################################################################### ### Other Functions ####################################################################### diff --git a/files/api/component_descriptors.json b/files/api/component_descriptors.json index 8b5914b..40482cc 100644 --- a/files/api/component_descriptors.json +++ b/files/api/component_descriptors.json @@ -22,9 +22,9 @@ "properties": { "Total GB": "sudo /usr/bin/lshw -json -c memory | jq -r '.[] | select(.description==\"System Memory\").size' | awk '{printf \"%.2f\\n\", $1/1073741824}'", "RAM Module Count": "sudo /usr/bin/lshw -json -c memory | jq -r '.[] | select(.id | contains(\"bank\")) | .id ' | wc -l", - "RAM Type": "/usr/sbin/dmidecode --type 17 | grep Type: | sort -u | cut -d: -f2 | xargs", - "RAM Speed": "/usr/sbin/dmidecode --type 17 | grep Speed: | grep -v Configured | sort -u | cut -d: -f2 | xargs", - "RAM Voltage": "/usr/sbin/dmidecode --type 17 | grep 'Configured Voltage' | sort -u | cut -d: -f2 | xargs" + "RAM Type": "sudo /usr/sbin/dmidecode --type 17 | grep Type: | sort -u | cut -d: -f2 | xargs", + "RAM Speed": "sudo /usr/sbin/dmidecode --type 17 | grep Speed: | grep -v Configured | sort -u | cut -d: -f2 | xargs", + "RAM Voltage": "sudo /usr/sbin/dmidecode --type 17 | grep 'Configured Voltage' | sort -u | cut -d: -f2 | xargs" }, "metrics": { "MB Used": "free -m | grep Mem | awk '{print $3}'", @@ -45,12 +45,11 @@ "Device Name": "lsblk -d -o NAME,SIZE | grep -v -e 0B -e NAME | awk '{{print $1}}' | grep {this_device}", "Device Path": "lsblk -d -o NAME,SIZE | grep -v -e 0B -e NAME | awk '{{print \"/dev/\"$1}}' | grep {this_device}", "Drive Type": "lsblk -d -o NAME,TRAN | grep {this_device} | awk '{{print $2}}'", - "Total Capacity": "lsblk -d -o NAME,SIZE | grep {this_device} | awk '{{print $2}}'" + "Total Capacity": "lsblk -d -o NAME,SIZE | grep {this_device} | awk '{{print $2}}'", + "SMART Check": "sudo /usr/sbin/smartctl -x --json /dev/{this_device} | jq -r .smart_status.passed" }, "metrics": { - "SMART Check": "/usr/sbin/smartctl -x --json /dev/{this_device} | jq -r .smart_status.passed", - "SATA GBW": "/usr/sbin/smartctl -x --json /dev/{this_device} | jq -r '.physical_block_size as $block |.ata_device_statistics.pages[] | select(.name == \"General Statistics\") | .table[] | select(.name == \"Logical Sectors Written\") | .value as $sectors | ($sectors * $block) / 1073741824 ' | awk '{{printf \"%.2f GiB Written\\n\", $0}}' || true", - "NVMe GBW": "/usr/sbin/smartctl -x --json /dev/{this_device} | jq -r ' .nvme_smart_health_information_log.data_units_written as $dw | .logical_block_size as $ls | ($dw * $ls) / 1073741824 ' | awk '{{printf \"%.2f GiB Written\\n\", $0}}' || true" + "placeholder": "" } } ] \ No newline at end of file diff --git a/files/api/new_descriptors.json b/files/api/new_descriptors.json new file mode 100644 index 0000000..bf2bd18 --- /dev/null +++ b/files/api/new_descriptors.json @@ -0,0 +1,24 @@ +[ + { + "name": "LAN", + "description": "", + "multi_check": "True", + "device_list": "", + "properties": { + "MAC Address": "", + "Device Name": "", + "Device ID": "" + }, + "metrics": { + "IP Address": "", + "MB Transmitted": "", + "MB Received": "", + "Link State": "", + "Link Speed": "" + } + }, + { + "SATA GBW": "sudo /usr/sbin/smartctl -x --json /dev/{this_device} | jq -r '.physical_block_size as $block |.ata_device_statistics.pages[] | select(.name == \"General Statistics\") | .table[] | select(.name == \"Logical Sectors Written\") | .value as $sectors | ($sectors * $block) / 1073741824 ' | awk '{{printf \"%.2f GiB Written\\n\", $0}}' || true", + "NVMe GBW": "sudo /usr/sbin/smartctl -x --json /dev/{this_device} | jq -r ' .nvme_smart_health_information_log.data_units_written as $dw | .logical_block_size as $ls | ($dw * $ls) / 1073741824 ' | awk '{{printf \"%.2f GiB Written\\n\", $0}}' || true" + } +] \ No newline at end of file diff --git a/files/web/html/index.php b/files/web/html/index.php index 4f315b8..fd34938 100644 --- a/files/web/html/index.php +++ b/files/web/html/index.php @@ -4,8 +4,17 @@ Matt-Cloud Cosmostat + - +
@@ -13,13 +22,73 @@ This dashboard shows the local Matt-Cloud system stats.

-

Live System Metrics

-
Connecting…
+

System Properties and Components

+
+ + + + [ + 'timeout' => 5, // seconds + 'header' => "User-Agent: PHP/" . PHP_VERSION . "\r\n" + ] + ]); + $json = @file_get_contents($apiUrl, false, $context); + if ($json === false) { + die('

Could not fetch data from the API.

'); + } + $data = json_decode($json, true); + if ($data === null) { + die('

Malformed JSON returned from the API.

'); + } + function h(string $s): string + { + return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); + } + ?> + + +

System Properties

+
    + +
  • + +
+ + + +

Components

+
+ +
+

+
    + +
  • + +
+
+ +
+ + + + +
+ +
+

Live System Metrics

+
Connecting...
+
+ diff --git a/files/web/html/src/redis.js b/files/web/html/src/redis.js index bc77270..dcfcdf0 100644 --- a/files/web/html/src/redis.js +++ b/files/web/html/src/redis.js @@ -54,15 +54,12 @@ function mergeRowsByName(data) { const source = row.Source; // <-- changed if (!source) return; if (!groups[source]) { - groups[source] = { Metric: [], Data: [], Property: [], Value: [] }; + groups[source] = { Metric: [], Data: [] }; } if ('Metric' in row && 'Data' in row) { groups[source].Metric.push(row.Metric); groups[source].Data.push(row.Data); - } else if ('Property' in row && 'Value' in row) { - groups[source].Property.push(row.Property); - groups[source].Value.push(row.Value); - } + } }); const merged = []; @@ -107,7 +104,7 @@ function orderRows(rows) { 4. Build an HTML table from an array of objects ------------------------------------------------------------ */ function buildTable(data) { - const cols = ['Source', 'Property', 'Value', 'Metric', 'Data']; // explicit order + const cols = ['Source', 'Metric', 'Data']; // explicit order const table = document.createElement('table'); // Header