mirror of
https://github.com/robcholz/vibebox.git
synced 2026-04-01 00:10:15 +02:00
feat: added disk support
This commit is contained in:
@@ -29,6 +29,7 @@ fn main() -> Result<()> {
|
||||
let args = vm::VmArg {
|
||||
cpu_count: config.box_cfg.cpu_count,
|
||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||
disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
|
||||
no_default_mounts: false,
|
||||
mounts: config.box_cfg.mounts.clone(),
|
||||
};
|
||||
|
||||
@@ -63,6 +63,7 @@ fn main() -> Result<()> {
|
||||
let args = vm::VmArg {
|
||||
cpu_count: config.box_cfg.cpu_count,
|
||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||
disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
|
||||
no_default_mounts: false,
|
||||
mounts: config.box_cfg.mounts.clone(),
|
||||
};
|
||||
@@ -80,6 +81,7 @@ fn main() -> Result<()> {
|
||||
let vm_args = vm::VmArg {
|
||||
cpu_count: config.box_cfg.cpu_count,
|
||||
ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
|
||||
disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
|
||||
no_default_mounts: false,
|
||||
mounts: config.box_cfg.mounts.clone(),
|
||||
};
|
||||
@@ -88,6 +90,7 @@ fn main() -> Result<()> {
|
||||
let vm_info = VmInfo {
|
||||
max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
|
||||
cpu_cores: vm_args.cpu_count,
|
||||
max_disk_gb: (vm_args.disk_bytes as f32) / 1024.0 / 1024.0 / 1024.0,
|
||||
system_name: "Debian".to_string(), // TODO: read system name from the VM.
|
||||
auto_shutdown_ms,
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ pub const CONFIG_PATH_ENV: &str = "VIBEBOX_CONFIG_PATH";
|
||||
const DEFAULT_CPU_COUNT: usize = 2;
|
||||
const DEFAULT_RAM_MB: u64 = 2048;
|
||||
const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000;
|
||||
const DEFAULT_DISK_GB: u64 = 5;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
@@ -25,6 +26,7 @@ pub struct Config {
|
||||
pub struct BoxConfig {
|
||||
pub cpu_count: usize,
|
||||
pub ram_mb: u64,
|
||||
pub disk_gb: u64,
|
||||
pub mounts: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -33,6 +35,7 @@ impl Default for BoxConfig {
|
||||
Self {
|
||||
cpu_count: default_cpu_count(),
|
||||
ram_mb: default_ram_mb(),
|
||||
disk_gb: default_disk_gb(),
|
||||
mounts: default_mounts(),
|
||||
}
|
||||
}
|
||||
@@ -67,6 +70,10 @@ fn default_mounts() -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn default_disk_gb() -> u64 {
|
||||
DEFAULT_DISK_GB
|
||||
}
|
||||
|
||||
pub fn config_path(project_root: &Path) -> PathBuf {
|
||||
project_root.join(CONFIG_FILENAME)
|
||||
}
|
||||
@@ -102,7 +109,7 @@ pub fn load_config_with_path(project_root: &Path, override_path: Option<&Path>)
|
||||
tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
|
||||
if trimmed.is_empty() {
|
||||
die(&format!(
|
||||
"config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)",
|
||||
"config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].disk_gb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
@@ -193,6 +200,7 @@ fn validate_schema(value: &toml::Value) -> Vec<String> {
|
||||
Some(table) => {
|
||||
validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors);
|
||||
validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors);
|
||||
validate_int(table, "disk_gb", "[box].disk_gb (integer)", &mut errors);
|
||||
validate_string_array(
|
||||
table,
|
||||
"mounts",
|
||||
@@ -259,6 +267,9 @@ fn validate_or_exit(config: &Config) {
|
||||
if config.box_cfg.ram_mb == 0 {
|
||||
die("box.ram_mb must be >= 1");
|
||||
}
|
||||
if config.box_cfg.disk_gb == 0 {
|
||||
die("box.disk_gb must be >= 1");
|
||||
}
|
||||
if config.supervisor.auto_shutdown_ms == 0 {
|
||||
die("supervisor.auto_shutdown_ms must be >= 1");
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
git \
|
||||
ripgrep \
|
||||
cloud-guest-utils \
|
||||
openssh-server \
|
||||
sudo
|
||||
|
||||
|
||||
55
src/resize_disk.sh
Normal file
55
src/resize_disk.sh
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DEV="$(findmnt / -n -o SOURCE || true)"
|
||||
ROOT_FSTYPE="$(findmnt / -n -o FSTYPE || true)"
|
||||
|
||||
if [ -z "$ROOT_DEV" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DISK_DEV=""
|
||||
PART_NUM=""
|
||||
|
||||
if command -v lsblk >/dev/null 2>&1; then
|
||||
DISK_DEV="$(lsblk -no pkname "$ROOT_DEV" 2>/dev/null | head -n1 || true)"
|
||||
PART_NUM="$(lsblk -no PARTNUM "$ROOT_DEV" 2>/dev/null | head -n1 || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$DISK_DEV" ] || [ -z "$PART_NUM" ]; then
|
||||
ROOT_BASENAME="$(basename "$ROOT_DEV")"
|
||||
if echo "$ROOT_BASENAME" | grep -Eq '^nvme.+p[0-9]+$'; then
|
||||
DISK_DEV="/dev/${ROOT_BASENAME%p[0-9]*}"
|
||||
PART_NUM="${ROOT_BASENAME##*p}"
|
||||
elif echo "$ROOT_BASENAME" | grep -Eq '^[a-z]+[0-9]+$'; then
|
||||
DISK_DEV="/dev/${ROOT_BASENAME%%[0-9]*}"
|
||||
PART_NUM="${ROOT_BASENAME##*[a-z]}"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$DISK_DEV" ] && [ -n "$PART_NUM" ]; then
|
||||
if command -v growpart >/dev/null 2>&1; then
|
||||
growpart "$DISK_DEV" "$PART_NUM" || true
|
||||
elif command -v sfdisk >/dev/null 2>&1; then
|
||||
sfdisk -N "$PART_NUM" --force "$DISK_DEV" <<'EOF' || true
|
||||
,,
|
||||
EOF
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v partprobe >/dev/null 2>&1; then
|
||||
partprobe "$DISK_DEV" || true
|
||||
fi
|
||||
|
||||
case "$ROOT_FSTYPE" in
|
||||
ext4|ext3|ext2)
|
||||
if command -v resize2fs >/dev/null 2>&1; then
|
||||
resize2fs "$ROOT_DEV" || true
|
||||
fi
|
||||
;;
|
||||
xfs)
|
||||
if command -v xfs_growfs >/dev/null 2>&1; then
|
||||
xfs_growfs / || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -41,6 +41,7 @@ const INFO_LINE_COUNT: u16 = 5;
|
||||
pub struct VmInfo {
|
||||
pub max_memory_mb: u64,
|
||||
pub cpu_cores: usize,
|
||||
pub max_disk_gb: f32,
|
||||
pub system_name: String,
|
||||
pub auto_shutdown_ms: u64,
|
||||
}
|
||||
@@ -570,11 +571,11 @@ fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
|
||||
Span::styled(&app.vm_info.system_name, Style::default().fg(Color::Green)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::raw("CPU / Memory: "),
|
||||
Span::raw("CPU / Memory / Disk: "),
|
||||
Span::styled(
|
||||
format!(
|
||||
"{} cores / {} MB",
|
||||
app.vm_info.cpu_cores, app.vm_info.max_memory_mb
|
||||
"{} cores / {} MB / {} GB",
|
||||
app.vm_info.cpu_cores, app.vm_info.max_memory_mb, app.vm_info.max_disk_gb
|
||||
),
|
||||
Style::default().fg(Color::Green),
|
||||
),
|
||||
|
||||
57
src/vm.rs
57
src/vm.rs
@@ -74,6 +74,7 @@ impl Drop for StatusFile {
|
||||
}
|
||||
const PROVISION_SCRIPT: &str = include_str!("provision.sh");
|
||||
const PROVISION_SCRIPT_NAME: &str = "provision.sh";
|
||||
const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
|
||||
const DEFAULT_RAW_NAME: &str = "default.raw";
|
||||
const INSTANCE_RAW_NAME: &str = "instance.raw";
|
||||
const BASE_DISK_RAW_NAME: &str = "disk.raw";
|
||||
@@ -166,6 +167,7 @@ fn expand_tilde_path(value: &str) -> PathBuf {
|
||||
pub struct VmArg {
|
||||
pub cpu_count: usize,
|
||||
pub ram_bytes: u64,
|
||||
pub disk_bytes: u64,
|
||||
pub no_default_mounts: bool,
|
||||
pub mounts: Vec<String>,
|
||||
}
|
||||
@@ -230,7 +232,15 @@ where
|
||||
std::slice::from_ref(&mise_directory_share),
|
||||
Some(&status_file),
|
||||
)?;
|
||||
ensure_instance_disk(&instance_raw, &default_raw, Some(&status_file))?;
|
||||
let _ = ensure_instance_disk(
|
||||
&instance_raw,
|
||||
&default_raw,
|
||||
args.disk_bytes,
|
||||
Some(&status_file),
|
||||
)?;
|
||||
let base_size = fs::metadata(&default_raw)?.len();
|
||||
let instance_size = fs::metadata(&instance_raw)?.len();
|
||||
let needs_resize = instance_size > base_size;
|
||||
let disk_path = instance_raw;
|
||||
|
||||
let mut login_actions = Vec::new();
|
||||
@@ -263,6 +273,11 @@ where
|
||||
directory_shares.push(DirectoryShare::from_mount_spec(spec)?);
|
||||
}
|
||||
|
||||
if needs_resize {
|
||||
let resize_cmd = script_command_from_content("resize_disk", RESIZE_DISK_SCRIPT)?;
|
||||
login_actions.push(Send(resize_cmd));
|
||||
}
|
||||
|
||||
if let Some(motd_action) = motd_login_action(&directory_shares) {
|
||||
login_actions.push(motd_action);
|
||||
}
|
||||
@@ -559,19 +574,51 @@ fn ensure_default_image(
|
||||
fn ensure_instance_disk(
|
||||
instance_raw: &Path,
|
||||
template_raw: &Path,
|
||||
target_bytes: u64,
|
||||
status: Option<&StatusFile>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
if instance_raw.exists() {
|
||||
return Ok(());
|
||||
let current_size = fs::metadata(instance_raw)?.len();
|
||||
if current_size != target_bytes {
|
||||
let current_gb = current_size as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||
let target_gb = target_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
|
||||
tracing::warn!(
|
||||
current_bytes = current_size,
|
||||
target_bytes,
|
||||
"instance disk size does not match config (current {:.2} GB, config {:.2} GB); disk_gb applies only on init. Run `vibebox reset` to recreate or set disk_gb to match; using existing disk.",
|
||||
current_gb,
|
||||
target_gb
|
||||
);
|
||||
}
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let template_size = fs::metadata(template_raw)?.len();
|
||||
if target_bytes < template_size {
|
||||
return Err(format!(
|
||||
"Requested disk size {} bytes is smaller than base image size {} bytes",
|
||||
target_bytes, template_size
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let target_size = target_bytes;
|
||||
let needs_resize = target_size > template_size;
|
||||
|
||||
if let Some(status) = status {
|
||||
status.update("creating instance disk...");
|
||||
}
|
||||
tracing::info!(path = %template_raw.display(), "creating instance disk");
|
||||
std::fs::create_dir_all(instance_raw.parent().unwrap())?;
|
||||
fs::copy(template_raw, instance_raw)?;
|
||||
Ok(())
|
||||
if target_size == template_size {
|
||||
fs::copy(template_raw, instance_raw)?;
|
||||
return Ok(needs_resize);
|
||||
}
|
||||
|
||||
let mut dst = std::fs::File::create(instance_raw)?;
|
||||
dst.set_len(target_size)?;
|
||||
let mut src = std::fs::File::open(template_raw)?;
|
||||
std::io::copy(&mut src, &mut dst)?;
|
||||
Ok(needs_resize)
|
||||
}
|
||||
|
||||
pub struct IoContext {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[box]
|
||||
cpu_count = 2
|
||||
ram_mb = 2048
|
||||
disk_gb = 5
|
||||
mounts = [
|
||||
"~/.codex:~/.codex:read-write",
|
||||
"~/.claude:~/.claude:read-write",
|
||||
|
||||
Reference in New Issue
Block a user