on main help sign in
feat: added disk support
727adffrobcholz
src/bin/vibebox-supervisor.rs+1
@@ -29,6 +29,7 @@ fn main() -> Result<()> {
29 let args = vm::VmArg {
29 let args = vm::VmArg {
30 cpu_count: config.box_cfg.cpu_count,
30 cpu_count: config.box_cfg.cpu_count,
31 ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
31 ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
32+ disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
32 no_default_mounts: false,
33 no_default_mounts: false,
33 mounts: config.box_cfg.mounts.clone(),
34 mounts: config.box_cfg.mounts.clone(),
34 };
35 };
src/bin/vibebox.rs+3
@@ -63,6 +63,7 @@ fn main() -> Result<()> {
63 let args = vm::VmArg {
63 let args = vm::VmArg {
64 cpu_count: config.box_cfg.cpu_count,
64 cpu_count: config.box_cfg.cpu_count,
65 ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
65 ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
66+ disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
66 no_default_mounts: false,
67 no_default_mounts: false,
67 mounts: config.box_cfg.mounts.clone(),
68 mounts: config.box_cfg.mounts.clone(),
68 };
69 };
@@ -80,6 +81,7 @@ fn main() -> Result<()> {
80 let vm_args = vm::VmArg {
81 let vm_args = vm::VmArg {
81 cpu_count: config.box_cfg.cpu_count,
82 cpu_count: config.box_cfg.cpu_count,
82 ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
83 ram_bytes: config.box_cfg.ram_mb.saturating_mul(1024 * 1024),
84+ disk_bytes: config.box_cfg.disk_gb.saturating_mul(1024 * 1024 * 1024),
83 no_default_mounts: false,
85 no_default_mounts: false,
84 mounts: config.box_cfg.mounts.clone(),
86 mounts: config.box_cfg.mounts.clone(),
85 };
87 };
@@ -88,6 +90,7 @@ fn main() -> Result<()> {
88 let vm_info = VmInfo {
90 let vm_info = VmInfo {
89 max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
91 max_memory_mb: vm_args.ram_bytes / (1024 * 1024),
90 cpu_cores: vm_args.cpu_count,
92 cpu_cores: vm_args.cpu_count,
93+ max_disk_gb: (vm_args.disk_bytes as f32) / 1024.0 / 1024.0 / 1024.0,
91 system_name: "Debian".to_string(), // TODO: read system name from the VM.
94 system_name: "Debian".to_string(), // TODO: read system name from the VM.
92 auto_shutdown_ms,
95 auto_shutdown_ms,
93 };
96 };
src/config.rs+12−1
@@ -13,6 +13,7 @@ pub const CONFIG_PATH_ENV: &str = "VIBEBOX_CONFIG_PATH";
13const DEFAULT_CPU_COUNT: usize = 2;
13const DEFAULT_CPU_COUNT: usize = 2;
14const DEFAULT_RAM_MB: u64 = 2048;
14const DEFAULT_RAM_MB: u64 = 2048;
15const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000;
15const DEFAULT_AUTO_SHUTDOWN_MS: u64 = 20000;
16+const DEFAULT_DISK_GB: u64 = 5;
16
17
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct Config {
19pub struct Config {
@@ -25,6 +26,7 @@ pub struct Config {
25pub struct BoxConfig {
26pub struct BoxConfig {
26 pub cpu_count: usize,
27 pub cpu_count: usize,
27 pub ram_mb: u64,
28 pub ram_mb: u64,
29+ pub disk_gb: u64,
28 pub mounts: Vec<String>,
30 pub mounts: Vec<String>,
29}
31}
30
32
@@ -33,6 +35,7 @@ impl Default for BoxConfig {
33 Self {
35 Self {
34 cpu_count: default_cpu_count(),
36 cpu_count: default_cpu_count(),
35 ram_mb: default_ram_mb(),
37 ram_mb: default_ram_mb(),
38+ disk_gb: default_disk_gb(),
36 mounts: default_mounts(),
39 mounts: default_mounts(),
37 }
40 }
38 }
41 }
@@ -67,6 +70,10 @@ fn default_mounts() -> Vec<String> {
67 Vec::new()
70 Vec::new()
68}
71}
69
72
73+fn default_disk_gb() -> u64 {
74+ DEFAULT_DISK_GB
75+}
76+
70pub fn config_path(project_root: &Path) -> PathBuf {
77pub fn config_path(project_root: &Path) -> PathBuf {
71 project_root.join(CONFIG_FILENAME)
78 project_root.join(CONFIG_FILENAME)
72}
79}
@@ -102,7 +109,7 @@ pub fn load_config_with_path(project_root: &Path, override_path: Option<&Path>)
102 tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
109 tracing::debug!(path = %path.display(), bytes = raw.len(), "loaded vibebox config");
103 if trimmed.is_empty() {
110 if trimmed.is_empty() {
104 die(&format!(
111 die(&format!(
105− "config file ({}) is empty. Required fields: [box].cpu_count (integer), [box].ram_mb (integer), [box].mounts (array of strings), [supervisor].auto_shutdown_ms (integer)",
112+ "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)",
106 path.display()
113 path.display()
107 ));
114 ));
108 }
115 }
@@ -193,6 +200,7 @@ fn validate_schema(value: &toml::Value) -> Vec<String> {
193 Some(table) => {
200 Some(table) => {
194 validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors);
201 validate_int(table, "cpu_count", "[box].cpu_count (integer)", &mut errors);
195 validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors);
202 validate_int(table, "ram_mb", "[box].ram_mb (integer)", &mut errors);
203+ validate_int(table, "disk_gb", "[box].disk_gb (integer)", &mut errors);
196 validate_string_array(
204 validate_string_array(
197 table,
205 table,
198 "mounts",
206 "mounts",
@@ -259,6 +267,9 @@ fn validate_or_exit(config: &Config) {
259 if config.box_cfg.ram_mb == 0 {
267 if config.box_cfg.ram_mb == 0 {
260 die("box.ram_mb must be >= 1");
268 die("box.ram_mb must be >= 1");
261 }
269 }
270+ if config.box_cfg.disk_gb == 0 {
271+ die("box.disk_gb must be >= 1");
272+ }
262 if config.supervisor.auto_shutdown_ms == 0 {
273 if config.supervisor.auto_shutdown_ms == 0 {
263 die("supervisor.auto_shutdown_ms must be >= 1");
274 die("supervisor.auto_shutdown_ms must be >= 1");
264 }
275 }
src/provision.sh+1
@@ -14,6 +14,7 @@ apt-get install -y --no-install-recommends \
14 curl \
14 curl \
15 git \
15 git \
16 ripgrep \
16 ripgrep \
17+ cloud-guest-utils \
17 openssh-server \
18 openssh-server \
18 sudo
19 sudo
19
20
src/resize_disk.shnew file+55
@@ -0,0 +1,55 @@
1+#!/bin/bash
2+set -euo pipefail
3+
4+ROOT_DEV="$(findmnt / -n -o SOURCE || true)"
5+ROOT_FSTYPE="$(findmnt / -n -o FSTYPE || true)"
6+
7+if [ -z "$ROOT_DEV" ]; then
8+ exit 0
9+fi
10+
11+DISK_DEV=""
12+PART_NUM=""
13+
14+if command -v lsblk >/dev/null 2>&1; then
15+ DISK_DEV="$(lsblk -no pkname "$ROOT_DEV" 2>/dev/null | head -n1 || true)"
16+ PART_NUM="$(lsblk -no PARTNUM "$ROOT_DEV" 2>/dev/null | head -n1 || true)"
17+fi
18+
19+if [ -z "$DISK_DEV" ] || [ -z "$PART_NUM" ]; then
20+ ROOT_BASENAME="$(basename "$ROOT_DEV")"
21+ if echo "$ROOT_BASENAME" | grep -Eq '^nvme.+p[0-9]+$'; then
22+ DISK_DEV="/dev/${ROOT_BASENAME%p[0-9]*}"
23+ PART_NUM="${ROOT_BASENAME##*p}"
24+ elif echo "$ROOT_BASENAME" | grep -Eq '^[a-z]+[0-9]+$'; then
25+ DISK_DEV="/dev/${ROOT_BASENAME%%[0-9]*}"
26+ PART_NUM="${ROOT_BASENAME##*[a-z]}"
27+ fi
28+fi
29+
30+if [ -n "$DISK_DEV" ] && [ -n "$PART_NUM" ]; then
31+ if command -v growpart >/dev/null 2>&1; then
32+ growpart "$DISK_DEV" "$PART_NUM" || true
33+ elif command -v sfdisk >/dev/null 2>&1; then
34+ sfdisk -N "$PART_NUM" --force "$DISK_DEV" <<'EOF' || true
35+,,
36+EOF
37+ fi
38+fi
39+
40+if command -v partprobe >/dev/null 2>&1; then
41+ partprobe "$DISK_DEV" || true
42+fi
43+
44+case "$ROOT_FSTYPE" in
45+ ext4|ext3|ext2)
46+ if command -v resize2fs >/dev/null 2>&1; then
47+ resize2fs "$ROOT_DEV" || true
48+ fi
49+ ;;
50+ xfs)
51+ if command -v xfs_growfs >/dev/null 2>&1; then
52+ xfs_growfs / || true
53+ fi
54+ ;;
55+esac
src/tui.rs+4−3
@@ -41,6 +41,7 @@ const INFO_LINE_COUNT: u16 = 5;
41pub struct VmInfo {
41pub struct VmInfo {
42 pub max_memory_mb: u64,
42 pub max_memory_mb: u64,
43 pub cpu_cores: usize,
43 pub cpu_cores: usize,
44+ pub max_disk_gb: f32,
44 pub system_name: String,
45 pub system_name: String,
45 pub auto_shutdown_ms: u64,
46 pub auto_shutdown_ms: u64,
46}
47}
@@ -570,11 +571,11 @@ fn render_header(buffer: &mut Buffer, area: Rect, app: &AppState) {
570 Span::styled(&app.vm_info.system_name, Style::default().fg(Color::Green)),
571 Span::styled(&app.vm_info.system_name, Style::default().fg(Color::Green)),
571 ]),
572 ]),
572 Line::from(vec![
573 Line::from(vec![
573− Span::raw("CPU / Memory: "),
574+ Span::raw("CPU / Memory / Disk: "),
574 Span::styled(
575 Span::styled(
575 format!(
576 format!(
576− "{} cores / {} MB",
577+ "{} cores / {} MB / {} GB",
577− app.vm_info.cpu_cores, app.vm_info.max_memory_mb
578+ app.vm_info.cpu_cores, app.vm_info.max_memory_mb, app.vm_info.max_disk_gb
578 ),
579 ),
579 Style::default().fg(Color::Green),
580 Style::default().fg(Color::Green),
580 ),
581 ),
src/vm.rs+52−5
@@ -74,6 +74,7 @@ impl Drop for StatusFile {
74}
74}
75const PROVISION_SCRIPT: &str = include_str!("provision.sh");
75const PROVISION_SCRIPT: &str = include_str!("provision.sh");
76const PROVISION_SCRIPT_NAME: &str = "provision.sh";
76const PROVISION_SCRIPT_NAME: &str = "provision.sh";
77+const RESIZE_DISK_SCRIPT: &str = include_str!("resize_disk.sh");
77const DEFAULT_RAW_NAME: &str = "default.raw";
78const DEFAULT_RAW_NAME: &str = "default.raw";
78const INSTANCE_RAW_NAME: &str = "instance.raw";
79const INSTANCE_RAW_NAME: &str = "instance.raw";
79const BASE_DISK_RAW_NAME: &str = "disk.raw";
80const BASE_DISK_RAW_NAME: &str = "disk.raw";
@@ -166,6 +167,7 @@ fn expand_tilde_path(value: &str) -> PathBuf {
166pub struct VmArg {
167pub struct VmArg {
167 pub cpu_count: usize,
168 pub cpu_count: usize,
168 pub ram_bytes: u64,
169 pub ram_bytes: u64,
170+ pub disk_bytes: u64,
169 pub no_default_mounts: bool,
171 pub no_default_mounts: bool,
170 pub mounts: Vec<String>,
172 pub mounts: Vec<String>,
171}
173}
@@ -230,7 +232,15 @@ where
230 std::slice::from_ref(&mise_directory_share),
232 std::slice::from_ref(&mise_directory_share),
231 Some(&status_file),
233 Some(&status_file),
232 )?;
234 )?;
233− ensure_instance_disk(&instance_raw, &default_raw, Some(&status_file))?;
235+ let _ = ensure_instance_disk(
236+ &instance_raw,
237+ &default_raw,
238+ args.disk_bytes,
239+ Some(&status_file),
240+ )?;
241+ let base_size = fs::metadata(&default_raw)?.len();
242+ let instance_size = fs::metadata(&instance_raw)?.len();
243+ let needs_resize = instance_size > base_size;
234 let disk_path = instance_raw;
244 let disk_path = instance_raw;
235
245
236 let mut login_actions = Vec::new();
246 let mut login_actions = Vec::new();
@@ -263,6 +273,11 @@ where
263 directory_shares.push(DirectoryShare::from_mount_spec(spec)?);
273 directory_shares.push(DirectoryShare::from_mount_spec(spec)?);
264 }
274 }
265
275
276+ if needs_resize {
277+ let resize_cmd = script_command_from_content("resize_disk", RESIZE_DISK_SCRIPT)?;
278+ login_actions.push(Send(resize_cmd));
279+ }
280+
266 if let Some(motd_action) = motd_login_action(&directory_shares) {
281 if let Some(motd_action) = motd_login_action(&directory_shares) {
267 login_actions.push(motd_action);
282 login_actions.push(motd_action);
268 }
283 }
@@ -559,19 +574,51 @@ fn ensure_default_image(
559fn ensure_instance_disk(
574fn ensure_instance_disk(
560 instance_raw: &Path,
575 instance_raw: &Path,
561 template_raw: &Path,
576 template_raw: &Path,
577+ target_bytes: u64,
562 status: Option<&StatusFile>,
578 status: Option<&StatusFile>,
563−) -> Result<(), Box<dyn std::error::Error>> {
579+) -> Result<bool, Box<dyn std::error::Error>> {
564 if instance_raw.exists() {
580 if instance_raw.exists() {
581+ let current_size = fs::metadata(instance_raw)?.len();
582+ if current_size != target_bytes {
583+ let current_gb = current_size as f64 / (1024.0 * 1024.0 * 1024.0);
584+ let target_gb = target_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
585+ tracing::warn!(
586+ current_bytes = current_size,
587+ target_bytes,
588+ "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.",
589+ current_gb,
590+ target_gb
591+ );
592+ }
565− return Ok(());
593+ return Ok(false);
594+ }
595+
596+ let template_size = fs::metadata(template_raw)?.len();
597+ if target_bytes < template_size {
598+ return Err(format!(
599+ "Requested disk size {} bytes is smaller than base image size {} bytes",
600+ target_bytes, template_size
601+ )
602+ .into());
566 }
603 }
604+ let target_size = target_bytes;
605+ let needs_resize = target_size > template_size;
567
606
568 if let Some(status) = status {
607 if let Some(status) = status {
569 status.update("creating instance disk...");
608 status.update("creating instance disk...");
570 }
609 }
571 tracing::info!(path = %template_raw.display(), "creating instance disk");
610 tracing::info!(path = %template_raw.display(), "creating instance disk");
572 std::fs::create_dir_all(instance_raw.parent().unwrap())?;
611 std::fs::create_dir_all(instance_raw.parent().unwrap())?;
612+ if target_size == template_size {
573− fs::copy(template_raw, instance_raw)?;
613+ fs::copy(template_raw, instance_raw)?;
574− Ok(())
614+ return Ok(needs_resize);
615+ }
616+
617+ let mut dst = std::fs::File::create(instance_raw)?;
618+ dst.set_len(target_size)?;
619+ let mut src = std::fs::File::open(template_raw)?;
620+ std::io::copy(&mut src, &mut dst)?;
621+ Ok(needs_resize)
575}
622}
576
623
577pub struct IoContext {
624pub struct IoContext {
vibebox.toml+1
@@ -1,6 +1,7 @@
1[box]
1[box]
2cpu_count = 2
2cpu_count = 2
3ram_mb = 2048
3ram_mb = 2048
4+disk_gb = 5
4mounts = [
5mounts = [
5 "~/.codex:~/.codex:read-write",
6 "~/.codex:~/.codex:read-write",
6 "~/.claude:~/.claude:read-write",
7 "~/.claude:~/.claude:read-write",