841 lines
31 KiB
PHP
841 lines
31 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 . '/qemu/' . $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 . '/qemu/' . $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 . '/qemu/' . $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 . '/qemu/' . $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 . '/qemu/' . $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;
|
|
}
|
|
|
|
try {
|
|
$proxmox = new Proxmox;
|
|
$proxmox->request(
|
|
'/nodes/' . $this->server->node->name . '/qemu/' . $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 . '/qemu/' . $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 . '/qemu/' . $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;
|
|
}
|
|
|
|
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 . '/qemu/' . $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 . '/qemu/' . $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 . '/qemu/' . $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;
|
|
}
|
|
}
|