Add CPU usage to host inventory
This commit is contained in:
+33
-2
@@ -30,7 +30,7 @@ import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
VERSION = "1.4.1"
|
||||
VERSION = "1.4.2"
|
||||
LABEL = "io.electric-cherry"
|
||||
MAX_FILE = 32 * 1024 * 1024
|
||||
LIBRARY_DOWNLOAD_WORKERS = 4
|
||||
@@ -710,6 +710,8 @@ class Agent:
|
||||
self.docker = docker or Docker()
|
||||
self.stopping = threading.Event()
|
||||
self.current_operation = None
|
||||
self.cpu_sample_lock = threading.Lock()
|
||||
self.cpu_sample = None
|
||||
self.instance_locks = {}
|
||||
self.read_gate = ReadWriteGate()
|
||||
self.read_claim_lock = threading.Lock()
|
||||
@@ -899,7 +901,36 @@ class Agent:
|
||||
except (OSError, ValueError):
|
||||
raise AgentError("Controller request failed") from None
|
||||
|
||||
def cpu_usage_percent(self):
|
||||
"""Host-wide busy time since the previous inventory sample, without waiting."""
|
||||
with self.cpu_sample_lock:
|
||||
previous = self.cpu_sample
|
||||
self.cpu_sample = None
|
||||
try:
|
||||
with Path("/proc/stat").open() as source:
|
||||
fields = source.readline().split()
|
||||
if not fields or fields[0] != "cpu" or len(fields) < 9:
|
||||
return None
|
||||
# Guest time is already included in user/nice. Idle and I/O
|
||||
# wait are not busy; steal counts as unavailable CPU capacity.
|
||||
counters = tuple(int(value) for value in fields[1:9])
|
||||
if any(value < 0 for value in counters):
|
||||
return None
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
self.cpu_sample = counters
|
||||
if previous is None:
|
||||
return None
|
||||
deltas = [current - old for current, old in zip(counters, previous)]
|
||||
total = sum(deltas)
|
||||
# Counters can reset and Linux can revise I/O wait downwards.
|
||||
# Rebase rather than reporting a misleading utilization value.
|
||||
if total <= 0 or any(delta < 0 for delta in deltas):
|
||||
return None
|
||||
return round(100 * (total - deltas[3] - deltas[4]) / total, 1)
|
||||
|
||||
def inventory(self):
|
||||
cpu_usage = self.cpu_usage_percent()
|
||||
usage = shutil.disk_usage(self.root)
|
||||
memory = {}
|
||||
try:
|
||||
@@ -1004,7 +1035,7 @@ class Agent:
|
||||
os_name = line.split("=", 1)[1].strip('"')
|
||||
update = self.update_inventory()
|
||||
capabilities = ["instance.cancel-create", "operations.read-lane"] + (["host.decommission"] if self.can_uninstall() else []) + (["agent.update"] if update["enabled"] else [])
|
||||
return {"hostname": socket.gethostname(), "agentVersion": VERSION, "protocolVersion": 1, "capabilities": capabilities, "agentUpdate": update, "os": os_name, "osVersion": platform.release(), "architecture": platform.machine(), "cpu": platform.processor() or platform.machine(), "cpuCount": os.cpu_count() or 0, "memoryTotal": memory.get("MemTotal", 0), "memoryAvailable": memory.get("MemAvailable", 0), "storageTotal": usage.total, "storageAvailable": usage.free, "dockerVersion": docker_version, "dockerRunning": docker_running, "directoriesWritable": os.access(self.root, os.W_OK), "runtimeImages": sorted(set(images)), "libraries": libraries, "libraryCaches": caches, "currentOperation": self.current_operation, "portRange": self.config.get("portRange", {"min": 28960, "max": 29060})}
|
||||
return {"hostname": socket.gethostname(), "agentVersion": VERSION, "protocolVersion": 1, "capabilities": capabilities, "agentUpdate": update, "os": os_name, "osVersion": platform.release(), "architecture": platform.machine(), "cpu": platform.processor() or platform.machine(), "cpuCount": os.cpu_count() or 0, **({"cpuUsagePercent": cpu_usage} if cpu_usage is not None else {}), "memoryTotal": memory.get("MemTotal", 0), "memoryAvailable": memory.get("MemAvailable", 0), "storageTotal": usage.total, "storageAvailable": usage.free, "dockerVersion": docker_version, "dockerRunning": docker_running, "directoriesWritable": os.access(self.root, os.W_OK), "runtimeImages": sorted(set(images)), "libraries": libraries, "libraryCaches": caches, "currentOperation": self.current_operation, "portRange": self.config.get("portRange", {"min": 28960, "max": 29060})}
|
||||
|
||||
def safe_to_disrupt(self, instance_id, generation):
|
||||
health = self.inspect(instance_id, generation)
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location("ec_agent", Path(__file__).resolve().parents[1] / "ec_agent.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
@@ -74,6 +74,39 @@ class AgentTests(unittest.TestCase):
|
||||
self.agent.journal.close()
|
||||
self.temp.cleanup()
|
||||
|
||||
def cpu_sample(self, line):
|
||||
with patch.object(Path, "open", mock_open(read_data=line)):
|
||||
return self.agent.cpu_usage_percent()
|
||||
|
||||
def test_cpu_usage_uses_interval_and_excludes_guest_double_counting(self):
|
||||
self.assertIsNone(self.cpu_sample("cpu 100 0 50 800 50 0 0 0 20 0\n"))
|
||||
# 30 busy ticks (including steal), 60 idle, 10 I/O wait = 30%.
|
||||
self.assertEqual(self.cpu_sample("cpu 120 0 55 860 60 0 0 5 30 0\n"), 30.0)
|
||||
self.assertEqual(self.cpu_sample("cpu 120 0 55 960 60 0 0 5 30 0\n"), 0.0)
|
||||
self.assertEqual(self.cpu_sample("cpu 220 0 55 960 60 0 0 5 30 0\n"), 100.0)
|
||||
|
||||
def test_cpu_usage_rebases_after_reset_or_invalid_sample(self):
|
||||
baseline = "cpu 100 0 50 800 50 0 0 0\n"
|
||||
self.assertIsNone(self.cpu_sample(baseline))
|
||||
self.assertIsNone(self.cpu_sample(baseline))
|
||||
self.assertIsNone(self.cpu_sample("cpu 10 0 5 80 5 0 0 0\n"))
|
||||
self.assertIsNone(self.cpu_sample("cpu 20 0 5 90 4 0 0 0\n"))
|
||||
for invalid in ("", "cpu0 1 2 3 4 5 6 7 8", "cpu 1 2", "cpu x 0 0 0 0 0 0 0", "cpu -1 0 0 0 0 0 0 0"):
|
||||
self.assertIsNone(self.cpu_sample(invalid))
|
||||
self.assertIsNone(self.cpu_sample(baseline))
|
||||
with patch.object(Path, "open", side_effect=OSError("unavailable")):
|
||||
self.assertIsNone(self.agent.cpu_usage_percent())
|
||||
self.assertIsNone(self.cpu_sample(baseline))
|
||||
|
||||
def test_inventory_reports_cpu_only_when_available(self):
|
||||
for usage in (None, 0.0, 42.3, 100.0):
|
||||
with patch.object(self.agent, "cpu_usage_percent", return_value=usage):
|
||||
inventory = self.agent.inventory()
|
||||
if usage is None:
|
||||
self.assertNotIn("cpuUsagePercent", inventory)
|
||||
else:
|
||||
self.assertEqual(inventory["cpuUsagePercent"], usage)
|
||||
|
||||
def operation(self, kind, payload=None, op_id="operation-1", generation=1):
|
||||
return {"id": op_id, "kind": kind, "instanceId": INSTANCE, "generation": generation, "payload": payload or {}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user