166 lines
6.1 KiB
Python
166 lines
6.1 KiB
Python
import os
|
|
from flask import Flask, jsonify, render_template
|
|
from kubernetes import client, config
|
|
from kubernetes.client.rest import ApiException
|
|
|
|
app = Flask(__name__)
|
|
|
|
def get_k8s_client():
|
|
try:
|
|
config.load_incluster_config()
|
|
except config.ConfigException:
|
|
try:
|
|
config.load_kube_config()
|
|
except Exception as e:
|
|
print(f"Error loading kubeconfig: {e}")
|
|
# Try a fallback if they mounted to /root/.kube/config but KUBECONFIG is wrong
|
|
try:
|
|
config.load_kube_config(config_file="/root/.kube/config")
|
|
except Exception as e2:
|
|
raise RuntimeError(f"Failed to load kubeconfig. Make sure the file exists and is accessible. Error: {e}")
|
|
return client.CoreV1Api(), client.CustomObjectsApi()
|
|
|
|
def parse_cpu(cpu_str):
|
|
if not cpu_str: return 0
|
|
cpu_str = str(cpu_str)
|
|
if cpu_str.endswith('m'):
|
|
return int(cpu_str[:-1])
|
|
if cpu_str.endswith('n'):
|
|
return int(cpu_str[:-1]) // 1000000
|
|
try:
|
|
return int(float(cpu_str) * 1000)
|
|
except:
|
|
return 0
|
|
|
|
def parse_memory(mem_str):
|
|
if not mem_str: return 0
|
|
mem_str = str(mem_str)
|
|
if mem_str.endswith('Ki'):
|
|
return int(mem_str[:-2]) * 1024
|
|
if mem_str.endswith('Mi'):
|
|
return int(mem_str[:-2]) * 1024**2
|
|
if mem_str.endswith('Gi'):
|
|
return int(mem_str[:-2]) * 1024**3
|
|
try:
|
|
return int(mem_str)
|
|
except:
|
|
return 0
|
|
|
|
@app.route('/api/cluster-data')
|
|
def cluster_data():
|
|
v1, custom = get_k8s_client()
|
|
|
|
cluster = {
|
|
"name": "Cluster",
|
|
"children": []
|
|
}
|
|
|
|
try:
|
|
nodes = v1.list_node().items
|
|
pods = v1.list_pod_for_all_namespaces().items
|
|
|
|
node_metrics = {}
|
|
pod_metrics = {}
|
|
try:
|
|
nm = custom.list_cluster_custom_object("metrics.k8s.io", "v1beta1", "nodes")
|
|
for item in nm.get('items', []):
|
|
node_metrics[item['metadata']['name']] = item['usage']
|
|
|
|
pm = custom.list_cluster_custom_object("metrics.k8s.io", "v1beta1", "pods")
|
|
for item in pm.get('items', []):
|
|
key = f"{item['metadata']['namespace']}/{item['metadata']['name']}"
|
|
pod_metrics[key] = item['containers']
|
|
except ApiException:
|
|
pass # Metrics server not available
|
|
|
|
for node in nodes:
|
|
node_name = node.metadata.name
|
|
|
|
cpu_allocatable = parse_cpu(node.status.allocatable.get('cpu', '0'))
|
|
mem_allocatable = parse_memory(node.status.allocatable.get('memory', '0'))
|
|
|
|
node_usage_cpu = parse_cpu(node_metrics.get(node_name, {}).get('cpu', '0'))
|
|
node_usage_mem = parse_memory(node_metrics.get(node_name, {}).get('memory', '0'))
|
|
|
|
node_data = {
|
|
"name": node_name,
|
|
"type": "node",
|
|
"cpu_allocatable": cpu_allocatable,
|
|
"mem_allocatable": mem_allocatable,
|
|
"cpu_usage": node_usage_cpu,
|
|
"mem_usage": node_usage_mem,
|
|
"children": []
|
|
}
|
|
|
|
node_cpu_total = 0
|
|
node_mem_total = 0
|
|
|
|
for pod in pods:
|
|
if pod.spec.node_name == node_name:
|
|
pod_cpu_req = 0
|
|
pod_mem_req = 0
|
|
|
|
if pod.spec.containers:
|
|
for c in pod.spec.containers:
|
|
if c.resources and c.resources.requests:
|
|
pod_cpu_req += parse_cpu(c.resources.requests.get('cpu', '0'))
|
|
pod_mem_req += parse_memory(c.resources.requests.get('memory', '0'))
|
|
|
|
key = f"{pod.metadata.namespace}/{pod.metadata.name}"
|
|
if key in pod_metrics:
|
|
pod_cpu = sum([parse_cpu(c['usage'].get('cpu', '0')) for c in pod_metrics[key]])
|
|
pod_mem = sum([parse_memory(c['usage'].get('memory', '0')) for c in pod_metrics[key]])
|
|
else:
|
|
pod_cpu = pod_cpu_req
|
|
pod_mem = pod_mem_req
|
|
|
|
# D3 Treemap doesn't like 0 values, so minimum size 1
|
|
pod_cpu_val = max(pod_cpu, 1)
|
|
pod_mem_val = max(pod_mem, 1)
|
|
|
|
node_cpu_total += pod_cpu
|
|
node_mem_total += pod_mem
|
|
|
|
node_data["children"].append({
|
|
"name": pod.metadata.name,
|
|
"namespace": pod.metadata.namespace,
|
|
"type": "pod",
|
|
"cpu": pod_cpu_val,
|
|
"memory": pod_mem_val,
|
|
"cpu_raw": pod_cpu,
|
|
"memory_raw": pod_mem,
|
|
"status": pod.status.phase
|
|
})
|
|
|
|
# Optionally add an "Unused" block so the node size matches allocatable
|
|
unused_cpu = max(0, cpu_allocatable - node_cpu_total)
|
|
unused_mem = max(0, mem_allocatable - node_mem_total)
|
|
|
|
if unused_cpu > 0 or unused_mem > 0:
|
|
node_data["children"].append({
|
|
"name": "Available Capacity",
|
|
"namespace": "",
|
|
"type": "unused",
|
|
"cpu": max(unused_cpu, 1),
|
|
"memory": max(unused_mem, 1),
|
|
"cpu_raw": unused_cpu,
|
|
"memory_raw": unused_mem,
|
|
"status": "Available"
|
|
})
|
|
|
|
cluster["children"].append(node_data)
|
|
|
|
return jsonify(cluster)
|
|
|
|
except Exception as e:
|
|
import traceback
|
|
traceback.print_exc()
|
|
return jsonify({"error": str(e)}), 500
|
|
|
|
@app.route('/')
|
|
def index():
|
|
return render_template('index.html')
|
|
|
|
if __name__ == '__main__':
|
|
app.run(host='0.0.0.0', port=5000)
|