1use serde_json::Value;
19
20use crate::args::{build_create_args, strvec, CreateOpts, Kind};
21use crate::error::{Error, Result};
22use crate::process::{checked, run_full, run_full_stream, spawn};
23use crate::types::{ExecResult, SandboxInfo};
24
25fn looks_like_machine_id(line: &str) -> bool {
28 line.len() >= 6
29 && line
30 .chars()
31 .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))
32}
33
34fn parse_ssh_port(stderr: &str) -> Option<u16> {
36 let idx = stderr.find("ssh -p ")?;
37 let digits: String = stderr[idx + "ssh -p ".len()..]
38 .chars()
39 .take_while(|c| c.is_ascii_digit())
40 .collect();
41 digits.parse().ok()
42}
43
44#[derive(Debug, Clone)]
50pub struct Sandbox {
51 id: String,
52 ssh_port: Option<u16>,
53}
54
55macro_rules! common_setters {
63 () => {
64 pub fn name(mut self, name: impl Into<String>) -> Self {
66 self.opts.name = Some(name.into());
67 self
68 }
69
70 pub fn cpus(mut self, cpus: u32) -> Self {
72 self.opts.cpus = Some(cpus);
73 self
74 }
75
76 pub fn mem(mut self, mib: u32) -> Self {
78 self.opts.mem = Some(mib);
79 self
80 }
81
82 pub fn port(mut self, forward: impl Into<String>) -> Self {
84 self.opts.net.touched = true;
85 self.opts.net.ports.push(forward.into());
86 self
87 }
88
89 pub fn forward(self, host: u16, guest: u16) -> Self {
91 self.port(format!("{host}:{guest}"))
92 }
93
94 pub fn mac(mut self, mac: impl Into<String>) -> Self {
96 self.opts.net.touched = true;
97 self.opts.net.mac = Some(mac.into());
98 self
99 }
100
101 pub fn network(mut self, network: impl Into<String>) -> Self {
103 self.opts.net.touched = true;
104 self.opts.net.network = Some(network.into());
105 self
106 }
107
108 pub fn no_net(mut self) -> Self {
110 self.opts.net.touched = true;
111 self.opts.net.no_net = true;
112 self
113 }
114
115 pub fn log_level(mut self, level: u32) -> Self {
118 self.opts.log_level = Some(level);
119 self
120 }
121
122 pub fn to_args(&self) -> Vec<String> {
125 build_create_args(self.kind, &self.opts)
126 }
127
128 pub fn create(self) -> Result<Sandbox> {
130 Sandbox::create_with(self.kind, self.opts)
131 }
132 };
133}
134
135macro_rules! disk_setters {
136 () => {
137 pub fn persist(mut self) -> Self {
139 self.opts.persist = true;
140 self
141 }
142
143 pub fn volume(mut self, name: impl Into<String>) -> Self {
145 self.opts.volume = Some(name.into());
146 self
147 }
148
149 pub fn attach_disk(mut self, disk: impl Into<String>) -> Self {
151 self.opts.attach_disk.push(disk.into());
152 self
153 }
154 };
155}
156
157macro_rules! define_builder {
158 ($(#[$doc:meta])* $name:ident) => {
159 $(#[$doc])*
160 #[derive(Debug, Clone)]
161 pub struct $name {
162 kind: Kind,
163 opts: CreateOpts,
164 }
165 };
166}
167
168define_builder!(
169 LinuxBuilder
171);
172define_builder!(
173 FreebsdBuilder
175);
176define_builder!(
177 NetbsdBuilder
179);
180define_builder!(
181 FirmwareBuilder
183);
184define_builder!(
185 KernelBuilder
187);
188define_builder!(
189 NanosBuilder
191);
192define_builder!(
193 OsvBuilder
195);
196define_builder!(
197 UnikraftBuilder
199);
200define_builder!(
201 Solo5Builder
204);
205
206impl LinuxBuilder {
207 common_setters!();
208
209 pub fn kernel(mut self, kernel: impl Into<String>) -> Self {
211 self.opts.kernel = Some(kernel.into());
212 self
213 }
214
215 pub fn kernel_version(mut self, version: impl Into<String>) -> Self {
217 self.opts.kernel_version = Some(version.into());
218 self
219 }
220
221 pub fn initramfs(mut self) -> Self {
223 self.opts.initramfs_flag = true;
224 self
225 }
226
227 pub fn volume(mut self, name: impl Into<String>) -> Self {
229 self.opts.volume = Some(name.into());
230 self
231 }
232
233 pub fn mount(mut self, mount: impl Into<String>) -> Self {
236 self.opts.mounts.push(mount.into());
237 self
238 }
239
240 pub fn attach_disk(mut self, disk: impl Into<String>) -> Self {
245 self.opts.attach_disk.push(disk.into());
246 self
247 }
248
249 pub fn entrypoint(mut self, entrypoint: impl Into<String>) -> Self {
251 self.opts.entrypoint = Some(entrypoint.into());
252 self
253 }
254
255 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
259 self.opts.env.push((key.into(), value.into()));
260 self
261 }
262
263 pub fn envs<K, V>(mut self, vars: impl IntoIterator<Item = (K, V)>) -> Self
265 where
266 K: Into<String>,
267 V: Into<String>,
268 {
269 self.opts
270 .env
271 .extend(vars.into_iter().map(|(k, v)| (k.into(), v.into())));
272 self
273 }
274
275 pub fn console(mut self, console: impl Into<String>) -> Self {
277 self.opts.console = Some(console.into());
278 self
279 }
280
281 pub fn command<I, S>(mut self, command: I) -> Self
283 where
284 I: IntoIterator<Item = S>,
285 S: Into<String>,
286 {
287 self.opts.command = strvec(command);
288 self
289 }
290}
291
292impl FreebsdBuilder {
293 common_setters!();
294 disk_setters!();
295
296 pub fn version(mut self, version: impl Into<String>) -> Self {
298 self.opts.version = Some(version.into());
299 self
300 }
301
302 pub fn firmware(mut self, firmware: impl Into<String>) -> Self {
304 self.opts.firmware = Some(firmware.into());
305 self
306 }
307
308 pub fn force(mut self) -> Self {
310 self.opts.force = true;
311 self
312 }
313}
314
315impl NetbsdBuilder {
316 common_setters!();
317 disk_setters!();
318
319 pub fn version(mut self, version: impl Into<String>) -> Self {
321 self.opts.version = Some(version.into());
322 self
323 }
324
325 pub fn force(mut self) -> Self {
327 self.opts.force = true;
328 self
329 }
330}
331
332impl FirmwareBuilder {
333 common_setters!();
334 disk_setters!();
335}
336
337impl KernelBuilder {
338 common_setters!();
339 disk_setters!();
340
341 pub fn format(mut self, format: impl Into<String>) -> Self {
343 self.opts.format = Some(format.into());
344 self
345 }
346
347 pub fn initramfs(mut self, path: impl Into<String>) -> Self {
350 self.opts.initramfs_path = Some(path.into());
351 self
352 }
353
354 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
356 self.opts.cmdline = Some(cmdline.into());
357 self
358 }
359
360 pub fn disk(mut self, disk: impl Into<String>) -> Self {
362 self.opts.disk = Some(disk.into());
363 self
364 }
365}
366
367impl NanosBuilder {
368 common_setters!();
369
370 pub fn kernel(mut self, kernel: impl Into<String>) -> Self {
372 self.opts.kernel = Some(kernel.into());
373 self
374 }
375
376 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
378 self.opts.cmdline = Some(cmdline.into());
379 self
380 }
381
382 pub fn persist(mut self) -> Self {
384 self.opts.persist = true;
385 self
386 }
387}
388
389impl OsvBuilder {
390 common_setters!();
391
392 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
394 self.opts.cmdline = Some(cmdline.into());
395 self
396 }
397
398 pub fn disk(mut self, disk: impl Into<String>) -> Self {
400 self.opts.disk = Some(disk.into());
401 self
402 }
403
404 pub fn gic(mut self, gic: impl Into<String>) -> Self {
406 self.opts.gic = Some(gic.into());
407 self
408 }
409
410 pub fn persist(mut self) -> Self {
412 self.opts.persist = true;
413 self
414 }
415}
416
417impl UnikraftBuilder {
418 common_setters!();
419
420 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
423 self.opts.cmdline = Some(cmdline.into());
424 self
425 }
426
427 pub fn initramfs(mut self, path: impl Into<String>) -> Self {
429 self.opts.initramfs_path = Some(path.into());
430 self
431 }
432
433 pub fn mount(mut self, mount: impl Into<String>) -> Self {
436 self.opts.mounts.push(mount.into());
437 self
438 }
439}
440
441impl Solo5Builder {
442 common_setters!();
443
444 pub fn block(mut self, block: impl Into<String>) -> Self {
448 self.opts.block.push(block.into());
449 self
450 }
451
452 pub fn args<I, S>(mut self, args: I) -> Self
455 where
456 I: IntoIterator<Item = S>,
457 S: Into<String>,
458 {
459 self.opts.trailing_args = strvec(args);
460 self
461 }
462}
463
464impl Sandbox {
467 pub fn fs(&self) -> crate::FileSystem {
469 crate::filesystem::FileSystem::new(&self.id)
470 }
471
472 pub fn cache(&self) -> crate::Cache {
474 crate::cache::Cache::new(&self.id)
475 }
476
477 pub fn linux(image: impl Into<String>) -> LinuxBuilder {
479 LinuxBuilder {
480 kind: Kind::Linux,
481 opts: CreateOpts {
482 image: Some(image.into()),
483 ..Default::default()
484 },
485 }
486 }
487
488 pub fn freebsd() -> FreebsdBuilder {
490 FreebsdBuilder {
491 kind: Kind::Freebsd,
492 opts: CreateOpts::default(),
493 }
494 }
495
496 pub fn netbsd() -> NetbsdBuilder {
498 NetbsdBuilder {
499 kind: Kind::Netbsd,
500 opts: CreateOpts::default(),
501 }
502 }
503
504 pub fn firmware(firmware: impl Into<String>, disk: impl Into<String>) -> FirmwareBuilder {
506 FirmwareBuilder {
507 kind: Kind::Firmware,
508 opts: CreateOpts {
509 firmware: Some(firmware.into()),
510 disk: Some(disk.into()),
511 ..Default::default()
512 },
513 }
514 }
515
516 pub fn kernel(kernel: impl Into<String>) -> KernelBuilder {
518 KernelBuilder {
519 kind: Kind::Kernel,
520 opts: CreateOpts {
521 kernel: Some(kernel.into()),
522 ..Default::default()
523 },
524 }
525 }
526
527 pub fn nanos(image: impl Into<String>) -> NanosBuilder {
529 NanosBuilder {
530 kind: Kind::Nanos,
531 opts: CreateOpts {
532 image: Some(image.into()),
533 ..Default::default()
534 },
535 }
536 }
537
538 pub fn osv(image: impl Into<String>) -> OsvBuilder {
541 OsvBuilder {
542 kind: Kind::Osv,
543 opts: CreateOpts {
544 image: Some(image.into()),
545 ..Default::default()
546 },
547 }
548 }
549
550 pub fn unikraft(path: impl Into<String>) -> UnikraftBuilder {
552 UnikraftBuilder {
553 kind: Kind::Unikraft,
554 opts: CreateOpts {
555 path: Some(path.into()),
556 ..Default::default()
557 },
558 }
559 }
560
561 pub fn solo5(path: impl Into<String>) -> Solo5Builder {
564 Solo5Builder {
565 kind: Kind::Solo5,
566 opts: CreateOpts {
567 path: Some(path.into()),
568 ..Default::default()
569 },
570 }
571 }
572
573 fn create_with(kind: Kind, opts: CreateOpts) -> Result<Sandbox> {
574 let log_level = opts.log_level.unwrap_or(1);
577 let args = build_create_args(kind, &opts);
578 let res = run_full(&args, &[], None, log_level)?;
579 if res.exit_code != 0 {
580 return Err(Error::CommandFailed {
581 exit_code: res.exit_code,
582 stdout: res.stdout,
583 stderr: res.stderr,
584 command: "bsdkrun create".to_string(),
585 });
586 }
587
588 let machine_id = res
591 .stdout
592 .lines()
593 .map(str::trim)
594 .rfind(|line| looks_like_machine_id(line))
595 .map(str::to_string);
596 let Some(id) = machine_id else {
597 return Err(Error::CommandFailed {
598 exit_code: res.exit_code,
599 stdout: res.stdout,
600 stderr: res.stderr,
601 command: "bsdkrun create (no machine id in output)".to_string(),
602 });
603 };
604 let ssh_port = parse_ssh_port(&res.stderr);
605 Ok(Sandbox { id, ssh_port })
606 }
607
608 pub fn from_id(id: impl Into<String>) -> Sandbox {
610 Sandbox {
611 id: id.into(),
612 ssh_port: None,
613 }
614 }
615
616 pub fn get(id: &str) -> Result<Sandbox> {
618 for info in Self::list(true)? {
619 if info.id == id || info.id.starts_with(id) || info.name == Some(id.to_string()) {
620 return Ok(Sandbox {
621 id: info.id,
622 ssh_port: None,
623 });
624 }
625 }
626 Err(Error::SandboxNotFound { id: id.to_string() })
627 }
628
629 pub fn list(all: bool) -> Result<Vec<SandboxInfo>> {
631 let mut args = vec!["ps".to_string(), "--json".to_string()];
632 if all {
633 args.push("--all".to_string());
634 }
635 let res = checked(&args, "bsdkrun ps")?;
636 let raw = if res.stdout.trim().is_empty() {
637 "[]".to_string()
638 } else {
639 res.stdout
640 };
641 let rows: Value = serde_json::from_str(&raw)?;
642 Ok(rows
643 .as_array()
644 .map(|rows| rows.iter().map(SandboxInfo::from_row).collect())
645 .unwrap_or_default())
646 }
647
648 pub fn id(&self) -> &str {
650 &self.id
651 }
652
653 pub fn ssh_port(&self) -> Option<u16> {
655 self.ssh_port
656 }
657
658 pub fn command(&self, program: impl Into<String>) -> CommandBuilder {
673 CommandBuilder {
674 sandbox_id: self.id.clone(),
675 argv: vec![program.into()],
676 env: Vec::new(),
677 cwd: None,
678 stdin: None,
679 tty: false,
680 log_level: 0,
681 stdout: None,
682 stderr: None,
683 }
684 }
685
686 pub fn exec<I, S>(&self, argv: I) -> Result<ExecResult>
692 where
693 I: IntoIterator<Item = S>,
694 S: Into<String>,
695 {
696 let mut argv = strvec(argv).into_iter();
697 let Some(program) = argv.next() else {
698 return Err(Error::InvalidInput("exec needs a non-empty argv".into()));
699 };
700 let mut builder = self.command(program);
701 builder.argv.extend(argv);
702 builder.run()
703 }
704
705 pub fn logs(&self) -> Result<String> {
707 Ok(run_full(&strvec(["logs", self.id.as_str()]), &[], None, 0)?.stdout)
708 }
709
710 pub fn boot_logs(&self) -> Result<String> {
712 Ok(run_full(&strvec(["logs", "--boot", self.id.as_str()]), &[], None, 0)?.stdout)
713 }
714
715 pub fn shell(&self) -> Result<i32> {
719 spawn(["shell", self.id.as_str()])
720 }
721
722 pub fn status(&self) -> Result<Option<SandboxInfo>> {
726 Ok(Self::list(true)?
727 .into_iter()
728 .find(|info| info.id == self.id))
729 }
730
731 pub fn is_running(&self) -> Result<bool> {
733 Ok(self.status()?.map(|info| info.running).unwrap_or(false))
734 }
735
736 pub fn stop(&self) -> Result<()> {
740 checked(&strvec(["stop", self.id.as_str()]), "bsdkrun stop")?;
741 Ok(())
742 }
743
744 pub fn start(&self) -> Result<()> {
746 checked(&strvec(["start", self.id.as_str()]), "bsdkrun start")?;
747 Ok(())
748 }
749
750 pub fn remove(&self, force: bool) -> Result<()> {
752 let mut args = vec!["rm".to_string()];
753 if force {
754 args.push("--force".to_string());
755 }
756 args.push(self.id.clone());
757 checked(&args, "bsdkrun rm")?;
758 Ok(())
759 }
760
761 pub fn update(&self) -> UpdateBuilder {
769 UpdateBuilder {
770 id: self.id.clone(),
771 cpus: None,
772 mem: None,
773 }
774 }
775
776 pub fn connect_network(&self, network: &str) -> Result<()> {
778 checked(
779 &strvec(["network", "connect", self.id.as_str(), network]),
780 "bsdkrun network connect",
781 )?;
782 Ok(())
783 }
784
785 pub fn disconnect_network(&self) -> Result<()> {
787 checked(
788 &strvec(["network", "disconnect", self.id.as_str()]),
789 "bsdkrun network disconnect",
790 )?;
791 Ok(())
792 }
793
794 pub fn ssh_setup(&self) -> SshSetupBuilder {
806 SshSetupBuilder {
807 id: self.id.clone(),
808 user: None,
809 keys: Vec::new(),
810 }
811 }
812
813 pub fn tailscale_up(&self) -> TailscaleUpBuilder {
821 TailscaleUpBuilder {
822 id: self.id.clone(),
823 authkey: None,
824 hostname: None,
825 extra: Vec::new(),
826 }
827 }
828}
829
830pub struct CommandBuilder {
832 sandbox_id: String,
833 argv: Vec<String>,
834 env: Vec<(String, String)>,
835 cwd: Option<String>,
836 stdin: Option<Vec<u8>>,
837 tty: bool,
838 log_level: u32,
839 stdout: Option<Box<dyn std::io::Write + Send>>,
840 stderr: Option<Box<dyn std::io::Write + Send>>,
841}
842
843impl CommandBuilder {
844 pub fn arg(mut self, arg: impl Into<String>) -> Self {
846 self.argv.push(arg.into());
847 self
848 }
849
850 pub fn args<I, S>(mut self, args: I) -> Self
852 where
853 I: IntoIterator<Item = S>,
854 S: Into<String>,
855 {
856 self.argv.extend(strvec(args));
857 self
858 }
859
860 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
862 self.env.push((key.into(), value.into()));
863 self
864 }
865
866 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
868 self.cwd = Some(cwd.into());
869 self
870 }
871
872 pub fn stdin(mut self, data: impl AsRef<[u8]>) -> Self {
874 self.stdin = Some(data.as_ref().to_vec());
875 self
876 }
877
878 pub fn tty(mut self, tty: bool) -> Self {
880 self.tty = tty;
881 self
882 }
883
884 pub fn log_level(mut self, level: u32) -> Self {
886 self.log_level = level;
887 self
888 }
889
890 pub fn stdout(mut self, writer: impl std::io::Write + Send + 'static) -> Self {
892 self.stdout = Some(Box::new(writer));
893 self
894 }
895
896 pub fn stderr(mut self, writer: impl std::io::Write + Send + 'static) -> Self {
898 self.stderr = Some(Box::new(writer));
899 self
900 }
901
902 pub fn run(self) -> Result<ExecResult> {
907 let mut argv = self.argv;
908 if let Some(cwd) = &self.cwd {
909 let mut wrapped = strvec([
911 "/bin/sh",
912 "-c",
913 "cd \"$1\" && shift && exec \"$@\"",
914 "sh",
915 cwd.as_str(),
916 ]);
917 wrapped.append(&mut argv);
918 argv = wrapped;
919 }
920
921 let mut cli = vec!["exec".to_string()];
922 if self.tty {
923 cli.push("-t".to_string());
924 }
925 for (key, value) in &self.env {
926 cli.push("-e".to_string());
927 cli.push(format!("{key}={value}"));
928 }
929 cli.push(self.sandbox_id.clone());
930 cli.extend(argv.iter().cloned());
931
932 let res = run_full_stream(
933 &cli,
934 &[],
935 self.stdin.as_deref(),
936 self.log_level,
937 self.stdout,
938 self.stderr,
939 )?;
940 Ok(ExecResult {
941 stdout: res.stdout,
942 stderr: res.stderr,
943 exit_code: res.exit_code,
944 command: format!("exec {}", argv.join(" ")),
945 })
946 }
947}
948
949#[derive(Debug, Clone)]
951pub struct UpdateBuilder {
952 id: String,
953 cpus: Option<u32>,
954 mem: Option<u32>,
955}
956
957impl UpdateBuilder {
958 pub fn cpus(mut self, cpus: u32) -> Self {
959 self.cpus = Some(cpus);
960 self
961 }
962
963 pub fn mem(mut self, mib: u32) -> Self {
964 self.mem = Some(mib);
965 self
966 }
967
968 pub fn apply(self) -> Result<()> {
970 let mut args = strvec(["update", self.id.as_str()]);
971 if let Some(cpus) = self.cpus {
972 args.push("--cpus".to_string());
973 args.push(cpus.to_string());
974 }
975 if let Some(mem) = self.mem {
976 args.push("--mem".to_string());
977 args.push(mem.to_string());
978 }
979 checked(&args, "bsdkrun update")?;
980 Ok(())
981 }
982}
983
984#[derive(Debug, Clone)]
986pub struct SshSetupBuilder {
987 id: String,
988 user: Option<String>,
989 keys: Vec<String>,
990}
991
992impl SshSetupBuilder {
993 pub fn user(mut self, user: impl Into<String>) -> Self {
995 self.user = Some(user.into());
996 self
997 }
998
999 pub fn key(mut self, key: impl Into<String>) -> Self {
1001 self.keys.push(key.into());
1002 self
1003 }
1004
1005 pub fn run(self) -> Result<ExecResult> {
1008 let mut action = vec!["setup".to_string()];
1009 if let Some(user) = &self.user {
1010 action.push("--user".to_string());
1011 action.push(user.clone());
1012 }
1013 for key in &self.keys {
1014 action.push("--key".to_string());
1015 action.push(key.clone());
1016 }
1017 run_agent("ssh", &self.id, &action, &[])
1018 }
1019}
1020
1021#[derive(Debug, Clone)]
1023pub struct TailscaleUpBuilder {
1024 id: String,
1025 authkey: Option<String>,
1026 hostname: Option<String>,
1027 extra: Vec<String>,
1028}
1029
1030impl TailscaleUpBuilder {
1031 pub fn authkey(mut self, authkey: impl Into<String>) -> Self {
1034 self.authkey = Some(authkey.into());
1035 self
1036 }
1037
1038 pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
1040 self.hostname = Some(hostname.into());
1041 self
1042 }
1043
1044 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1046 self.extra.push(arg.into());
1047 self
1048 }
1049
1050 pub fn run(self) -> Result<ExecResult> {
1052 let mut action = vec!["setup".to_string()];
1053 if let Some(hostname) = &self.hostname {
1054 action.push("--hostname".to_string());
1055 action.push(hostname.clone());
1056 }
1057 action.extend(self.extra.iter().cloned());
1058 let env: Vec<(String, String)> = self
1059 .authkey
1060 .map(|key| vec![("TS_AUTHKEY".to_string(), key)])
1061 .unwrap_or_default();
1062 run_agent("tailscale", &self.id, &action, &env)
1063 }
1064}
1065
1066fn run_agent(
1067 family: &str,
1068 id: &str,
1069 action: &[String],
1070 env: &[(String, String)],
1071) -> Result<ExecResult> {
1072 let mut args = vec![family.to_string(), id.to_string()];
1073 args.extend(action.iter().cloned());
1074 let res = run_full(&args, env, None, 0)?;
1075 ExecResult {
1076 stdout: res.stdout,
1077 stderr: res.stderr,
1078 exit_code: res.exit_code,
1079 command: format!("{family} {}", action.join(" ")),
1080 }
1081 .ok_or_err()
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086 use super::*;
1087
1088 fn s(items: &[&str]) -> Vec<String> {
1089 items.iter().map(|s| s.to_string()).collect()
1090 }
1091
1092 #[test]
1095 fn linux_env_is_emitted_sorted_by_key() {
1096 assert_eq!(
1097 Sandbox::linux("alpine")
1098 .env("ZED", "3")
1099 .env("ALPHA", "1")
1100 .envs([("MID", "2")])
1101 .to_args(),
1102 s(&["linux", "alpine", "-d", "-e", "ALPHA=1", "-e", "MID=2", "-e", "ZED=3"])
1103 );
1104 }
1105
1106 #[test]
1107 fn linux_without_env_emits_nothing() {
1108 assert_eq!(
1109 Sandbox::linux("alpine").to_args(),
1110 s(&["linux", "alpine", "-d"])
1111 );
1112 }
1113
1114 #[test]
1115 fn linux_minimal() {
1116 assert_eq!(
1117 Sandbox::linux("alpine").to_args(),
1118 s(&["linux", "alpine", "-d"])
1119 );
1120 }
1121
1122 #[test]
1123 fn linux_full() {
1124 let args = Sandbox::linux("ghcr.io/owner/name:tag")
1125 .kernel("vmlinux")
1126 .kernel_version("6.6")
1127 .initramfs()
1128 .volume("web")
1129 .mount("~/project:/src")
1130 .mount("~/data:/data:ro")
1131 .entrypoint("/bin/sh")
1132 .console("hvc0")
1133 .port("8080:80")
1134 .forward(2222, 22)
1135 .network("devnet")
1136 .name("api")
1137 .cpus(2)
1138 .mem(1024)
1139 .command(["node", "server.js"])
1140 .to_args();
1141 assert_eq!(
1142 args,
1143 s(&[
1144 "linux",
1145 "ghcr.io/owner/name:tag",
1146 "-d",
1147 "--kernel",
1148 "vmlinux",
1149 "--kernel-version",
1150 "6.6",
1151 "--initramfs",
1152 "-v",
1153 "web",
1154 "--mount",
1155 "~/project:/src",
1156 "--mount",
1157 "~/data:/data:ro",
1158 "--entrypoint",
1159 "/bin/sh",
1160 "--console",
1161 "hvc0",
1162 "--port",
1163 "8080:80",
1164 "--port",
1165 "2222:22",
1166 "--network",
1167 "devnet",
1168 "--name",
1169 "api",
1170 "--cpus",
1171 "2",
1172 "--mem",
1173 "1024",
1174 "--",
1175 "node",
1176 "server.js",
1177 ])
1178 );
1179 }
1180
1181 #[test]
1182 fn net_disabled_ordering() {
1183 let args = Sandbox::linux("alpine")
1185 .no_net()
1186 .port("2222:22")
1187 .mac("de:ad:be:ef:00:01")
1188 .network("devnet")
1189 .to_args();
1190 assert_eq!(
1191 args,
1192 s(&[
1193 "linux",
1194 "alpine",
1195 "-d",
1196 "--no-net",
1197 "--port",
1198 "2222:22",
1199 "--mac",
1200 "de:ad:be:ef:00:01",
1201 "--network",
1202 "devnet",
1203 ])
1204 );
1205 }
1206
1207 #[test]
1208 fn freebsd_full() {
1209 let args = Sandbox::freebsd()
1210 .version("14.3")
1211 .firmware("KRUN_EFI.fd")
1212 .force()
1213 .persist()
1214 .volume("db")
1215 .attach_disk("extra.raw")
1216 .attach_disk("ro.raw:ro")
1217 .mem(2048)
1218 .name("bsd")
1219 .to_args();
1220 assert_eq!(
1221 args,
1222 s(&[
1223 "freebsd",
1224 "-d",
1225 "--version",
1226 "14.3",
1227 "--firmware",
1228 "KRUN_EFI.fd",
1229 "--force",
1230 "--persist",
1231 "-v",
1232 "db",
1233 "--attach-disk",
1234 "extra.raw",
1235 "--attach-disk",
1236 "ro.raw:ro",
1237 "--name",
1238 "bsd",
1239 "--mem",
1240 "2048",
1241 ])
1242 );
1243 }
1244
1245 #[test]
1246 fn netbsd_minimal() {
1247 assert_eq!(
1248 Sandbox::netbsd().version("10.1").volume("db").to_args(),
1249 s(&["netbsd", "-d", "--version", "10.1", "-v", "db"])
1250 );
1251 }
1252
1253 #[test]
1254 fn firmware_positionals() {
1255 assert_eq!(
1256 Sandbox::firmware("KRUN_EFI.fd", "disk.raw").to_args(),
1257 s(&[
1258 "firmware",
1259 "--firmware",
1260 "KRUN_EFI.fd",
1261 "--disk",
1262 "disk.raw",
1263 "-d",
1264 ])
1265 );
1266 }
1267
1268 #[test]
1269 fn kernel_initramfs_takes_a_path() {
1270 let args = Sandbox::kernel("netbsd")
1272 .format("elf")
1273 .initramfs("initrd.img")
1274 .cmdline("root=ld0a")
1275 .disk("root.raw")
1276 .to_args();
1277 assert_eq!(
1278 args,
1279 s(&[
1280 "kernel",
1281 "--kernel",
1282 "netbsd",
1283 "-d",
1284 "--format",
1285 "elf",
1286 "--initramfs",
1287 "initrd.img",
1288 "--cmdline",
1289 "root=ld0a",
1290 "--disk",
1291 "root.raw",
1292 ])
1293 );
1294 }
1295
1296 #[test]
1297 fn nanos_image_goes_last() {
1298 let args = Sandbox::nanos("hello")
1299 .cmdline("x=1")
1300 .persist()
1301 .mem(512)
1302 .to_args();
1303 assert_eq!(
1304 args,
1305 s(&[
1306 "nanos",
1307 "-d",
1308 "--cmdline",
1309 "x=1",
1310 "--persist",
1311 "--mem",
1312 "512",
1313 "hello",
1314 ])
1315 );
1316 }
1317
1318 #[test]
1319 fn osv_image_goes_last() {
1320 let args = Sandbox::osv("loader.img")
1321 .disk("root.raw")
1322 .gic("v2")
1323 .to_args();
1324 assert_eq!(
1325 args,
1326 s(&[
1327 "osv",
1328 "-d",
1329 "--disk",
1330 "root.raw",
1331 "--gic",
1332 "v2",
1333 "loader.img",
1334 ])
1335 );
1336 }
1337
1338 #[test]
1339 fn unikraft_defaults_path() {
1340 assert_eq!(
1341 Sandbox::unikraft(".").cmdline("helloworld").to_args(),
1342 s(&["unikraft", "-d", "--cmdline", "helloworld", "."])
1343 );
1344 }
1345
1346 #[test]
1347 fn solo5_trailing_args_after_separator() {
1348 let args = Sandbox::solo5("dist/hello.hvt")
1349 .block("storage=disk.img")
1350 .args(["--ipv4=10.0.0.2/24"])
1351 .to_args();
1352 assert_eq!(
1353 args,
1354 s(&[
1355 "solo5",
1356 "-d",
1357 "--block",
1358 "storage=disk.img",
1359 "dist/hello.hvt",
1360 "--",
1361 "--ipv4=10.0.0.2/24",
1362 ])
1363 );
1364 }
1365
1366 #[test]
1367 fn machine_id_lines_are_recognized() {
1368 assert!(looks_like_machine_id("fab8f81e4f91"));
1369 assert!(looks_like_machine_id("abc123"));
1370 assert!(!looks_like_machine_id("abc12")); assert!(!looks_like_machine_id("pulling alpine:3.20"));
1372 assert!(!looks_like_machine_id("FAB8F81E4F91")); assert!(!looks_like_machine_id(""));
1374 }
1375
1376 #[test]
1377 fn ssh_port_is_read_from_the_banner() {
1378 assert_eq!(
1379 parse_ssh_port(" connect with: ssh -p 2222 root@localhost\n"),
1380 Some(2222)
1381 );
1382 assert_eq!(parse_ssh_port("no banner here"), None);
1383 }
1384}