host === null || $instance->vmid === null) { return null; } $node = $instance->host->node ?? 'pve'; $client = $this->pve->forHost($instance->host); if (! $client->guestAgentPing($node, (int) $instance->vmid)) { return null; } // -B1 so the numbers are bytes and no locale can reinterpret them. $result = $client->guestExec($node, (int) $instance->vmid, 'df -B1 --output=size,used /var/www 2>/dev/null || df -B1 --output=size,used /'); $out = trim((string) ($result['out-data'] ?? $result['output'] ?? '')); // Header line, then one line of two integers. Anything else is a shell // that answered something we did not ask for, and is discarded rather // than parsed hopefully. $lines = preg_split('/\r?\n/', $out) ?: []; $last = trim((string) end($lines)); if (preg_match('/^(\d+)\s+(\d+)$/', $last, $m) !== 1) { return null; } return ['total' => (int) $m[1], 'used' => (int) $m[2]]; } /** * Take a reading and write it into today's row, so everything that reads the * fill level sees it. * * The same row the sampler accumulates into, and the fill level is * overwritten rather than added to — it is a state, not a flow. Returns null * when there was nothing to record, so a caller can say "we could not look" * instead of "you are fine". */ public function record(Instance $instance): ?InstanceMetric { $disk = $this->read($instance); if ($disk === null) { return null; } $metric = InstanceMetric::query()->firstOrCreate( ['instance_id' => $instance->id, 'day' => now()->toDateString()], ); $metric->disk_used_bytes = $disk['used']; $metric->disk_total_bytes = $disk['total']; $metric->save(); return $metric; } }