<?phpnamespace App\Model;/** * The objects of one pool as XAPI gives them (records by class and ref), kept up to date by event.from, with the questions the * windows ask: the VMs of a host, the storage of a VM, the pool master... */final class Cache{ public const NULL = 'OpaqueRef:NULL'; /** @var array<string, array<string, array<string, mixed>>> class => ref => record */ public array $records = []; /** @var array<string, string> ref => class */ private array $classOf = []; /** bumped at each change: the windows redraw what depends on it */ public int $version = 0; /** @param array<string, mixed> $record */ public function put(string $class, string $ref, array $record): void { $class = strtolower($class); $this->records[$class][$ref] = $record; $this->classOf[$ref] = $class; $this->version++; } public function remove(string $class, string $ref): void { unset($this->records[strtolower($class)][$ref], $this->classOf[$ref]); $this->version++; } /** @return array<string, mixed>|null */ public function get(string $ref): ?array { $class = $this->classOf[$ref] ?? null; return $class === null ? null : ($this->records[$class][$ref] ?? null); } public function classOf(string $ref): ?string { return $this->classOf[$ref] ?? null; } /** @return array<string, array<string, mixed>> */ public function all(string $class): array { return $this->records[$class] ?? []; } public function field(string $ref, string $field, mixed $default = null): mixed { return $this->get($ref)[$field] ?? $default; } public function byName(string $class, string $name): ?string { foreach ($this->all($class) as $ref => $record) { if (($record['name_label'] ?? '') === $name) { return $ref; } } return null; } public function byUuid(string $class, string $uuid): ?string { foreach ($this->all($class) as $ref => $record) { if (($record['uuid'] ?? '') === $uuid) { return $ref; } } return null; } // the pool -------------------------------------------------------------------------------------------------------- public function poolRef(): ?string { return array_key_first($this->all('pool')); } public function master(): ?string { $pool = $this->poolRef(); return $pool === null ? null : (string)$this->field($pool, 'master'); } /** A pool of one host shows as the host itself (as in XenCenter): the pool has no name. */ public function isRealPool(): bool { $pool = $this->poolRef(); return $pool !== null && (count($this->all('host')) > 1 || trim((string)$this->field($pool, 'name_label')) !== ''); } /** @return array<string, array<string, mixed>> hosts sorted by name */ public function hosts(): array { $hosts = $this->all('host'); uasort($hosts, fn ($a, $b) => strnatcasecmp((string)$a['name_label'], (string)$b['name_label'])); return $hosts; } // VMs --------------------------------------------------------------------------------------------------------------- /** @param array<string, mixed> $vm */ public static function isRealVm(array $vm): bool { return empty($vm['is_a_template']) && empty($vm['is_a_snapshot']) && empty($vm['is_control_domain']); } /** @return array<string, array<string, mixed>> the VMs (no template, snapshot or dom0), sorted by name */ public function vms(): array { $vms = array_filter($this->all('vm'), [self::class, 'isRealVm']); uasort($vms, fn ($a, $b) => strnatcasecmp((string)$a['name_label'], (string)$b['name_label'])); return $vms; } /** @return array<string, array<string, mixed>> VMs running (or paused) on a host */ public function vmsOn(string $host): array { return array_filter($this->vms(), fn ($vm) => ($vm['resident_on'] ?? '') === $host); } /** * Halted / suspended VMs: under the host they are bound to (affinity, or local storage), else under the pool. * * @return array<string, array<string, mixed>> */ public function vmsHomedOn(?string $host): array { return array_filter($this->vms(), function (array $vm, string $ref) use ($host): bool { if (isset($this->all('host')[(string)($vm['resident_on'] ?? '')])) { return false; } return $this->homeOf($ref) === $host; }, ARRAY_FILTER_USE_BOTH); } /** The host a halted VM belongs to: the one of its local disks, else none (pool level). */ public function homeOf(string $vm): ?string { foreach ($this->disksOf($vm) as $disk) { $sr = (string)($disk['vdi']['SR'] ?? ''); if ($sr !== '' && !$this->field($sr, 'shared', true)) { $host = $this->hostOfLocalSr($sr); if ($host !== null) { return $host; } } } if (!$this->isRealPool()) { return $this->master(); } return null; } public function hostOfLocalSr(string $sr): ?string { foreach ((array)$this->field($sr, 'PBDs', []) as $pbd) { $host = (string)$this->field((string)$pbd, 'host', ''); if ($host !== '' && $this->get($host) !== null) { return $host; } } return null; } /** @return array<string, array<string, mixed>> */ public function templates(): array { $out = array_filter($this->all('vm'), fn ($vm) => !empty($vm['is_a_template']) && empty($vm['is_a_snapshot'])); uasort($out, fn ($a, $b) => strnatcasecmp((string)$a['name_label'], (string)$b['name_label'])); return $out; } /** @return array<string, array<string, mixed>> newest first */ public function snapshotsOf(string $vm): array { $out = []; foreach ((array)$this->field($vm, 'snapshots', []) as $ref) { if (($record = $this->get((string)$ref)) !== null) { $out[(string)$ref] = $record; } } uasort($out, fn ($a, $b) => strcmp((string)$b['snapshot_time'], (string)$a['snapshot_time'])); return $out; } /** @return list<array{ref: string, vbd: array<string, mixed>, vdi: array<string, mixed>|null}> by position */ public function disksOf(string $vm, bool $withCd = false): array { $out = []; foreach ((array)$this->field($vm, 'VBDs', []) as $ref) { $vbd = $this->get((string)$ref); if ($vbd === null || (!$withCd && ($vbd['type'] ?? '') === 'CD')) { continue; } $vdi = $this->get((string)($vbd['VDI'] ?? '')); $out[] = ['ref' => (string)$ref, 'vbd' => $vbd, 'vdi' => $vdi]; } usort($out, fn ($a, $b) => (int)$a['vbd']['userdevice'] <=> (int)$b['vbd']['userdevice']); return $out; } /** @return list<array{ref: string, vif: array<string, mixed>}> */ public function vifsOf(string $vm): array { $out = []; foreach ((array)$this->field($vm, 'VIFs', []) as $ref) { if (($vif = $this->get((string)$ref)) !== null) { $out[] = ['ref' => (string)$ref, 'vif' => $vif]; } } usort($out, fn ($a, $b) => (int)$a['vif']['device'] <=> (int)$b['vif']['device']); return $out; } /** @return array<string, array<string, mixed>> the SRs a host sees (PBDs) */ public function srsOf(string $host): array { $out = []; foreach ((array)$this->field($host, 'PBDs', []) as $pbd) { $sr = (string)$this->field((string)$pbd, 'SR', ''); if (($record = $this->get($sr)) !== null) { $out[$sr] = $record; } } uasort($out, fn ($a, $b) => strnatcasecmp((string)$a['name_label'], (string)$b['name_label'])); return $out; } /** @return array<string, array<string, mixed>> all the SRs, shared ones first */ public function srs(): array { $out = array_filter($this->all('sr'), fn ($sr) => ($sr['type'] ?? '') !== 'udev' || !empty($sr['VDIs'])); uasort($out, fn ($a, $b) => [(int)!$a['shared'], strtolower((string)$a['name_label'])] <=> [(int)!$b['shared'], strtolower((string)$b['name_label'])]); return $out; } /** @return array<string, array<string, mixed>> the networks guests can use */ public function networks(): array { $out = array_filter($this->all('network'), fn ($n) => ($n['other_config']['is_host_internal_management_network'] ?? '') !== 'true'); uasort($out, fn ($a, $b) => strnatcasecmp((string)$a['name_label'], (string)$b['name_label'])); return $out; } /** @return array<string, array<string, mixed>> the PIFs (network cards) of a host, physical first */ public function pifsOf(string $host): array { $out = []; foreach ((array)$this->field($host, 'PIFs', []) as $ref) { if (($pif = $this->get((string)$ref)) !== null) { $out[(string)$ref] = $pif; } } uasort($out, fn ($a, $b) => [(int)$a['VLAN'] !== -1, $a['device']] <=> [(int)$b['VLAN'] !== -1, $b['device']]); return $out; } /** @return array<string, array<string, mixed>> newest first */ public function tasks(): array { $out = $this->all('task'); uasort($out, fn ($a, $b) => strcmp((string)$b['created'], (string)$a['created'])); return $out; } /** @return array<string, array<string, mixed>> alerts, newest first */ public function messages(): array { $out = $this->all('message'); uasort($out, fn ($a, $b) => strcmp((string)$b['timestamp'], (string)$a['timestamp'])); return $out; } /** The object an alert is about (obj_uuid + cls). */ public function objectOfMessage(array $message): ?string { $class = strtolower((string)($message['cls'] ?? '')); return $this->byUuid($class === 'vm' ? 'vm' : $class, (string)($message['obj_uuid'] ?? '')); } // figures ----------------------------------------------------------------------------------------------------------- /** @return array{total: float, free: float, used: float} bytes of a host */ public function hostMemory(string $host): array { $metrics = $this->get((string)$this->field($host, 'metrics', '')) ?? []; $total = (float)($metrics['memory_total'] ?? 0); $free = (float)($metrics['memory_free'] ?? 0); return ['total' => $total, 'free' => $free, 'used' => max(0, $total - $free)]; } public function vmMetrics(string $vm): array { return $this->get((string)$this->field($vm, 'metrics', '')) ?? []; } public function guestMetrics(string $vm): array { return $this->get((string)$this->field($vm, 'guest_metrics', '')) ?? []; } /** The IP addresses the guest tools report. @return list<string> */ public function ipsOf(string $vm): array { $networks = (array)($this->guestMetrics($vm)['networks'] ?? []); $out = []; foreach ($networks as $key => $ip) { if (preg_match('#^\d+/(ip|ipv4/\d+|ipv6/\d+)$#', (string)$key) && !in_array($ip, $out, true)) { $out[] = (string)$ip; } } return $out; } public function name(string $ref): string { $record = $this->get($ref); if ($record === null) { return ''; } $name = (string)($record['name_label'] ?? $record['device'] ?? $record['name'] ?? ''); if (($this->classOf($ref) === 'sr') && ($record['other_config']['i18n-key'] ?? '') === 'local-storage') { $host = $this->hostOfLocalSr($ref); return $host !== null && count($this->all('host')) > 1 ? 'Local storage on ' . $this->name($host) : 'Local storage'; } return $name; }}