+5
-3
@@ -1,11 +1,13 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Mount special filesystems
|
# Create console device
|
||||||
mkdir -p /dev /proc /sys
|
# TODO: is this necessary?
|
||||||
[ -c /dev/console ] || mknod -m 600 /dev/console c 5 1
|
[ -c /dev/console ] || mknod -m 600 /dev/console c 5 1
|
||||||
|
|
||||||
|
# Mount special filesystems
|
||||||
|
mkdir -p /proc /sys
|
||||||
mount -t proc proc /proc
|
mount -t proc proc /proc
|
||||||
mount -t sysfs sysfs /sys
|
mount -t sysfs sysfs /sys
|
||||||
mount -t devtmpfs dev /dev
|
|
||||||
|
|
||||||
# Networking
|
# Networking
|
||||||
echo slim > /proc/sys/kernel/hostname
|
echo slim > /proc/sys/kernel/hostname
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ mkdir -p /dev /proc /sys
|
|||||||
[ -c /dev/console ] || mknod -m 600 /dev/console c 5 1
|
[ -c /dev/console ] || mknod -m 600 /dev/console c 5 1
|
||||||
mount -t proc proc /proc
|
mount -t proc proc /proc
|
||||||
mount -t sysfs sysfs /sys
|
mount -t sysfs sysfs /sys
|
||||||
mount -t devtmpfs dev /dev
|
|
||||||
|
|
||||||
# Networking
|
# Networking
|
||||||
echo slim > /proc/sys/kernel/hostname
|
echo slim > /proc/sys/kernel/hostname
|
||||||
|
|||||||
+13
-17
@@ -43,10 +43,10 @@ pub(crate) fn build(BuildCmd { kind, name, init }: BuildCmd) -> Result<()> {
|
|||||||
ImageKind::Qcow2 => build_qcow2(image, &mount_path),
|
ImageKind::Qcow2 => build_qcow2(image, &mount_path),
|
||||||
};
|
};
|
||||||
|
|
||||||
let unmounted = Command::new("podman")
|
let unmounted = cmd(&[
|
||||||
.args(["unshare", "--", "podman", "image", "unmount", image])
|
"podman", "unshare", "--", "podman", "image", "unmount", image,
|
||||||
.output()
|
])
|
||||||
.is_ok_and(|out| out.status.success());
|
.is_ok();
|
||||||
if unmounted {
|
if unmounted {
|
||||||
println!("Unmounted image.");
|
println!("Unmounted image.");
|
||||||
} else {
|
} else {
|
||||||
@@ -137,19 +137,15 @@ fn build_initrd(image: &str, mount_path: &Path) -> Result<()> {
|
|||||||
println!("Registry: {}", reg_dir.display());
|
println!("Registry: {}", reg_dir.display());
|
||||||
|
|
||||||
let vmlinuz_dst = reg_dir.join("vmlinuz");
|
let vmlinuz_dst = reg_dir.join("vmlinuz");
|
||||||
let cp_status = Command::new("podman")
|
cmd(&[
|
||||||
.args([
|
"podman",
|
||||||
"unshare",
|
"unshare",
|
||||||
"--",
|
"--",
|
||||||
"cp",
|
"cp",
|
||||||
&vmlinuz_src,
|
&vmlinuz_src,
|
||||||
vmlinuz_dst.to_str().context("registry path is not UTF-8")?,
|
vmlinuz_dst.to_str().context("registry path is not UTF-8")?,
|
||||||
])
|
])
|
||||||
.status()
|
.context("Failed to copy vmlinuz out of the image mount")?;
|
||||||
.context("Failed to copy vmlinuz out of the image mount")?;
|
|
||||||
if !cp_status.success() {
|
|
||||||
bail!("Failed to copy vmlinuz to registry");
|
|
||||||
}
|
|
||||||
println!("Copied vmlinuz -> {}", vmlinuz_dst.display());
|
println!("Copied vmlinuz -> {}", vmlinuz_dst.display());
|
||||||
|
|
||||||
// Pack the rootfs as a gzipped newc cpio archive; vmlinuz is excluded
|
// Pack the rootfs as a gzipped newc cpio archive; vmlinuz is excluded
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ use std::process::Command;
|
|||||||
|
|
||||||
use anyhow::{Context, anyhow, bail};
|
use anyhow::{Context, anyhow, bail};
|
||||||
|
|
||||||
|
/// Spawn a subprocess with arguments.
|
||||||
|
///
|
||||||
|
/// Collects `stdout` and returns it on success.
|
||||||
|
/// Returns `Err` if the exit code isn't 0, or if the process failed to spawn.
|
||||||
pub fn cmd(cmd: &[&str]) -> anyhow::Result<String> {
|
pub fn cmd(cmd: &[&str]) -> anyhow::Result<String> {
|
||||||
let [cmd, args @ ..] = cmd else {
|
let [cmd, args @ ..] = cmd else {
|
||||||
bail!("missing command");
|
bail!("missing command");
|
||||||
|
|||||||
+121
@@ -0,0 +1,121 @@
|
|||||||
|
use std::{
|
||||||
|
fmt::{self, Display},
|
||||||
|
net::{Ipv4Addr, SocketAddr},
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{Context, anyhow, bail};
|
||||||
|
use serde::{Deserialize, Serialize, de::Error};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub enum Protocol {
|
||||||
|
Tcp,
|
||||||
|
Udp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct PortForward {
|
||||||
|
pub protocol: Protocol,
|
||||||
|
pub from: SocketAddr,
|
||||||
|
pub to_addr: Option<Ipv4Addr>,
|
||||||
|
pub to_port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Protocol {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(match self {
|
||||||
|
Protocol::Tcp => "tcp",
|
||||||
|
Protocol::Udp => "udp",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for PortForward {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}:{}-", self.protocol, self.from)?;
|
||||||
|
if let Some(to_addr) = self.to_addr {
|
||||||
|
write!(f, ":{}", to_addr)?;
|
||||||
|
}
|
||||||
|
write!(f, ":{}", self.to_port)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PortForward {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let result = || {
|
||||||
|
let (protocol, s) = s.split_once(':').context("Missing ':'")?;
|
||||||
|
let (from, to) = s.split_once('-').context("Missing '-'")?;
|
||||||
|
let (to_addr, to_port) = to.split_once(':').context("Missing ':'")?;
|
||||||
|
|
||||||
|
let from = if let Some(from) = from.strip_prefix(':') {
|
||||||
|
let port: u16 = from.parse().context("Invalid port number")?;
|
||||||
|
SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), port)
|
||||||
|
} else {
|
||||||
|
from.parse()?
|
||||||
|
};
|
||||||
|
|
||||||
|
let to_addr = if to_addr.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(to_addr.parse()?)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(PortForward {
|
||||||
|
from,
|
||||||
|
to_addr,
|
||||||
|
to_port: to_port.parse()?,
|
||||||
|
protocol: match protocol {
|
||||||
|
"udp" => Protocol::Udp,
|
||||||
|
"tcp" => Protocol::Tcp,
|
||||||
|
_ => bail!("Invalid protocol, expected 'udp' or 'tcp'"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
result()
|
||||||
|
.with_context(|| anyhow!("Expected '(tcp|udp):<address>-<address>', got {s:?}"))
|
||||||
|
.context("Malformed PortForward string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for PortForward {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
format_args!("{}", self).serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for PortForward {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let s: String = Deserialize::deserialize(deserializer)?;
|
||||||
|
s.parse().map_err(D::Error::custom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use super::PortForward;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_port_forwards() {
|
||||||
|
let valid = [
|
||||||
|
"tcp:0.0.0.0:80-:8080",
|
||||||
|
"udp::1234-:1234",
|
||||||
|
"tcp:0.0.0.0:80-1.2.3.4:8080",
|
||||||
|
"udp::1234-255.255.255.255:1234",
|
||||||
|
];
|
||||||
|
|
||||||
|
for valid in valid {
|
||||||
|
PortForward::from_str(valid).expect("Failed to parse a valid port forward string");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
mod build;
|
mod build;
|
||||||
mod command;
|
mod command;
|
||||||
|
mod forward;
|
||||||
mod image;
|
mod image;
|
||||||
mod meta;
|
mod meta;
|
||||||
mod qemu;
|
mod qemu;
|
||||||
|
|||||||
+26
-3
@@ -3,9 +3,10 @@
|
|||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
use std::process::Command;
|
use std::{fmt::Write as _, process::Command};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
forward::PortForward,
|
||||||
meta::Meta,
|
meta::Meta,
|
||||||
registry::{registry_base_dir, registry_dir},
|
registry::{registry_base_dir, registry_dir},
|
||||||
};
|
};
|
||||||
@@ -18,9 +19,19 @@ pub struct RunCmd {
|
|||||||
/// Amount of memory to give the VM, in qemu's format.
|
/// Amount of memory to give the VM, in qemu's format.
|
||||||
#[clap(short, long, default_value = "1024M")]
|
#[clap(short, long, default_value = "1024M")]
|
||||||
memory: String,
|
memory: String,
|
||||||
|
|
||||||
|
/// Forward ports from host to guest. Example: `tcp:0.0.0.0:80-:8080`
|
||||||
|
#[clap(long)]
|
||||||
|
forward: Vec<PortForward>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(RunCmd { name, memory }: RunCmd) -> Result<()> {
|
pub(crate) fn run(
|
||||||
|
RunCmd {
|
||||||
|
name,
|
||||||
|
memory,
|
||||||
|
forward,
|
||||||
|
}: RunCmd,
|
||||||
|
) -> Result<()> {
|
||||||
let reg_dir = registry_dir(&name)?;
|
let reg_dir = registry_dir(&name)?;
|
||||||
let vmlinuz_path = registry_base_dir()?.join("vmlinuz");
|
let vmlinuz_path = registry_base_dir()?.join("vmlinuz");
|
||||||
let initrd_path = reg_dir.join("initrd");
|
let initrd_path = reg_dir.join("initrd");
|
||||||
@@ -41,6 +52,7 @@ pub(crate) fn run(RunCmd { name, memory }: RunCmd) -> Result<()> {
|
|||||||
"earlyprintk=serial",
|
"earlyprintk=serial",
|
||||||
"nokaslr",
|
"nokaslr",
|
||||||
&init_arg,
|
&init_arg,
|
||||||
|
"devtmpfs.mount=1", // Automatically mount /dev at boot
|
||||||
];
|
];
|
||||||
|
|
||||||
let qcow2_arg = format!("file={},format=qcow2,if=virtio", qcow2_path.display());
|
let qcow2_arg = format!("file={},format=qcow2,if=virtio", qcow2_path.display());
|
||||||
@@ -63,15 +75,26 @@ pub(crate) fn run(RunCmd { name, memory }: RunCmd) -> Result<()> {
|
|||||||
|
|
||||||
let cmdline = cmdline.join(" ");
|
let cmdline = cmdline.join(" ");
|
||||||
|
|
||||||
|
// add NIC and forward any user specified ports
|
||||||
|
let mut network = "user,model=virtio-net-pci".to_string();
|
||||||
|
for forward in &forward {
|
||||||
|
_ = write!(&mut network, ",hostfwd={forward}");
|
||||||
|
}
|
||||||
|
|
||||||
let mut command = Command::new("qemu-system-x86_64");
|
let mut command = Command::new("qemu-system-x86_64");
|
||||||
|
|
||||||
|
// TODO: make a lot of this configurable. especially -cpus
|
||||||
command
|
command
|
||||||
|
.args(["-accel", "kvm"])
|
||||||
|
.args(["-cpu", "host", "-smp", "cpus=8"])
|
||||||
.args(["-m", &memory])
|
.args(["-m", &memory])
|
||||||
.arg("-kernel")
|
.arg("-kernel")
|
||||||
.arg(vmlinuz_path.as_os_str())
|
.arg(vmlinuz_path.as_os_str())
|
||||||
.args(fs_args)
|
.args(fs_args)
|
||||||
|
.args(["-snapshot"])
|
||||||
.args(["-append", &cmdline])
|
.args(["-append", &cmdline])
|
||||||
.args(["-nographic"])
|
.args(["-nographic"])
|
||||||
.args(["-nic", "user,model=virtio-net-pci"]);
|
.args(["-nic", &network]);
|
||||||
|
|
||||||
println!("{command:?}");
|
println!("{command:?}");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user