-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
### What does this PR do? * Using QEMU we add a feature to "Launch VM" in the background * Uses websockets, qemu as well as our xterm.js library to achieve this * Launches in "snapshot" mode so no data is written to .raw file so the file can be easily re-used ### Screenshot / video of UI <!-- If this PR is changing UI, please include screenshots or screencasts showing the difference --> ### What issues does this PR fix or reference? <!-- Include any related issues from Podman Desktop repository (or from another issue tracker). --> Closes #813 ### How to test this PR? <!-- Please explain steps to reproduce --> 1. Be on macOS silicon 2. `brew install qemu` 3. Build a bootc container image 4. Press launch VM button in actions bar Signed-off-by: Charlie Drage <[email protected]>
- Loading branch information
Showing
22 changed files
with
620 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# QEMU Install | ||
|
||
Virtual Machine support is **experimental** and is only meant to run *one VM at a time* within the BootC extension. | ||
|
||
Below are installation instructions on how to get started. | ||
|
||
We currently only support macOS. | ||
|
||
## macOS | ||
|
||
Install QEMU on macOS by running the following with `brew`: | ||
|
||
```sh | ||
brew install qemu | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,6 +65,7 @@ | |
"vitest": "^2.0.2" | ||
}, | ||
"dependencies": { | ||
"@xterm/addon-attach": "^0.11.0", | ||
"js-yaml": "^4.1.0" | ||
}, | ||
"packageManager": "[email protected]+sha512.73a29afa36a0d092ece5271de5177ecbf8318d454ecd701343131b8ebc0c1a91c487da46ab77c8e596d6acf1461e3594ced4becedf8921b074fbd8653ed7051c" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import path from 'node:path'; | ||
import * as extensionApi from '@podman-desktop/api'; | ||
import { isMac } from './machine-utils'; | ||
import fs from 'node:fs'; | ||
|
||
// Ignore the following line as this is where we will be storing the pid file | ||
// similar to other projects that use pid files in /tmp | ||
// eslint-disable-next-line sonarjs/publicly-writable-directories | ||
const pidFile = '/tmp/qemu-podman-desktop.pid'; | ||
|
||
// Must use "homebrew" qemu binaries on macOS | ||
// as they are found to be the most stable and reliable for the project | ||
// as well as containing the necessary "edk2-aarch64-code.fd" file | ||
// it is not advised to use the qemu binaries from qemu.org due to edk2-aarch64-code.fd not being included. | ||
const macQemuArm64Binary = '/opt/homebrew/bin/qemu-system-aarch64'; | ||
const macQemuArm64Edk2 = '/opt/homebrew/share/qemu/edk2-aarch64-code.fd'; | ||
const macQemuX86Binary = '/opt/homebrew/bin/qemu-system-x86_64'; | ||
|
||
// Host port forwarding for VM we will by default port forward 22 on the bootable container | ||
// to :2222 on the host | ||
const hostForwarding = 'hostfwd=tcp::2222-:22'; | ||
|
||
// Default memory size for the VM and websocket port location | ||
const memorySize = '4G'; | ||
const websocketPort = '45252'; | ||
|
||
// Raw image location | ||
const rawImageLocation = 'image/disk.raw'; | ||
|
||
export async function launchVM(folder: string, architecture: string): Promise<void> { | ||
// Will ONLY work with RAW images located at image/disk.raw which is the default output location | ||
const diskImage = path.join(folder, rawImageLocation); | ||
|
||
// Check to see that the disk image exists before continuing | ||
if (!fs.existsSync(diskImage)) { | ||
throw new Error(`Raw disk image not found: ${diskImage}`); | ||
} | ||
|
||
// Before launching, make sure that we stop any previously running VM's and ignore any errors when stopping | ||
try { | ||
await stopVM(); | ||
} catch (e) { | ||
console.error('Error stopping VM, it may have already been stopped: ', e); | ||
} | ||
|
||
// Generate the launch command and then run process.exec | ||
try { | ||
const command = generateLaunchCommand(diskImage, architecture); | ||
|
||
// If generateLaunchCommand returns an empty array, then we are not able to launch the VM | ||
// so simply error out and return | ||
if (command.length === 0) { | ||
throw new Error( | ||
'Unable to generate the launch command for the VM, must be on the appropriate OS (mac or linux) and architecture (x86_64 or aarch64)', | ||
); | ||
} | ||
|
||
// Execute the command | ||
await extensionApi.process.exec('sh', ['-c', `${command.join(' ')}`]); | ||
} catch (e) { | ||
// Output the stderr information if it exists as that helps with debugging | ||
// why the command could not run. | ||
if (e instanceof Error && 'stderr' in e) { | ||
console.error('Error launching VM: ', e.stderr); | ||
} else { | ||
console.error('Error launching VM: ', e); | ||
} | ||
throw e; | ||
} | ||
} | ||
|
||
// Stop VM by killing the process with the pid file (/tmp/qemu-podman-desktop.pid) | ||
export async function stopVM(): Promise<void> { | ||
try { | ||
await extensionApi.process.exec('sh', ['-c', `kill -9 \`cat ${pidFile}\``]); | ||
} catch (e) { | ||
if (e instanceof Error && 'stderr' in e) { | ||
console.error('Error stopping VM: ', e.stderr); | ||
} else { | ||
console.error('Error stopping VM: ', e); | ||
} | ||
} | ||
} | ||
|
||
// Generate launch command for qemu | ||
// this all depends on what architecture we are launching as well as | ||
// operating system | ||
function generateLaunchCommand(diskImage: string, architecture: string): string[] { | ||
let command: string[] = []; | ||
switch (architecture) { | ||
// Case for anything amd64 | ||
case 'amd64': | ||
if (isMac()) { | ||
command = [ | ||
macQemuX86Binary, | ||
'-m', | ||
memorySize, | ||
'-nographic', | ||
'-cpu', | ||
'Broadwell-v4', | ||
'-pidfile', | ||
pidFile, | ||
'-serial', | ||
`websocket:127.0.0.1:${websocketPort},server,nowait`, | ||
'-netdev', | ||
`user,id=mynet0,${hostForwarding}`, | ||
'-device', | ||
'e1000,netdev=mynet0', | ||
// Make sure we always have snapshot here as we don't want to modify the original image | ||
'-snapshot', | ||
diskImage, | ||
]; | ||
} | ||
break; | ||
|
||
// For any arm64 images | ||
case 'arm64': | ||
if (isMac()) { | ||
command = [ | ||
macQemuArm64Binary, | ||
'-m', | ||
memorySize, | ||
'-nographic', | ||
'-M', | ||
'virt', | ||
'-accel', | ||
'hvf', | ||
'-cpu', | ||
'host', | ||
'-smp', | ||
'4', | ||
'-serial', | ||
`websocket:127.0.0.1:${websocketPort},server,nowait`, | ||
'-pidfile', | ||
pidFile, | ||
'-netdev', | ||
`user,id=usernet,${hostForwarding}`, | ||
'-device', | ||
'virtio-net,netdev=usernet', | ||
'-drive', | ||
`file=${macQemuArm64Edk2},format=raw,if=pflash,readonly=on`, | ||
// Make sure we always have snapshot here as we don't want to modify the original image | ||
'-snapshot', | ||
diskImage, | ||
]; | ||
} | ||
break; | ||
default: | ||
break; | ||
} | ||
return command; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<script lang="ts"> | ||
import Label from './lib/upstream/Label.svelte'; | ||
export let status: string; | ||
function getClassColor(): string { | ||
if ( | ||
status.includes('Connection closed') || | ||
status.includes('Connection error') || | ||
status.includes('VM launch error') | ||
) { | ||
return 'bg-[var(--pd-status-disconnected)]'; | ||
} | ||
return 'bg-[var(--pd-status-connected)]'; | ||
} | ||
</script> | ||
|
||
{#if status} | ||
<Label role="status" name={status}><div class="w-2 h-2 {getClassColor()} rounded-full mx-1"></div></Label> | ||
{/if} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.