the tabs of a kind of object */ public static function tabs(string $kind): array { return match ($kind) { 'root' => ['Dashboard', 'Home', 'Search'], // Dashboard: src/Feature/Pools/Dashboard.php 'disconnected' => ['General'], 'pool' => ['General', 'Memory', 'Storage', 'Networking', 'Search', 'Updates', 'Logs'], 'host' => ['General', 'Memory', 'Storage', 'Networking', 'NICs', 'Performance', 'Search', 'Updates', 'Logs'], 'vm' => ['General', 'Memory', 'Storage', 'Networking', 'Console', 'Performance', 'Snapshots', 'Logs'], 'template' => ['General', 'Memory', 'Storage', 'Networking', 'Logs'], 'snapshot' => ['General', 'Storage', 'Networking'], 'sr' => ['General', 'Storage', 'Logs'], 'network' => ['General', 'Networking'], 'notifications' => ['Alerts', 'Events'], 'group' => ['Search'], default => ['General'], }; } /** What kind of object a ref of a connection is: pool, host, vm, template, snapshot, sr, network. */ public static function kind(?Connection $c, string $ref): string { if (str_starts_with($ref, 'notif:')) { return 'notifications'; } if ($ref === TreeBuilder::ROOT || $c === null) { return str_starts_with($ref, 'type:') ? 'group' : 'root'; } if (!$c->isConnected()) { return 'disconnected'; } $class = $c->cache->classOf($ref); if ($class === 'vm') { $vm = $c->cache->get($ref) ?? []; return !empty($vm['is_a_snapshot']) ? 'snapshot' : (!empty($vm['is_a_template']) ? 'template' : 'vm'); } return $class ?? 'root'; } public function page(?Connection $c, string $ref, string $tab, ?Rrd $rrd = null): Page { $kind = self::kind($c, $ref); if ($kind === 'notifications') { return $tab === 'Events' ? $this->events(null, null) : $this->alerts(null, null); } if ($kind === 'root' || $kind === 'group') { return $tab === 'Search' ? $this->search(null, null, $rrd) : new Page($tab === 'Dashboard' && $kind === 'root' ? 'dashboard' : 'home'); } if ($kind === 'disconnected' && $c !== null) { return $this->disconnected($c); } /** @var Connection $c */ // + the buttons of storage, networks and disks (src/Feature/Storage) return \App\Feature\Storage\Menus::page($c, $ref, $kind, $tab, match ($tab) { 'General' => $this->general($c, $ref, $kind), 'Memory' => $this->memory($c, $ref, $kind), 'Storage' => $this->storage($c, $ref, $kind), 'Networking' => $this->networking($c, $ref, $kind), 'NICs' => $this->nics($c, $ref), 'Snapshots' => $this->snapshots($c, $ref), 'Search' => $this->search($c, $kind === 'host' ? $ref : null, $rrd), 'Logs' => $this->logs($c, $ref, $kind), 'Console' => $this->console($c, $ref), 'Performance' => $this->performance($c, $ref, $kind, $rrd), 'Updates' => \App\Feature\Updates\UpdatesPage::page($c, $ref, $kind), default => Page::text(''), }); } // general --------------------------------------------------------------------------------------------------------------- private function disconnected(Connection $c): Page { $state = match ($c->state) { Connection::CONNECTING => $c->phase ?: 'Connecting…', Connection::FAILED => 'Failed: ' . $c->error, default => 'Disconnected', }; return Page::sheet([['title' => 'General', 'rows' => [ ['Server:', $c->profile->host], ['Connection:', \App\Model\ServerProfile::METHODS[$c->profile->method] ?? $c->profile->method], ['User name:', $c->profile->user], ['State:', $state], ]]], [['id' => 'server.connect', 'text' => 'Connect', 'enabled' => $c->state !== Connection::CONNECTING, 'style' => 'primary'], ['id' => 'server.remove', 'text' => 'Remove from XenCenter']]); } private function general(Connection $c, string $ref, string $kind): Page { $cache = $c->cache; $r = $cache->get($ref) ?? []; $props = [['id' => 'object.properties', 'text' => 'Properties']]; switch ($kind) { case 'pool': $master = (string)$cache->master(); $sw = (array)$cache->field($master, 'software_version', []); $cpus = 0; foreach ($cache->hosts() as $h) { $cpus += (int)($h['cpu_info']['cpu_count'] ?? 0); } return Page::sheet([ ['title' => 'General', 'rows' => [['Name:', (string)$r['name_label']], ['Description:', (string)$r['name_description']], ['Tags:', self::tags($r)], ['Folder:', (string)($r['other_config']['folder'] ?? '') ?: '(none)'], ['Pool master:', $cache->name($master)], ['Number of servers:', (string)count($cache->hosts())], ['Number of VMs:', (string)count($cache->vms())], ['Default storage:', $cache->name((string)($r['default_SR'] ?? ''))], ['UUID:', (string)$r['uuid']]]], ['title' => 'Software', 'rows' => [['Product:', trim(($sw['product_brand'] ?? '') . ' ' . ($sw['product_version'] ?? ''))], ['Build:', (string)($sw['build_number'] ?? '')], ['XAPI:', (string)($sw['xapi'] ?? '')], ['Xen:', (string)($sw['xen'] ?? '')]]], ['title' => 'CPUs', 'rows' => [['Total physical CPUs:', (string)$cpus]]], ['title' => 'High Availability', 'rows' => [['HA:', !empty($r['ha_enabled']) ? 'Enabled' : 'Not configured']]], ], $props); case 'host': $metrics = $cache->get((string)($r['metrics'] ?? '')) ?? []; $sw = (array)($r['software_version'] ?? []); $cpu = (array)($r['cpu_info'] ?? []); $boot = (int)($r['other_config']['boot_time'] ?? 0); $agent = (int)($r['other_config']['agent_start_time'] ?? 0); $mgmt = []; foreach ($cache->pifsOf($ref) as $pif) { if (!empty($pif['management'])) { $mgmt[] = $pif['IP'] . ' (' . $pif['device'] . ')'; } } $isMaster = $ref === $cache->master(); return Page::sheet([ ['title' => 'General', 'rows' => [['Name:', (string)$r['name_label']], ['Description:', (string)$r['name_description']], ['Tags:', self::tags($r)], ['Pool master:', $isMaster ? 'Yes' : 'No'], ['Enabled:', ($r['enabled'] ?? true) ? 'Yes' : 'No (maintenance mode)'], ['iSCSI IQN:', (string)($r['iscsi_iqn'] ?? '')], ['Server uptime:', $boot > 0 ? Units::duration(time() - $boot) : ''], ['Toolstack uptime:', $agent > 0 ? Units::duration(time() - $agent) : ''], ['Live:', ($metrics['live'] ?? false) ? 'Yes' : 'No'], ['UUID:', (string)$r['uuid']]]], ['title' => 'Management Interfaces', 'rows' => [['Address:', (string)($r['address'] ?? '')], ['Hostname:', (string)($r['hostname'] ?? '')], ['Management interface:', implode("\n", $mgmt) ?: '(none)']]], ['title' => 'Memory', 'rows' => [['Server:', self::memoryLine($cache->hostMemory($ref))], ['Xen:', Units::bytes((float)($r['memory_overhead'] ?? 0))]]], ['title' => 'Version Details', 'rows' => [['Product:', trim(($sw['product_brand'] ?? '') . ' ' . ($sw['product_version'] ?? ''))], ['Build number:', (string)($sw['build_number'] ?? '')], ['Build date:', (string)($sw['date'] ?? '')], ['XAPI:', (string)($sw['xapi'] ?? '')], ['Xen:', (string)($sw['xen'] ?? '')], ['Kernel:', (string)($sw['linux'] ?? '')]]], ['title' => 'CPUs', 'rows' => [['Number of CPUs:', (string)($cpu['cpu_count'] ?? '') . ' (' . ($cpu['socket_count'] ?? '?') . ' sockets)'], ['Vendor:', (string)($cpu['vendor'] ?? '')], ['Model:', (string)($cpu['modelname'] ?? '')], ['Speed:', Units::mhz((string)($cpu['speed'] ?? '0'))]]], ['title' => 'BIOS', 'rows' => [['Vendor:', (string)($r['bios_strings']['system-manufacturer'] ?? '')], ['Model:', (string)($r['bios_strings']['system-product-name'] ?? '')], ['Serial number:', (string)($r['bios_strings']['system-serial-number'] ?? '')]]], ], $props); case 'vm': case 'template': case 'snapshot': $metrics = $cache->vmMetrics($ref); $guest = $cache->guestMetrics($ref); $os = (string)($guest['os_version']['name'] ?? ''); $pv = (array)($guest['PV_drivers_version'] ?? []); $start = Units::time((string)($metrics['start_time'] ?? '')); $running = ($r['power_state'] ?? '') === 'Running'; $tools = $guest === [] ? 'Not installed' : (($guest['PV_drivers_up_to_date'] ?? false) ? 'Optimized (version ' . implode('.', array_slice(array_values($pv), 0, 3)) . ')' : 'Out of date'); $sections = [['title' => 'General', 'rows' => array_values(array_filter([ ['Name:', (string)$r['name_label']], ['Description:', (string)$r['name_description']], ['Tags:', self::tags($r)], ['Operating System:', $os !== '' ? $os : ($kind === 'vm' ? 'Unknown' : '')], $kind === 'vm' ? ['Virtualization mode:', strtoupper((string)($r['domain_type'] ?? 'hvm'))] : null, $kind === 'vm' ? ['Power state:', (string)$r['power_state']] : null, $kind === 'vm' ? ['Home server:', ($a = (string)($r['affinity'] ?? '')) !== '' && $cache->get($a) !== null ? $cache->name($a) : '(none)'] : null, $kind === 'vm' && $running ? ['Running on:', $cache->name((string)$r['resident_on'])] : null, $kind === 'vm' && $running ? ['Time since startup:', $start > 0 ? Units::duration(time() - $start) : ''] : null, $kind === 'vm' ? ['Management agent:', $tools] : null, $kind === 'snapshot' ? ['Snapshot of:', $cache->name((string)$r['snapshot_of'])] : null, $kind === 'snapshot' ? ['Created:', Units::date((string)$r['snapshot_time'])] : null, $kind === 'vm' && $cache->ipsOf($ref) !== [] ? ['IP addresses:', implode("\n", $cache->ipsOf($ref))] : null, ['UUID:', (string)$r['uuid']], ]))]]; $sections[] = ['title' => 'CPUs', 'rows' => [['vCPUs at startup:', (string)$r['VCPUs_at_startup']], ['Maximum vCPUs:', (string)$r['VCPUs_max']], ['Topology:', self::topology($r)]]]; $sections[] = ['title' => 'Boot Options', 'rows' => [['Boot order:', self::bootOrder((string)($r['HVM_boot_params']['order'] ?? 'cd'))], ['Firmware:', strtoupper((string)($r['HVM_boot_params']['firmware'] ?? 'bios'))], ['Secure Boot:', ($r['platform']['secureboot'] ?? 'false') === 'true' ? 'Enabled' : 'Disabled'], ['Auto-restart after crash:', ($r['actions_after_crash'] ?? '') === 'restart' ? 'Yes' : 'No']]]; if ($kind === 'vm') { $sections[] = ['title' => 'High Availability', 'rows' => [['Restart priority:', ($r['ha_restart_priority'] ?? '') === '' ? 'Do not restart' : (string)$r['ha_restart_priority']]]]; } return Page::sheet($sections, $kind === 'snapshot' ? [] : $props); case 'sr': $pbds = []; foreach ((array)($r['PBDs'] ?? []) as $pbd) { $p = $cache->get((string)$pbd) ?? []; $pbds[] = $cache->name((string)($p['host'] ?? '')) . ': ' . (!empty($p['currently_attached']) ? 'Connected' : 'Unplugged'); } $size = (float)($r['physical_size'] ?? 0); $used = (float)($r['physical_utilisation'] ?? 0); return Page::sheet([ ['title' => 'General', 'rows' => [['Name:', $cache->name($ref)], ['Description:', (string)$r['name_description']], ['Tags:', self::tags($r)], ['Type:', self::srType((string)$r['type'])], ['Shared:', !empty($r['shared']) ? 'Yes' : 'No'], ['UUID:', (string)$r['uuid']]]], ['title' => 'Status', 'rows' => [['State:', 'OK'], ['Connected to:', implode("\n", $pbds) ?: '(none)']]], ['title' => 'Size', 'rows' => [['Usage:', ['bar' => $size > 0 ? $used / $size : 0, 'text' => Units::bytes($used) . ' used of ' . Units::bytes($size)]], ['Virtual allocation:', Units::bytes((float)($r['virtual_allocation'] ?? 0))]]], ], [['id' => 'object.properties', 'text' => 'Properties'], ['id' => 'sr.rescan', 'text' => 'Rescan']]); case 'network': $hosts = []; foreach ((array)($r['PIFs'] ?? []) as $pif) { $p = $cache->get((string)$pif) ?? []; $hosts[] = $cache->name((string)($p['host'] ?? '')) . ': ' . ($p['device'] ?? '') . ((int)($p['VLAN'] ?? -1) >= 0 ? ' (VLAN ' . $p['VLAN'] . ')' : ''); } return Page::sheet([['title' => 'General', 'rows' => [['Name:', (string)$r['name_label']], ['Description:', (string)$r['name_description']], ['Bridge:', (string)($r['bridge'] ?? '')], ['MTU:', (string)($r['MTU'] ?? '')], ['NICs:', implode("\n", $hosts) ?: '(internal network)'], ['VIFs:', (string)count((array)($r['VIFs'] ?? []))], ['UUID:', (string)$r['uuid']]]]], $props); } return Page::text(''); } // memory ------------------------------------------------------------------------------------------------------------- private function memory(Connection $c, string $ref, string $kind): Page { $cache = $c->cache; if ($kind === 'pool') { $sections = []; foreach ($cache->hosts() as $host => $h) { $sections[] = ['title' => (string)$h['name_label'], 'rows' => $this->hostMemoryRows($cache, $host)]; } return Page::sheet($sections); } if ($kind === 'host') { return Page::sheet([['title' => 'Server memory', 'rows' => $this->hostMemoryRows($cache, $ref)]]); } $r = $cache->get($ref) ?? []; $metrics = $cache->vmMetrics($ref); $actual = (float)($metrics['memory_actual'] ?? 0); $max = (float)$r['memory_static_max']; $running = in_array($r['power_state'] ?? '', ['Running', 'Paused'], true); return Page::sheet([['title' => 'Memory', 'rows' => [ ['Current usage:', $running ? ['bar' => $max > 0 ? $actual / $max : 0, 'text' => Units::bytes($actual) . ' of ' . Units::bytes($max)] : 'Not running'], ['Static maximum:', Units::bytes($max)], ['Dynamic minimum:', Units::bytes((float)$r['memory_dynamic_min'])], ['Dynamic maximum:', Units::bytes((float)$r['memory_dynamic_max'])], ['Static minimum:', Units::bytes((float)$r['memory_static_min'])], ['Dynamic Memory Control:', (float)$r['memory_dynamic_min'] < (float)$r['memory_dynamic_max'] ? 'Enabled' : 'Off (fixed memory)'], ]]], $kind === 'snapshot' ? [] : [['id' => 'vm.memory', 'text' => 'Edit…', 'enabled' => ($r['power_state'] ?? '') === 'Halted']]); } /** @return list */ private function hostMemoryRows(Cache $cache, string $host): array { $m = $cache->hostMemory($host); $rows = [['Used:', ['bar' => $m['total'] > 0 ? $m['used'] / $m['total'] : 0, 'text' => self::memoryLine($m)]], ['Free:', Units::bytes($m['free'])], ['Total:', Units::bytes($m['total'])]]; $vms = []; foreach ($cache->all('vm') as $ref => $vm) { if (($vm['resident_on'] ?? '') === $host) { $vms[] = $vm['name_label'] . ': ' . Units::bytes((float)($cache->vmMetrics($ref)['memory_actual'] ?? $vm['memory_dynamic_max'])); } } sort($vms, SORT_NATURAL | SORT_FLAG_CASE); $rows[] = ['VMs (' . count($vms) . '):', implode("\n", $vms) ?: '(none)']; return $rows; } // storage ----------------------------------------------------------------------------------------------------------- private function storage(Connection $c, string $ref, string $kind): Page { $cache = $c->cache; if ($kind === 'vm' || $kind === 'template' || $kind === 'snapshot') { $rows = []; foreach ($cache->disksOf($ref, true) as $d) { $vbd = $d['vbd']; $vdi = $d['vdi']; $cd = ($vbd['type'] ?? '') === 'CD'; $rows[] = ['id' => $d['ref'], 'icon' => $cd ? 'sr-iso' : 'disk', 'cells' => [ (string)$vbd['userdevice'], $cd ? ($vdi === null ? 'DVD drive (empty)' : (string)$vdi['name_label']) : (string)($vdi['name_label'] ?? ''), (string)($vdi['name_description'] ?? ''), $vdi === null ? '' : $cache->name((string)$vdi['SR']), $vdi === null ? '' : Units::bytes((float)$vdi['virtual_size']), ($vbd['mode'] ?? '') === 'RO' ? 'Yes' : 'No', !empty($vbd['currently_attached']) ? 'Yes' : 'No', '/dev/' . ($vbd['device'] ?? '')], 'sort' => [str_pad((string)$vbd['userdevice'], 3, '0', STR_PAD_LEFT)]]; } return Page::grid([['title' => 'Position', 'width' => 0.8], ['title' => 'Name', 'width' => 2.2], ['title' => 'Description', 'width' => 1.6], ['title' => 'SR', 'width' => 1.6], ['title' => 'Size', 'width' => 1, 'align' => 'right'], ['title' => 'Read only', 'width' => 0.9], ['title' => 'Active', 'width' => 0.7], ['title' => 'Device path', 'width' => 1.1]], $rows, [], 'This VM has no virtual disk.'); } if ($kind === 'sr') { $rows = []; foreach ((array)$cache->field($ref, 'VDIs', []) as $vdiRef) { $vdi = $cache->get((string)$vdiRef); if ($vdi === null) { continue; } $vms = []; foreach ((array)$vdi['VBDs'] as $vbd) { $vm = (string)$cache->field((string)$vbd, 'VM', ''); if ($vm !== '') { $vms[] = $cache->name($vm); } } $rows[] = ['id' => (string)$vdiRef, 'icon' => 'disk', 'cells' => [(string)$vdi['name_label'], (string)$vdi['name_description'], Units::bytes((float)$vdi['virtual_size']), implode(', ', $vms)], 'sort' => [null, null, (float)$vdi['virtual_size']]]; } return Page::grid([['title' => 'Name', 'width' => 2.5], ['title' => 'Description', 'width' => 2], ['title' => 'Size', 'width' => 1, 'align' => 'right'], ['title' => 'VM', 'width' => 2]], $rows, [['id' => 'sr.rescan', 'text' => 'Rescan']], 'No virtual disk on this storage.'); } $srs = $kind === 'host' ? $cache->srsOf($ref) : $cache->srs(); $rows = []; foreach ($srs as $sr => $r) { $size = (float)$r['physical_size']; $used = (float)$r['physical_utilisation']; $rows[] = ['id' => $sr, 'icon' => Icons::of('sr', $r), 'cells' => [$cache->name($sr), (string)$r['name_description'], self::srType((string)$r['type']), !empty($r['shared']) ? 'Yes' : 'No', ['bar' => $size > 0 ? $used / $size : 0, 'text' => $size > 0 ? round(100 * $used / $size) . '% (' . Units::bytes($used) . ' used)' : ''], Units::bytes($size), Units::bytes((float)$r['virtual_allocation'])], 'sort' => [null, null, null, null, null, $size, (float)$r['virtual_allocation']]]; } return Page::grid([['title' => 'Name', 'width' => 2], ['title' => 'Description', 'width' => 2.4], ['title' => 'Type', 'width' => 1], ['title' => 'Shared', 'width' => 0.7], ['title' => 'Usage', 'width' => 1.8], ['title' => 'Size', 'width' => 0.9, 'align' => 'right'], ['title' => 'Virtual allocation', 'width' => 1.2, 'align' => 'right']], $rows); } // networks ---------------------------------------------------------------------------------------------------------- private function networking(Connection $c, string $ref, string $kind): Page { $cache = $c->cache; if ($kind === 'vm' || $kind === 'template' || $kind === 'snapshot') { $ips = $cache->guestMetrics($ref)['networks'] ?? []; $rows = []; foreach ($cache->vifsOf($ref) as $v) { $vif = $v['vif']; $rows[] = ['id' => $v['ref'], 'icon' => 'network', 'cells' => [(string)$vif['device'], (string)$vif['MAC'], ($vif['qos_algorithm_params']['kbps'] ?? '') !== '' ? $vif['qos_algorithm_params']['kbps'] . ' kbit/s' : '', $cache->name((string)$vif['network']), (string)($ips[$vif['device'] . '/ip'] ?? $ips[$vif['device'] . '/ipv4/0'] ?? ''), !empty($vif['currently_attached']) ? 'Yes' : 'No']]; } return Page::grid([['title' => 'Device', 'width' => 0.7], ['title' => 'MAC', 'width' => 1.5], ['title' => 'Limit', 'width' => 1], ['title' => 'Network', 'width' => 2.6], ['title' => 'IP address', 'width' => 1.5], ['title' => 'Active', 'width' => 0.7]], $rows, [], 'This VM has no network interface.'); } $networks = $kind === 'network' ? [$ref => $cache->get($ref) ?? []] : $cache->networks(); $rows = []; foreach ($networks as $net => $n) { $pif = null; foreach ((array)($n['PIFs'] ?? []) as $p) { $record = $cache->get((string)$p); if ($record !== null && ($kind !== 'host' || $record['host'] === $ref)) { $pif = $record; break; } } if ($kind === 'host' && $pif === null && !empty($n['PIFs'])) { continue; } $carrier = $pif !== null ? (bool)($cache->get((string)($pif['metrics'] ?? ''))['carrier'] ?? false) : false; $rows[] = ['id' => $net, 'icon' => 'network', 'cells' => [(string)$n['name_label'], (string)$n['name_description'], $pif === null ? '' : (string)$pif['device'], $pif !== null && (int)$pif['VLAN'] >= 0 ? (string)$pif['VLAN'] : '-', ($n['other_config']['automatic'] ?? 'true') === 'false' ? 'No' : 'Yes', $pif === null ? '-' : ($carrier ? 'Connected' : 'Disconnected'), $pif === null ? '-' : (string)$pif['MAC'], (string)($n['MTU'] ?? '')]]; } return Page::grid([['title' => 'Name', 'width' => 2.4], ['title' => 'Description', 'width' => 1.8], ['title' => 'NIC', 'width' => 0.7], ['title' => 'VLAN', 'width' => 0.6], ['title' => 'Auto', 'width' => 0.6], ['title' => 'Link status', 'width' => 1.1], ['title' => 'MAC', 'width' => 1.4], ['title' => 'MTU', 'width' => 0.6, 'align' => 'right']], $rows); } private function nics(Connection $c, string $host): Page { $cache = $c->cache; $rows = []; foreach ($cache->pifsOf($host) as $ref => $pif) { if ((int)$pif['VLAN'] >= 0) { continue; } $m = $cache->get((string)($pif['metrics'] ?? '')) ?? []; $speed = (int)($m['speed'] ?? 0); $rows[] = ['id' => $ref, 'icon' => 'nic', 'cells' => [(string)$pif['device'], (string)$pif['MAC'], !empty($m['carrier']) ? 'Connected' : 'Disconnected', $speed >= 1000 ? ($speed / 1000) . ' Gbit/s' : $speed . ' Mbit/s', !empty($m['duplex']) ? 'Full' : 'Half', (string)($m['vendor_name'] ?? ''), (string)($m['device_name'] ?? ''), (string)($m['pci_bus_path'] ?? '')]]; } return Page::grid([['title' => 'NIC', 'width' => 0.6], ['title' => 'MAC', 'width' => 1.4], ['title' => 'Link status', 'width' => 1], ['title' => 'Speed', 'width' => 0.9], ['title' => 'Duplex', 'width' => 0.6], ['title' => 'Vendor', 'width' => 1.5], ['title' => 'Device', 'width' => 2.6], ['title' => 'PCI bus path', 'width' => 1.1]], $rows); } // snapshots ----------------------------------------------------------------------------------------------------------- private function snapshots(Connection $c, string $vm): Page { $rows = []; foreach ($c->cache->snapshotsOf($vm) as $ref => $s) { $rows[] = ['id' => $ref, 'icon' => 'snapshot', 'cells' => [(string)$s['name_label'], Units::date((string)$s['snapshot_time']), (string)$s['name_description'], self::tags($s)], 'sort' => [null, (string)$s['snapshot_time']]]; } return Page::grid([['title' => 'Name', 'width' => 2], ['title' => 'Created', 'width' => 1.6], ['title' => 'Description', 'width' => 2.4], ['title' => 'Tags', 'width' => 1]], $rows, [['id' => 'vm.snapshot', 'text' => 'Take Snapshot…', 'style' => 'primary'], ['id' => 'snapshot.revert', 'text' => 'Revert To…', 'needsRow' => true], ['id' => 'snapshot.delete', 'text' => 'Delete', 'needsRow' => true, 'style' => 'danger']], 'There are no snapshots of this VM. Take one to keep its state.'); } // search --------------------------------------------------------------------------------------------------------------- /** The Search tab: every VM and server (of a connection, a host, or all), with live figures. */ public function search(?Connection $only, ?string $host, ?Rrd $rrd): Page { $rows = []; foreach ($this->session->connections as $c) { if (!$c->isConnected() || ($only !== null && $c !== $only)) { continue; } $cache = $c->cache; foreach ($cache->hosts() as $ref => $h) { if ($host !== null && $ref !== $host) { continue; } $m = $cache->hostMemory($ref); $cpu = self::lastAverage($rrd, 'host', (string)$h['uuid'], 'cpu'); $boot = (int)($h['other_config']['boot_time'] ?? 0); $rows[] = ['id' => $ref, 'icon' => Icons::of('host', $h, $cache), 'cells' => [(string)$h['name_label'], $cpu === null ? '' : ['bar' => $cpu, 'text' => round($cpu * 100) . '% of ' . ($h['cpu_info']['cpu_count'] ?? '?') . ' CPUs'], ['bar' => $m['total'] > 0 ? $m['used'] / $m['total'] : 0, 'text' => Units::bytes($m['used']) . ' of ' . Units::bytes($m['total'])], '', '', (string)$h['address'], $boot > 0 ? Units::duration(time() - $boot) : ''], 'sort' => ['0' . $h['name_label'], $cpu, $m['used']]]; } foreach ($cache->vms() as $ref => $vm) { if ($host !== null && ($vm['resident_on'] ?? '') !== $host) { continue; } $running = $vm['power_state'] === 'Running'; $cpu = $running ? self::lastAverage($rrd, 'vm', (string)$vm['uuid'], 'cpu') : null; $mem = (float)($cache->vmMetrics($ref)['memory_actual'] ?? 0); $freeSeries = $rrd !== null && $running ? $rrd->series('vm', (string)$vm['uuid'], 'memory_internal_free') : []; $free = $freeSeries === [] ? false : end($freeSeries); $used = $free !== false ? max(0.0, $mem - $free[1] * 1024) : $mem; $start = Units::time((string)($cache->vmMetrics($ref)['start_time'] ?? '')); $net = $running && $rrd !== null ? self::lastSum($rrd, 'vm', (string)$vm['uuid'], ['vif_0_rx', 'vif_0_tx']) : null; $disk = $running && $rrd !== null ? self::lastSum($rrd, 'vm', (string)$vm['uuid'], ['vbd_xvda_read', 'vbd_xvda_write']) : null; $rows[] = ['id' => $ref, 'icon' => Icons::of('vm', $vm), 'cells' => [' ' . $vm['name_label'], $cpu === null ? '' : ['bar' => $cpu, 'text' => round($cpu * 100) . '% of ' . $vm['VCPUs_at_startup'] . ' vCPUs'], $running ? ['bar' => $mem > 0 ? $used / $mem : 0, 'text' => Units::bytes($used) . ' of ' . Units::bytes($mem)] : '', $disk === null ? '' : Units::bytes($disk) . '/s', $net === null ? '' : Units::bytes($net) . '/s', implode(', ', array_slice($cache->ipsOf($ref), 0, 1)), $running && $start > 0 ? Units::duration(time() - $start) : ''], 'muted' => !$running, 'sort' => ['1' . $vm['name_label'], $cpu, $used, $disk, $net]]; } } return Page::grid([['title' => 'Name', 'width' => 2], ['title' => 'CPU usage', 'width' => 1.6], ['title' => 'Used memory', 'width' => 1.8], ['title' => 'Disks', 'width' => 0.9], ['title' => 'Network', 'width' => 0.9], ['title' => 'Address', 'width' => 1.2], ['title' => 'Uptime', 'width' => 1.3]], $rows, [], 'Nothing to show: add a server first.'); } private static function lastAverage(?Rrd $rrd, string $kind, string $uuid, string $prefix): ?float { if ($rrd === null) { return null; } $names = array_values(array_filter($rrd->names($kind, $uuid), fn ($n) => preg_match('/^' . $prefix . '\d+$/', $n))); if ($names === []) { return null; } $sum = 0.0; foreach ($names as $n) { $series = $rrd->series($kind, $uuid, $n); $sum += $series === [] ? 0 : (float)end($series)[1]; } return $sum / count($names); } private static function lastSum(Rrd $rrd, string $kind, string $uuid, array $names): ?float { $found = false; $sum = 0.0; foreach ($names as $n) { $series = $rrd->series($kind, $uuid, $n); if ($series !== []) { $found = true; $sum += (float)end($series)[1]; } } return $found ? $sum : null; } // logs, alerts, events --------------------------------------------------------------------------------------------------- /** The columns of every list of events and alerts (Logs, Notifications): one shape, so they mix and sort by date. */ public const LOG_COLUMNS = [['title' => 'Message', 'width' => 4], ['title' => 'Location', 'width' => 1.3], ['title' => 'Status', 'width' => 1.3], ['title' => 'Date', 'width' => 1.6]]; /** The buttons over them: Cancel (a running task), Dismiss (the selected line), Dismiss all (the lines shown). */ public const LOG_ACTIONS = [['id' => 'log.cancel', 'text' => 'Cancel', 'needsRow' => 'running'], ['id' => 'log.dismiss', 'text' => 'Dismiss', 'needsRow' => 'done'], ['id' => 'log.dismissAll', 'text' => 'Dismiss all']]; /** The Logs tab: what happened to this object (the pool: to everything of the pool), events and alerts together, newest first. */ private function logs(Connection $c, string $ref, string $kind): Page { $rows = [...$this->alertRows($c, $kind === 'pool' ? null : $ref), ...$this->eventRows($c, $kind === 'pool' ? null : $ref)]; return Page::grid(self::LOG_COLUMNS, self::newestFirst($rows), self::LOG_ACTIONS, 'Nothing happened to it yet: what you do and the alerts of the server are listed here.'); } public function events(?Connection $c, ?string $target): Page { return Page::grid(self::LOG_COLUMNS, self::newestFirst($this->eventRows($c, $target)), self::LOG_ACTIONS, 'No event: what you do (start a VM, take a snapshot…) is listed here.'); } public function alerts(?Connection $only, ?string $target): Page { return Page::grid(self::LOG_COLUMNS, self::newestFirst($this->alertRows($only, $target)), self::LOG_ACTIONS, 'There are no alerts.'); } /** @return list> the actions of this session (ids "log:") */ private function eventRows(?Connection $c, ?string $target): array { $rows = []; foreach ($this->session->log as $e) { if (($c !== null && $e->server !== $c->name()) || ($target !== null && $e->target !== $target)) { continue; } $running = $e->status === LogEntry::RUNNING; $status = match ($e->status) { LogEntry::RUNNING => ['bar' => $e->progress, 'text' => round($e->progress * 100) . '%', 'color' => '#6ea8f0'], LogEntry::DONE => 'Completed', LogEntry::CANCELLED => 'Cancelled', default => 'Failed', }; $rows[] = ['id' => 'log:' . $e->id, 'running' => $running, 'done' => !$running, 'at' => $e->at, 'icon' => match ($e->status) { LogEntry::RUNNING => 'info', LogEntry::DONE => 'ok', LogEntry::CANCELLED => 'warning', default => 'alert'}, 'cells' => [$e->title . ($e->detail !== '' ? ' — ' . $e->detail : ''), $e->server, $status, Units::stamp($e->at)], 'color' => $e->status === LogEntry::FAILED ? '#b42318' : null]; } return $rows; } /** @return list> the alerts XAPI keeps (ids "msg:") */ private function alertRows(?Connection $only, ?string $target): array { $rows = []; foreach ($this->session->connections as $c) { if (!$c->isConnected() || ($only !== null && $c !== $only)) { continue; } foreach ($c->cache->messages() as $ref => $m) { $object = $c->cache->objectOfMessage($m); if ($target !== null && $object !== $target) { continue; } $priority = (int)($m['priority'] ?? 5); $at = Units::time((string)$m['timestamp']); $rows[] = ['id' => 'msg:' . $ref, 'running' => false, 'done' => true, 'at' => $at, 'icon' => $priority <= 2 ? 'alert' : ($priority <= 4 ? 'warning' : 'info'), 'cells' => [self::messageText((string)($m['name'] ?? ''), (string)$m['body']), $object !== null ? $c->cache->name($object) : $c->name(), $priority <= 2 ? 'Critical' : ($priority <= 4 ? 'Warning' : 'Information'), Units::stamp($at)]]; } } return $rows; } /** @param list> $rows @return list> */ /** * The text of an alert: XAPI's performance alarms are "value: 0.95 / config: …" on several * lines; they become "CPU usage at 95% (alarm above 90% for 60 s)". Other bodies: their lines joined, XML tags removed. */ public static function messageText(string $name, string $body): string { if (preg_match('/value:\s*([-0-9.eE]+)/', $body, $v) && preg_match('/ 'CPU usage', 'network_usage' => 'Network usage', 'disk_usage' => 'Disk usage', 'mem_usage' => 'Memory usage', 'memory_free_kib' => 'Free memory', 'fs_usage' => 'Root file system usage', 'log_fs_usage' => 'Log file system usage', 'mem_usage_ratio' => 'Memory usage', 'sr_io_throughput_total_per_host' => 'Storage throughput', 'physical_utilisation' => 'Storage usage']; if ($what === 'sr_io_throughput_total_per_host') { $value *= 1048576; // perfmon counts it in MiB/s $level = $level !== null ? $level * 1048576 : null; } $label = $labels[$what] ?? ucfirst(str_replace('_', ' ', $what)); $ratio = str_ends_with($what, '_usage') || str_ends_with($what, '_ratio') || $what === 'physical_utilisation'; $show = fn (float $x): string => $ratio && $x <= 1.5 ? round($x * 100) . '%' : ($what === 'memory_free_kib' ? Units::bytes($x * 1024) : (str_contains($what, 'throughput') || $what === 'network_usage' || $what === 'disk_usage' ? Units::bytes($x) . '/s' : (string)round($x, 2))); $text = "$label at " . $show($value); if ($level !== null) { $text .= ' (alarm ' . ($what === 'memory_free_kib' ? 'below ' : 'above ') . $show($level) . ($period ? " for $period s" : '') . ')'; } return $text; } $plain = trim((string)preg_replace('/\s+/u', ' ', strip_tags(str_replace(['<', '>'], [' <', '> '], $body)))); return $plain !== '' ? $plain : ucfirst(strtolower(str_replace('_', ' ', $name))); } private static function newestFirst(array $rows): array { usort($rows, fn ($a, $b) => [$b['at'], $b['id']] <=> [$a['at'], $a['id']]); foreach ($rows as &$row) { $row['sort'] = [null, null, null, sprintf('%012d', $row['at'])]; // a click on Date sorts by the time, not the text } return $rows; } // console, performance ---------------------------------------------------------------------------------------------------- private function console(Connection $c, string $vm): Page { $r = $c->cache->get($vm) ?? []; if (!in_array($r['power_state'] ?? '', ['Running', 'Paused'], true)) { return Page::text("The console is only available while the VM is running.\n\nStart the VM to see its screen.", [['id' => 'vm.start', 'text' => 'Start', 'style' => 'primary', 'enabled' => ($r['power_state'] ?? '') === 'Halted']]); } $page = new Page('console'); $page->text = $vm; return $page; } private function performance(Connection $c, string $ref, string $kind, ?Rrd $rrd): Page { $r = $c->cache->get($ref) ?? []; $uuid = (string)($r['uuid'] ?? ''); $type = $kind === 'host' ? 'host' : 'vm'; if ($kind === 'vm' && ($r['power_state'] ?? '') !== 'Running') { return Page::text('Performance figures are recorded while the VM runs.'); } $page = new Page('charts'); if ($rrd === null) { $page->charts = [['title' => 'CPU', 'format' => 'percent', 'series' => []]]; return $page; } $cpus = array_values(array_filter($rrd->names($type, $uuid), fn ($n) => preg_match('/^cpu\d+$/', $n))); natsort($cpus); $cpuSeries = [['label' => 'Average', 'points' => $rrd->average($type, $uuid, $cpus)]]; foreach (array_slice(array_values($cpus), 0, 4) as $n) { $cpuSeries[] = ['label' => strtoupper(substr($n, 0, 3)) . ' ' . substr($n, 3), 'points' => $rrd->series($type, $uuid, $n)]; } $charts = [['title' => 'CPU performance', 'format' => 'percent', 'series' => $cpuSeries]]; if ($type === 'host') { $total = $rrd->series('host', $uuid, 'memory_total_kib'); $free = $rrd->series('host', $uuid, 'memory_free_kib'); $byTime = array_column($total, 1, 0); $used = []; foreach ($free as [$t, $v]) { if (isset($byTime[$t])) { $used[] = [$t, max(0.0, ($byTime[$t] - $v) * 1024)]; } } $charts[] = ['title' => 'Memory performance', 'format' => 'bytes', 'max' => ($total === [] ? null : end($total)[1] * 1024), 'series' => [['label' => 'Used memory', 'points' => $used]]]; $charts[] = ['title' => 'Network performance', 'format' => 'rate', 'series' => [['label' => 'eth0 receive', 'points' => $rrd->series('host', $uuid, 'pif_eth0_rx')], ['label' => 'eth0 send', 'points' => $rrd->series('host', $uuid, 'pif_eth0_tx')]]]; $charts[] = ['title' => 'Load average', 'format' => 'number', 'series' => [['label' => 'Load average', 'points' => $rrd->series('host', $uuid, 'loadavg')]]]; } else { $mem = $rrd->series('vm', $uuid, 'memory'); $free = $rrd->series('vm', $uuid, 'memory_internal_free'); $byTime = array_column($mem, 1, 0); $used = []; foreach ($free as [$t, $v]) { if (isset($byTime[$t])) { $used[] = [$t, max(0.0, $byTime[$t] - $v * 1024)]; } } $charts[] = ['title' => 'Memory performance', 'format' => 'bytes', 'max' => $mem === [] ? null : end($mem)[1], 'series' => [['label' => 'Used memory', 'points' => $used]]]; $charts[] = ['title' => 'Network performance', 'format' => 'rate', 'series' => [['label' => 'Receive', 'points' => $rrd->series('vm', $uuid, 'vif_0_rx')], ['label' => 'Send', 'points' => $rrd->series('vm', $uuid, 'vif_0_tx')]]]; $charts[] = ['title' => 'Disk performance', 'format' => 'rate', 'series' => [['label' => 'Read', 'points' => $rrd->series('vm', $uuid, 'vbd_xvda_read')], ['label' => 'Write', 'points' => $rrd->series('vm', $uuid, 'vbd_xvda_write')]]]; } $page->charts = $charts; return $page; } // little helpers ------------------------------------------------------------------------------------------------------- private static function topology(array $r): string { $cores = max(1, (int)($r['platform']['cores-per-socket'] ?? 1)); $sockets = max(1, intdiv(max(1, (int)$r['VCPUs_max']), $cores)); return $sockets . ($sockets === 1 ? ' socket' : ' sockets') . ' with ' . $cores . ($cores === 1 ? ' core' : ' cores') . ' per socket'; } private static function tags(array $r): string { $tags = (array)($r['tags'] ?? []); return $tags === [] ? '' : implode(', ', $tags); } /** @param array{total: float, free: float, used: float} $m */ private static function memoryLine(array $m): string { return Units::bytes($m['used']) . ' used of ' . Units::bytes($m['total']) . ($m['total'] > 0 ? ' (' . round(100 * $m['used'] / $m['total']) . '%)' : ''); } public static function srType(string $type): string { return match ($type) { 'lvm' => 'LVM', 'ext' => 'EXT', 'nfs' => 'NFS', 'lvmoiscsi' => 'iSCSI', 'lvmohba' => 'HBA', 'iso' => 'ISO', 'udev' => 'Removable', 'smb' => 'SMB', 'xfs' => 'XFS', 'zfs' => 'ZFS', 'glusterfs' => 'GlusterFS', 'cephfs' => 'CephFS', 'linstor' => 'XOSTOR', default => strtoupper($type), }; } private static function bootOrder(string $order): string { $names = ['c' => 'Hard disk', 'd' => 'DVD drive', 'n' => 'Network']; return implode(', ', array_map(fn ($ch) => $names[$ch] ?? $ch, str_split($order))); } }