Files
Virtnet.bond/extensions/Servers/Proxmox/Livewire/Network.php
Joseph.Rawlings 5f2e591c6e feat: add LXC container support to Proxmox extension
- Add is_lxc boolean column to ext_proxmox_nodes (default false, existing nodes unaffected)
- Add LXC checkbox to admin node creation form (disabled on edit)
- Dynamic API path selection via Node::vmType() (returns 'lxc' or 'qemu')
- LXC deployment: syncLxcSettings() for cores, memory, swap, hostname, rootfs, net config
- LXC network uses name=eth0 format with applyLxcPoolSettings() for VLAN/firewall/MTU
- All lifecycle operations LXC-aware: install, suspend, unsuspend, terminate, upgrade, reinstall
- VNC/xterm console uses correct type (lxc vs kvm)
- All 20+ Livewire components use vmType() for Proxmox API paths
- LXC user interface shows basic-only view (no advanced toggle, no KVM-specific tabs)
- Basic tabs for LXC: Overview, Network Configuration, Backups, Console
2026-03-02 07:11:25 -05:00

853 lines
32 KiB
PHP

<?php
namespace Paymenter\Extensions\Servers\Proxmox\Livewire;
use Illuminate\Http\Client\RequestException;
use Paymenter\Extensions\Servers\Proxmox\Models\Server;
use Paymenter\Extensions\Servers\Proxmox\Proxmox;
class Network extends Component
{
public $guestAgentNetworks = [];
public array $interfaces = [];
public array $interfaceSelections = [];
public array $interfaceConfigs = [];
public array $disconnectedConfigs = [];
public array $interfaceMultiqueues = [];
public string $interfaceType = 'virtio';
public string $selectedInterface = 'net0';
public bool $canRemoveInterfaces = false;
public array $original = [];
public array $interfaceTypeOptions = [
'virtio' => 'VirtIO (Paravirtualized)',
'e1000' => 'Intel E1000',
'e1000e' => 'Intel E1000e',
'rtl8139' => 'Realtek RTL8139',
'vmxnet3' => 'VMware vmxnet3',
'pcnet' => 'AMD PCnet',
];
public function mount(Server $server): void
{
parent::mount($server);
$this->loadGuestAgentNetworks();
$result = $this->loadVmInterfaces();
$this->interfaces = $result['interfaces'];
$this->interfaceConfigs = $result['configs'];
$this->interfaceSelections = collect($this->interfaces)
->map(fn ($meta) => $meta['model'] ?? 'virtio')
->toArray();
$this->interfaceMultiqueues = collect($this->interfaces)
->map(fn ($meta) => $meta['queues'] ?? 1)
->toArray();
$this->selectedInterface = array_key_first($this->interfaces) ?? 'net0';
$this->interfaceType = $this->interfaceSelections[$this->selectedInterface] ?? 'virtio';
$this->canRemoveInterfaces = count($this->interfaces) > 1;
// Store original values for synced comparison
if (empty($this->original)) {
$this->original = [
'interfaceSelections' => json_decode(json_encode($this->interfaceSelections), true),
'interfaceMultiqueues' => json_decode(json_encode($this->interfaceMultiqueues), true),
];
}
}
public function getCanUpdateInterfaceProperty(): bool
{
return isset($this->interfaces[$this->selectedInterface]['model'], $this->interfaceSelections[$this->selectedInterface])
&& strtolower($this->interfaces[$this->selectedInterface]['model']) !== strtolower($this->interfaceSelections[$this->selectedInterface]);
}
protected function loadVmInterfaces(): array
{
try {
$proxmox = new Proxmox;
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
$interfaces = [];
$configs = [];
foreach ($config as $key => $value) {
if (!is_string($key) || !str_starts_with($key, 'net')) {
continue;
}
$configs[$key] = (string) $value;
$segments = array_filter(array_map('trim', explode(',', (string) $value)));
$model = 'virtio';
$mac = null;
$mtu = null;
$linkdown = false;
$queues = 1;
foreach ($segments as $segment) {
if (!str_contains($segment, '=')) {
continue;
}
[$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2));
if (in_array(strtolower($segmentKey), array_keys($this->interfaceTypeOptions), true)) {
$model = strtolower($segmentKey);
$mac = $segmentValue;
} elseif (strtolower($segmentKey) === 'bridge') {
continue;
} elseif (strtolower($segmentKey) === 'mtu') {
$mtu = $segmentValue;
} elseif (strtolower($segmentKey) === 'link_down' || strtolower($segmentKey) === 'linkdown') {
$linkdown = (bool) $segmentValue;
} elseif (strtolower($segmentKey) === 'queues') {
$queues = max(1, (int) $segmentValue);
}
}
$interfaces[$key] = [
'model' => $model,
'mac' => $mac,
'mtu' => $mtu,
'linkdown' => $linkdown,
'queues' => $queues,
];
}
// Add disconnected devices from stored configs
foreach ($this->disconnectedConfigs as $key => $disconnectedConfig) {
if (!isset($interfaces[$key]) && !empty($disconnectedConfig)) {
$configs[$key] = $disconnectedConfig;
$segments = array_filter(array_map('trim', explode(',', $disconnectedConfig)));
$model = 'virtio';
$mac = null;
$mtu = null;
$queues = 1;
foreach ($segments as $segment) {
if (!str_contains($segment, '=')) {
continue;
}
[$segmentKey, $segmentValue] = array_map('trim', explode('=', $segment, 2));
if (in_array(strtolower($segmentKey), array_keys($this->interfaceTypeOptions), true)) {
$model = strtolower($segmentKey);
$mac = $segmentValue;
} elseif (strtolower($segmentKey) === 'bridge') {
continue;
} elseif (strtolower($segmentKey) === 'mtu') {
$mtu = $segmentValue;
} elseif (strtolower($segmentKey) === 'queues') {
$queues = max(1, (int) $segmentValue);
}
}
$interfaces[$key] = [
'model' => $model,
'mac' => $mac,
'mtu' => $mtu,
'linkdown' => true,
'queues' => $queues,
];
}
}
if (empty($interfaces)) {
$interfaces = [
'net0' => [
'model' => 'virtio',
'mac' => null,
'mtu' => null,
'linkdown' => false,
'queues' => 1,
],
];
$configs = ['net0' => ''];
}
[$interfaces, $configs] = $this->sortInterfaceArrays($interfaces, $configs);
return [
'interfaces' => $interfaces,
'configs' => $configs,
];
} catch (\Exception $e) {
$interfaces = [
'net0' => [
'model' => 'virtio',
'mac' => null,
'mtu' => null,
'linkdown' => false,
'queues' => 1,
],
];
$configs = ['net0' => ''];
[$interfaces, $configs] = $this->sortInterfaceArrays($interfaces, $configs);
return [
'interfaces' => $interfaces,
'configs' => $configs,
];
}
}
protected function loadGuestAgentNetworks()
{
try {
$proxmox = new Proxmox;
$response = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/agent/network-get-interfaces');
$json = $response->json();
// Check if the response indicates an error (VM not running, guest agent not available, etc.)
if (isset($json['data']) && $json['data'] === null) {
$this->guestAgentNetworks = [];
return;
}
$this->guestAgentNetworks = $json['data']['result'] ?? [];
} catch (RequestException $e) {
// HTTP error (500, 400, etc.) - VM not running or guest agent unavailable
$this->guestAgentNetworks = [];
} catch (\Exception $e) {
// Any other error
$this->guestAgentNetworks = [];
}
}
public function getNetworkInterfaceForIp($ipAddress)
{
foreach ($this->guestAgentNetworks as $interface) {
if (isset($interface['ip-addresses'])) {
foreach ($interface['ip-addresses'] as $ip) {
if (isset($ip['ip-address']) && $ip['ip-address'] === $ipAddress->ip) {
return $interface;
}
}
}
}
return null;
}
public function getAdditionalGuestAgentAddresses()
{
$knownIps = $this->server->ipAddresses->pluck('ip')->toArray();
$additionalAddresses = [];
foreach ($this->guestAgentNetworks as $interface) {
if (isset($interface['ip-addresses'])) {
foreach ($interface['ip-addresses'] as $ip) {
if (isset($ip['ip-address']) && !in_array($ip['ip-address'], $knownIps)) {
$additionalAddresses[] = [
'ip' => $ip['ip-address'],
'type' => $ip['ip-address-type'] ?? 'ipv4',
'prefix' => $ip['prefix'] ?? null,
'interface' => $interface,
];
}
}
}
}
return $additionalAddresses;
}
public function getCidrSuffix($ipAddress): ?string
{
$netmask = $ipAddress->netmask ?? null;
if (empty($netmask)) {
return null;
}
if (is_numeric($netmask)) {
return '/' . (int) $netmask;
}
$packed = @inet_pton($netmask);
if ($packed === false) {
return null;
}
$bits = 0;
foreach (str_split($packed) as $char) {
$bits += substr_count(sprintf('%08b', ord($char)), '1');
}
return '/' . $bits;
}
public function getCpuCores(): int
{
try {
$proxmox = new Proxmox;
$config = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
// CPU cores can be specified with 'cores' or 'cpus' in Proxmox config
$cores = (int) ($config['cores'] ?? $config['cpus'] ?? 1);
return max(1, $cores);
} catch (\Exception $e) {
// Fallback to 1 if unable to retrieve
return 1;
}
}
public function isPublicIp($ip, $type = 'ipv4')
{
if ($type === 'ipv6' || filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// Check for IPv6 private/local addresses
if ($ip === '::1' || strpos($ip, '::1/') === 0) {
return false; // localhost
}
// Link-local (fe80::/10)
if (preg_match('/^fe[89ab][0-9a-f]:/i', $ip)) {
return false;
}
// Unique local (fc00::/7) - fc00::/8 and fd00::/8
if (preg_match('/^fc[0-9a-f]{1,2}:/i', $ip) || preg_match('/^fd[0-9a-f]{1,2}:/i', $ip)) {
return false;
}
// IPv4-mapped IPv6 addresses
if (strpos($ip, '::ffff:') === 0 || strpos($ip, '::ffff:') !== false) {
return false;
}
return true;
} else {
// Check for IPv4 private/local addresses
$parts = explode('.', $ip);
if (count($parts) !== 4) {
return false;
}
$first = (int)$parts[0];
$second = (int)$parts[1];
// 10.0.0.0/8
if ($first === 10) {
return false;
}
// 172.16.0.0/12
if ($first === 172 && $second >= 16 && $second <= 31) {
return false;
}
// 192.168.0.0/16
if ($first === 192 && $second === 168) {
return false;
}
// 127.0.0.0/8 (localhost)
if ($first === 127) {
return false;
}
// 169.254.0.0/16 (link-local)
if ($first === 169 && $second === 254) {
return false;
}
return true;
}
}
/**
* Check if Network Devices settings are synced with the original values.
*/
public function getNetworkDevicesSyncedProperty(): bool
{
// If original hasn't been loaded yet, consider synced
if (!isset($this->original['interfaceSelections'])) {
return true;
}
$originalInterfaceSelections = isset($this->original['interfaceSelections']) && is_array($this->original['interfaceSelections'])
? $this->original['interfaceSelections']
: [];
$currentInterfaceSelections = is_array($this->interfaceSelections) ? $this->interfaceSelections : [];
// Normalize arrays: sort by keys and compare
ksort($originalInterfaceSelections);
ksort($currentInterfaceSelections);
if (count($originalInterfaceSelections) !== count($currentInterfaceSelections)) {
return false;
}
foreach ($originalInterfaceSelections as $key => $value) {
if (!isset($currentInterfaceSelections[$key]) || $currentInterfaceSelections[$key] !== $value) {
return false;
}
}
// Also check interfaceMultiqueues
$originalInterfaceMultiqueues = isset($this->original['interfaceMultiqueues']) && is_array($this->original['interfaceMultiqueues'])
? $this->original['interfaceMultiqueues']
: [];
$currentInterfaceMultiqueues = is_array($this->interfaceMultiqueues) ? $this->interfaceMultiqueues : [];
ksort($originalInterfaceMultiqueues);
ksort($currentInterfaceMultiqueues);
if (count($originalInterfaceMultiqueues) !== count($currentInterfaceMultiqueues)) {
return false;
}
foreach ($originalInterfaceMultiqueues as $key => $value) {
if (!isset($currentInterfaceMultiqueues[$key]) || $currentInterfaceMultiqueues[$key] !== $value) {
return false;
}
}
return true;
}
public function render()
{
return view('proxmox::network')->layoutData([
'sidebar' => true,
]);
}
public function updateInterfaceType(): void
{
if (!isset($this->interfaces[$this->selectedInterface], $this->interfaceSelections[$this->selectedInterface])) {
$this->notify('Select a valid network device.', 'error');
return;
}
$currentInterface = $this->interfaces[$this->selectedInterface];
$currentModel = strtolower($currentInterface['model'] ?? 'virtio');
try {
$targetType = strtolower($this->interfaceSelections[$this->selectedInterface] ?? $currentModel);
if (!array_key_exists($targetType, $this->interfaceTypeOptions)) {
$this->notify('Invalid interface type selected.', 'error');
return;
}
$proxmox = new Proxmox;
$currentConfig = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
$currentValue = (string) ($currentConfig[$this->selectedInterface] ?? '');
$segments = array_filter(array_map('trim', explode(',', $currentValue)));
$macValue = $currentInterface['mac'] ?? null;
$otherSegments = [];
foreach ($segments as $segment) {
if (!str_contains($segment, '=')) {
$otherSegments[] = $segment;
continue;
}
[$key, $value] = array_map('trim', explode('=', $segment, 2));
$lowerKey = strtolower($key);
if (array_key_exists($lowerKey, $this->interfaceTypeOptions)) {
// This is the existing model definition, capture MAC and skip so it can be replaced.
$macValue = $value;
continue;
}
$otherSegments[] = $key . '=' . $value;
}
if ($macValue === null || $macValue === '') {
$macValue = $this->generateMacAddress();
}
$newSegments = [
$targetType . '=' . $macValue,
];
foreach ($otherSegments as $segment) {
$newSegments[] = $segment;
}
$newValue = implode(',', $newSegments);
try {
$proxmox->request(
'/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config',
'put',
[$this->selectedInterface => $newValue]
);
} catch (\Exception $e) {
// Check if this is a hotplug error (occurs when server is powered on)
// These errors don't break functionality - the config is still saved
$errorMessage = strtolower($e->getMessage());
if (str_contains($errorMessage, 'hotplug') || str_contains($errorMessage, 'hot-unplugging')) {
// Suppress the error notification but continue to update the interface state
// The configuration is still applied, just with a hotplug warning from Proxmox
} else {
// Re-throw non-hotplug errors to be handled below
throw $e;
}
}
$this->interfaces[$this->selectedInterface]['model'] = $targetType;
$this->interfaces[$this->selectedInterface]['mac'] = $macValue;
$this->interfaceSelections[$this->selectedInterface] = $targetType;
// Update original values after successful save
$this->original = [
'interfaceSelections' => json_decode(json_encode($this->interfaceSelections), true),
'interfaceMultiqueues' => json_decode(json_encode($this->interfaceMultiqueues), true),
];
$this->notify(strtoupper($this->selectedInterface) . ' updated to ' . strtoupper($targetType) . '.', 'success');
} catch (\Exception $e) {
$this->notify('Failed to update interface type. You must power off the VPS before making this change. Error: ' . $e->getMessage(), 'error');
}
}
protected function generateMacAddress(): string
{
$bytes = random_bytes(6);
$bytes[0] = chr((ord($bytes[0]) | 0x02) & 0xfe); // locally administered, unicast
return implode(':', array_map(fn ($char) => sprintf('%02x', ord($char)), str_split($bytes)));
}
public function removeInterface(?string $name = null): void
{
$target = $name ?? $this->selectedInterface;
if (!$this->canRemoveInterfaces || !isset($this->interfaces[$target])) {
$this->notify('At least one network device is required.', 'error');
return;
}
// Check VM status - must be stopped to remove interfaces
if (!$this->status || $this->status->status !== 'stopped') {
$this->notify('VM must be stopped to remove network devices.', 'error');
return;
}
try {
$proxmox = new Proxmox;
$proxmox->request(
'/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config',
'put',
['delete' => $target]
);
// Refresh interfaces to get updated state
$this->refreshInterfaces();
$this->notify('Network device removed successfully.', 'success');
} catch (\Exception $e) {
$this->notify('Failed to remove network device: ' . $e->getMessage(), 'error');
}
}
public function refreshInterfaces(): void
{
$result = $this->loadVmInterfaces();
$this->interfaces = $result['interfaces'];
$this->interfaceConfigs = $result['configs'];
$this->interfaceSelections = collect($this->interfaces)
->map(fn ($meta) => $meta['model'] ?? 'virtio')
->toArray();
$this->interfaceMultiqueues = collect($this->interfaces)
->map(fn ($meta) => $meta['queues'] ?? 1)
->toArray();
$this->selectedInterface = array_key_first($this->interfaces) ?? 'net0';
$this->interfaceType = $this->interfaceSelections[$this->selectedInterface] ?? 'virtio';
$this->canRemoveInterfaces = count($this->interfaces) > 1;
// Update original values after refresh
$this->original = [
'interfaceSelections' => json_decode(json_encode($this->interfaceSelections), true),
'interfaceMultiqueues' => json_decode(json_encode($this->interfaceMultiqueues), true),
];
$this->notify('Network interfaces refreshed.', 'success');
}
public function queueInterfaceChange(string $name): void
{
$this->selectedInterface = $name;
$this->interfaceSelections[$name] = $this->interfaceSelections[$name] ?? ($this->interfaces[$name]['model'] ?? 'virtio');
$this->interfaceType = $this->interfaceSelections[$name];
}
public function updateMultiqueue(string $name, int $queues): void
{
if (!isset($this->interfaces[$name])) {
$this->notify('Network device not found.', 'error');
return;
}
$cpuCores = $this->getCpuCores();
$queues = max(1, min($queues, $cpuCores));
$this->interfaceMultiqueues[$name] = $queues;
try {
$proxmox = new Proxmox;
$currentConfig = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
$currentValue = (string) ($currentConfig[$name] ?? '');
if (empty($currentValue)) {
$this->notify('Network device configuration not found.', 'error');
return;
}
$segments = array_filter(array_map('trim', explode(',', $currentValue)));
$newSegments = [];
$queuesFound = false;
foreach ($segments as $segment) {
if (!str_contains($segment, '=')) {
$newSegments[] = $segment;
continue;
}
[$key, $value] = array_map('trim', explode('=', $segment, 2));
$lowerKey = strtolower($key);
if ($lowerKey === 'queues') {
$queuesFound = true;
$newSegments[] = 'queues=' . $queues;
} else {
$newSegments[] = $key . '=' . $value;
}
}
// If queues parameter wasn't found, add it
if (!$queuesFound) {
$newSegments[] = 'queues=' . $queues;
}
$newValue = implode(',', $newSegments);
$proxmox->request(
'/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config',
'put',
[$name => $newValue]
);
$this->interfaces[$name]['queues'] = $queues;
// Update original state to reflect the saved change
if (isset($this->original['interfaceMultiqueues'])) {
$this->original['interfaceMultiqueues'][$name] = $queues;
}
$this->notify('Multiqueue updated for ' . strtoupper($name) . ' to ' . $queues . ' queues.', 'success');
} catch (\Exception $e) {
$this->notify('Failed to update multiqueue: ' . $e->getMessage(), 'error');
}
}
public function getCanAddInterfaceProperty(): bool
{
return count($this->interfaces) < 5;
}
protected function getNextInterfaceName(): string
{
$existing = array_keys($this->interfaces);
for ($i = 0; $i < 5; $i++) {
$name = 'net' . $i;
if (!in_array($name, $existing, true)) {
return $name;
}
}
return 'net' . count($existing);
}
public function addInterface(): void
{
if (!$this->canAddInterface) {
$this->notify('Maximum of 5 network devices allowed.', 'error');
return;
}
// Check VM status - must be stopped to add interfaces
if (!$this->status || $this->status->status !== 'stopped') {
$this->notify('VM must be stopped to add network devices.', 'error');
return;
}
try {
// Get bridge and MTU from the lowest numbered network device
$interfaceKeys = array_keys($this->interfaceConfigs);
if (empty($interfaceKeys)) {
$this->notify('No existing network devices found.', 'error');
return;
}
// Sort interface keys to find the lowest numbered one (net0, net1, etc.)
usort($interfaceKeys, function ($a, $b) {
$numA = (int) preg_replace('/[^0-9]/', '', $a);
$numB = (int) preg_replace('/[^0-9]/', '', $b);
return $numA <=> $numB;
});
$lowestInterface = $interfaceKeys[0];
$sourceConfig = $this->interfaceConfigs[$lowestInterface] ?? '';
$bridge = null;
$mtu = null;
if ($sourceConfig) {
$segments = array_filter(array_map('trim', explode(',', $sourceConfig)));
foreach ($segments as $segment) {
if (str_contains($segment, '=')) {
[$key, $value] = array_map('trim', explode('=', $segment, 2));
if (strtolower($key) === 'bridge') {
$bridge = $value;
} elseif (strtolower($key) === 'mtu') {
$mtu = $value;
}
}
}
}
$newInterfaceName = $this->getNextInterfaceName();
$macAddress = $this->generateMacAddress();
$newConfig = 'virtio=' . $macAddress;
if ($bridge) {
$newConfig .= ',bridge=' . $bridge;
}
if ($mtu) {
$newConfig .= ',mtu=' . $mtu;
}
$proxmox = new Proxmox;
$proxmox->request(
'/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config',
'put',
[$newInterfaceName => $newConfig]
);
// Refresh interfaces to get the new device
$this->refreshInterfaces();
$this->selectedInterface = $newInterfaceName;
$this->notify('Network device ' . strtoupper($newInterfaceName) . ' added successfully.', 'success');
} catch (\Exception $e) {
$this->notify('Failed to add network device: ' . $e->getMessage(), 'error');
}
}
public function toggleInterfaceLink(string $name): void
{
if (!isset($this->interfaces[$name])) {
$this->notify('Network device not found.', 'error');
return;
}
try {
$proxmox = new Proxmox;
$currentConfig = $proxmox->request('/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config')->json()['data'] ?? [];
$currentValue = (string) ($currentConfig[$name] ?? '');
if (empty($currentValue)) {
$this->notify('Network device configuration not found.', 'error');
return;
}
$currentLinkdown = $this->interfaces[$name]['linkdown'] ?? false;
$segments = array_filter(array_map('trim', explode(',', $currentValue)));
$newSegments = [];
$linkDownFound = false;
foreach ($segments as $segment) {
if (!str_contains($segment, '=')) {
$newSegments[] = $segment;
continue;
}
[$key, $value] = array_map('trim', explode('=', $segment, 2));
$lowerKey = strtolower($key);
if ($lowerKey === 'link_down' || $lowerKey === 'linkdown') {
$linkDownFound = true;
// Remove link_down when connecting
if ($currentLinkdown) {
continue; // Skip it to remove
}
// Keep it when disconnecting (already has it)
$newSegments[] = 'link_down=1';
} elseif ($lowerKey === 'bridge') {
$newSegments[] = $key . '=' . $value;
// If we need to add link_down (disconnecting) and haven't found it yet, add it after bridge
if (!$currentLinkdown && !$linkDownFound) {
$newSegments[] = 'link_down=1';
$linkDownFound = true;
}
} elseif ($lowerKey === 'mtu') {
// If we need to add link_down and haven't yet, add it before mtu
if (!$currentLinkdown && !$linkDownFound) {
$newSegments[] = 'link_down=1';
$linkDownFound = true;
}
$newSegments[] = $key . '=' . $value;
} else {
$newSegments[] = $key . '=' . $value;
}
}
// If we need to add link_down and it still wasn't added (no bridge or mtu found)
if (!$currentLinkdown && !$linkDownFound) {
$newSegments[] = 'link_down=1';
}
$newValue = implode(',', $newSegments);
$proxmox->request(
'/nodes/' . $this->server->node->name . '/' . $this->server->node->vmType() . '/' . $this->server->vm_id . '/config',
'put',
[$name => $newValue]
);
// Refresh interfaces to get updated state
$this->refreshInterfaces();
$status = $currentLinkdown ? 'connected' : 'disconnected';
$this->notify('Network device ' . strtoupper($name) . ' ' . $status . ' successfully.', 'success');
} catch (\Exception $e) {
$this->notify('Failed to toggle network device: ' . $e->getMessage(), 'error');
}
}
protected function sortInterfaceArrays(array $interfaces, array $configs): array
{
$keys = array_keys($interfaces);
usort($keys, function ($a, $b) {
return $this->interfaceIndexFromKey($a) <=> $this->interfaceIndexFromKey($b);
});
$sortedInterfaces = [];
$sortedConfigs = [];
foreach ($keys as $key) {
$sortedInterfaces[$key] = $interfaces[$key];
$sortedConfigs[$key] = $configs[$key] ?? '';
}
return [$sortedInterfaces, $sortedConfigs];
}
protected function interfaceIndexFromKey(string $key): int
{
if (preg_match('/(\d+)/', $key, $matches)) {
return (int) $matches[1];
}
return PHP_INT_MAX;
}
}