ensureDevelopmentEnvironment(); $profiles = config('development-qemu.profiles'); $profile = $profiles[$profileName] ?? null; if (! is_array($profile)) { throw new InvalidArgumentException("Unknown development QEMU profile: {$profileName}"); } ConfigureDevelopmentQemuHost::run(); $this->configureDhcpReservation($profile); if ($resetManagedVms) { foreach ($profiles as $managedProfile) { Process::run('virsh destroy '.escapeshellarg($managedProfile['domain'])); Process::run('virsh undefine '.escapeshellarg($managedProfile['domain'])); $this->deleteVmData($managedProfile['domain']); } } $this->createVm($profile); ConfigureDevelopmentQemuHost::run(); $this->waitForSsh($profile['ip']); } /** @param array{domain: string, ip: string, user: string, mac: string, image: string, image_url: string, os_variant: string, provisioner: string} $profile */ private function createVm(array $profile): void { $directory = config('development-qemu.storage_path'); File::ensureDirectoryExists($directory); File::chmod($directory, 0777); $this->moveLegacyFiles($directory); $baseImage = "{$directory}/{$profile['image']}"; $disk = "{$directory}/{$profile['domain']}.qcow2"; $userData = "{$directory}/{$profile['domain']}-user-data.yaml"; $networkConfig = "{$directory}/{$profile['domain']}-network.yaml"; if (! File::exists($baseImage)) { $this->runOrFail(sprintf( 'curl --fail --location --output %s %s', escapeshellarg($baseImage), escapeshellarg($profile['image_url']), )); } if (! File::exists($disk)) { $this->runOrFail(sprintf( 'qemu-img create -f qcow2 -F qcow2 -b %s %s %s', escapeshellarg($baseImage), escapeshellarg($disk), escapeshellarg(config('development-qemu.disk_size')), )); } if (File::exists($baseImage)) { File::chmod($baseImage, 0644); } if (File::exists($disk)) { File::chmod($disk, 0666); } File::put($userData, $this->userData($profile)); File::put($networkConfig, $this->networkConfig($profile)); $this->runOrFail(sprintf( 'virt-install --connect qemu:///system --name %s --memory %d --vcpus %d --import --os-variant %s --disk path=%s,format=qcow2,bus=virtio --network network=%s,model=virtio,mac=%s --cloud-init user-data=%s,network-config=%s,disable=on --noautoconsole', escapeshellarg($profile['domain']), config('development-qemu.memory'), config('development-qemu.vcpus'), escapeshellarg($profile['os_variant']), escapeshellarg($disk), escapeshellarg(config('development-qemu.libvirt_network')), escapeshellarg($profile['mac']), escapeshellarg($userData), escapeshellarg($networkConfig), )); } private function moveLegacyFiles(string $directory): void { $legacyDirectory = storage_path('app/development-qemu'); if ($legacyDirectory === $directory || ! File::isDirectory($legacyDirectory)) { return; } foreach (File::files($legacyDirectory) as $file) { $destination = "{$directory}/{$file->getFilename()}"; if (! File::exists($destination)) { File::move($file->getPathname(), $destination); } } } private function deleteVmData(string $domain): void { $directory = config('development-qemu.storage_path'); File::delete([ "{$directory}/{$domain}.qcow2", "{$directory}/{$domain}-user-data.yaml", "{$directory}/{$domain}-network.yaml", ]); } /** @param array{user: string, provisioner: string} $profile */ private function userData(array $profile): string { $publicKey = config('development-qemu.public_key'); $adminGroup = $profile['provisioner'] === 'apt' ? 'sudo' : 'wheel'; $sudo = $profile['user'] === 'root' ? '' : " groups: [{$adminGroup}]\n sudo: ALL=(ALL) NOPASSWD:ALL\n"; [$packages, $startDocker] = match ($profile['provisioner']) { 'apk' => [" - docker\n - sudo", 'rc-update add docker default && service docker start'], 'rpm' => [" - curl\n - sudo", 'curl -fsSL https://get.docker.com | sh && systemctl enable --now docker'], default => [" - docker.io\n - sudo", 'systemctl enable --now docker'], }; $addUserToDockerGroup = $profile['user'] === 'root' ? '' : "\n - usermod -aG docker {$profile['user']}"; return <<failed()) { throw new RuntimeException(trim($networkXml->errorOutput()) ?: 'Unable to inspect the libvirt network.'); } if (str_contains($networkXml->output(), $profile['mac']) && str_contains($networkXml->output(), $profile['ip'])) { return; } $host = sprintf("", $profile['mac'], $profile['domain'], $profile['ip']); $this->runOrFail("virsh net-update {$network} add-last ip-dhcp-host ".escapeshellarg($host).' --live --config'); } private function waitForSsh(string $ip): void { $container = escapeshellarg(config('development-qemu.coolify_container')); $probe = <<<'PHP' $deadline = time() + 120; do { $socket = @fsockopen($argv[1], 22, $errorCode, $errorMessage, 1); if (is_resource($socket)) { fclose($socket); exit(0); } sleep(1); } while (time() < $deadline); exit(1); PHP; $this->runOrFail("docker exec {$container} php -r ".escapeshellarg($probe).' '.escapeshellarg($ip)); } private function runOrFail(string $command): void { $result = Process::forever()->run($command); if ($result->failed()) { throw new RuntimeException(trim($result->errorOutput()) ?: "Command failed: {$command}"); } } private function ensureDevelopmentEnvironment(): void { if (! in_array(config('app.env'), ['local', 'development', 'dev'], true)) { throw new RuntimeException('QEMU VMs may only be managed in development environments.'); } } }