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 entrypoint(mut self, entrypoint: impl Into<String>) -> Self {
242 self.opts.entrypoint = Some(entrypoint.into());
243 self
244 }
245
246 pub fn console(mut self, console: impl Into<String>) -> Self {
248 self.opts.console = Some(console.into());
249 self
250 }
251
252 pub fn command<I, S>(mut self, command: I) -> Self
254 where
255 I: IntoIterator<Item = S>,
256 S: Into<String>,
257 {
258 self.opts.command = strvec(command);
259 self
260 }
261}
262
263impl FreebsdBuilder {
264 common_setters!();
265 disk_setters!();
266
267 pub fn version(mut self, version: impl Into<String>) -> Self {
269 self.opts.version = Some(version.into());
270 self
271 }
272
273 pub fn firmware(mut self, firmware: impl Into<String>) -> Self {
275 self.opts.firmware = Some(firmware.into());
276 self
277 }
278
279 pub fn force(mut self) -> Self {
281 self.opts.force = true;
282 self
283 }
284}
285
286impl NetbsdBuilder {
287 common_setters!();
288 disk_setters!();
289
290 pub fn version(mut self, version: impl Into<String>) -> Self {
292 self.opts.version = Some(version.into());
293 self
294 }
295
296 pub fn force(mut self) -> Self {
298 self.opts.force = true;
299 self
300 }
301}
302
303impl FirmwareBuilder {
304 common_setters!();
305 disk_setters!();
306}
307
308impl KernelBuilder {
309 common_setters!();
310 disk_setters!();
311
312 pub fn format(mut self, format: impl Into<String>) -> Self {
314 self.opts.format = Some(format.into());
315 self
316 }
317
318 pub fn initramfs(mut self, path: impl Into<String>) -> Self {
321 self.opts.initramfs_path = Some(path.into());
322 self
323 }
324
325 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
327 self.opts.cmdline = Some(cmdline.into());
328 self
329 }
330
331 pub fn disk(mut self, disk: impl Into<String>) -> Self {
333 self.opts.disk = Some(disk.into());
334 self
335 }
336}
337
338impl NanosBuilder {
339 common_setters!();
340
341 pub fn kernel(mut self, kernel: impl Into<String>) -> Self {
343 self.opts.kernel = Some(kernel.into());
344 self
345 }
346
347 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
349 self.opts.cmdline = Some(cmdline.into());
350 self
351 }
352
353 pub fn persist(mut self) -> Self {
355 self.opts.persist = true;
356 self
357 }
358}
359
360impl OsvBuilder {
361 common_setters!();
362
363 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
365 self.opts.cmdline = Some(cmdline.into());
366 self
367 }
368
369 pub fn disk(mut self, disk: impl Into<String>) -> Self {
371 self.opts.disk = Some(disk.into());
372 self
373 }
374
375 pub fn gic(mut self, gic: impl Into<String>) -> Self {
377 self.opts.gic = Some(gic.into());
378 self
379 }
380
381 pub fn persist(mut self) -> Self {
383 self.opts.persist = true;
384 self
385 }
386}
387
388impl UnikraftBuilder {
389 common_setters!();
390
391 pub fn cmdline(mut self, cmdline: impl Into<String>) -> Self {
394 self.opts.cmdline = Some(cmdline.into());
395 self
396 }
397
398 pub fn initramfs(mut self, path: impl Into<String>) -> Self {
400 self.opts.initramfs_path = Some(path.into());
401 self
402 }
403
404 pub fn mount(mut self, mount: impl Into<String>) -> Self {
407 self.opts.mounts.push(mount.into());
408 self
409 }
410}
411
412impl Solo5Builder {
413 common_setters!();
414
415 pub fn block(mut self, block: impl Into<String>) -> Self {
419 self.opts.block.push(block.into());
420 self
421 }
422
423 pub fn args<I, S>(mut self, args: I) -> Self
426 where
427 I: IntoIterator<Item = S>,
428 S: Into<String>,
429 {
430 self.opts.trailing_args = strvec(args);
431 self
432 }
433}
434
435impl Sandbox {
438 pub fn linux(image: impl Into<String>) -> LinuxBuilder {
440 LinuxBuilder {
441 kind: Kind::Linux,
442 opts: CreateOpts {
443 image: Some(image.into()),
444 ..Default::default()
445 },
446 }
447 }
448
449 pub fn freebsd() -> FreebsdBuilder {
451 FreebsdBuilder {
452 kind: Kind::Freebsd,
453 opts: CreateOpts::default(),
454 }
455 }
456
457 pub fn netbsd() -> NetbsdBuilder {
459 NetbsdBuilder {
460 kind: Kind::Netbsd,
461 opts: CreateOpts::default(),
462 }
463 }
464
465 pub fn firmware(firmware: impl Into<String>, disk: impl Into<String>) -> FirmwareBuilder {
467 FirmwareBuilder {
468 kind: Kind::Firmware,
469 opts: CreateOpts {
470 firmware: Some(firmware.into()),
471 disk: Some(disk.into()),
472 ..Default::default()
473 },
474 }
475 }
476
477 pub fn kernel(kernel: impl Into<String>) -> KernelBuilder {
479 KernelBuilder {
480 kind: Kind::Kernel,
481 opts: CreateOpts {
482 kernel: Some(kernel.into()),
483 ..Default::default()
484 },
485 }
486 }
487
488 pub fn nanos(image: impl Into<String>) -> NanosBuilder {
490 NanosBuilder {
491 kind: Kind::Nanos,
492 opts: CreateOpts {
493 image: Some(image.into()),
494 ..Default::default()
495 },
496 }
497 }
498
499 pub fn osv(image: impl Into<String>) -> OsvBuilder {
502 OsvBuilder {
503 kind: Kind::Osv,
504 opts: CreateOpts {
505 image: Some(image.into()),
506 ..Default::default()
507 },
508 }
509 }
510
511 pub fn unikraft(path: impl Into<String>) -> UnikraftBuilder {
513 UnikraftBuilder {
514 kind: Kind::Unikraft,
515 opts: CreateOpts {
516 path: Some(path.into()),
517 ..Default::default()
518 },
519 }
520 }
521
522 pub fn solo5(path: impl Into<String>) -> Solo5Builder {
525 Solo5Builder {
526 kind: Kind::Solo5,
527 opts: CreateOpts {
528 path: Some(path.into()),
529 ..Default::default()
530 },
531 }
532 }
533
534 fn create_with(kind: Kind, opts: CreateOpts) -> Result<Sandbox> {
535 let log_level = opts.log_level.unwrap_or(1);
538 let args = build_create_args(kind, &opts);
539 let res = run_full(&args, &[], None, log_level)?;
540 if res.exit_code != 0 {
541 return Err(Error::CommandFailed {
542 exit_code: res.exit_code,
543 stdout: res.stdout,
544 stderr: res.stderr,
545 command: "bsdkrun create".to_string(),
546 });
547 }
548
549 let machine_id = res
552 .stdout
553 .lines()
554 .map(str::trim)
555 .rfind(|line| looks_like_machine_id(line))
556 .map(str::to_string);
557 let Some(id) = machine_id else {
558 return Err(Error::CommandFailed {
559 exit_code: res.exit_code,
560 stdout: res.stdout,
561 stderr: res.stderr,
562 command: "bsdkrun create (no machine id in output)".to_string(),
563 });
564 };
565 let ssh_port = parse_ssh_port(&res.stderr);
566 Ok(Sandbox { id, ssh_port })
567 }
568
569 pub fn from_id(id: impl Into<String>) -> Sandbox {
571 Sandbox {
572 id: id.into(),
573 ssh_port: None,
574 }
575 }
576
577 pub fn get(id: &str) -> Result<Sandbox> {
579 for info in Self::list(true)? {
580 if info.id == id || info.id.starts_with(id) || info.name == Some(id.to_string()) {
581 return Ok(Sandbox {
582 id: info.id,
583 ssh_port: None,
584 });
585 }
586 }
587 Err(Error::SandboxNotFound { id: id.to_string() })
588 }
589
590 pub fn list(all: bool) -> Result<Vec<SandboxInfo>> {
592 let mut args = vec!["ps".to_string(), "--json".to_string()];
593 if all {
594 args.push("--all".to_string());
595 }
596 let res = checked(&args, "bsdkrun ps")?;
597 let raw = if res.stdout.trim().is_empty() {
598 "[]".to_string()
599 } else {
600 res.stdout
601 };
602 let rows: Value = serde_json::from_str(&raw)?;
603 Ok(rows
604 .as_array()
605 .map(|rows| rows.iter().map(SandboxInfo::from_row).collect())
606 .unwrap_or_default())
607 }
608
609 pub fn id(&self) -> &str {
611 &self.id
612 }
613
614 pub fn ssh_port(&self) -> Option<u16> {
616 self.ssh_port
617 }
618
619 pub fn command(&self, program: impl Into<String>) -> CommandBuilder {
634 CommandBuilder {
635 sandbox_id: self.id.clone(),
636 argv: vec![program.into()],
637 env: Vec::new(),
638 cwd: None,
639 stdin: None,
640 tty: false,
641 log_level: 0,
642 stdout: None,
643 stderr: None,
644 }
645 }
646
647 pub fn exec<I, S>(&self, argv: I) -> Result<ExecResult>
653 where
654 I: IntoIterator<Item = S>,
655 S: Into<String>,
656 {
657 let mut argv = strvec(argv).into_iter();
658 let Some(program) = argv.next() else {
659 return Err(Error::InvalidInput("exec needs a non-empty argv".into()));
660 };
661 let mut builder = self.command(program);
662 builder.argv.extend(argv);
663 builder.run()
664 }
665
666 pub fn logs(&self) -> Result<String> {
668 Ok(run_full(&strvec(["logs", self.id.as_str()]), &[], None, 0)?.stdout)
669 }
670
671 pub fn boot_logs(&self) -> Result<String> {
673 Ok(run_full(&strvec(["logs", "--boot", self.id.as_str()]), &[], None, 0)?.stdout)
674 }
675
676 pub fn shell(&self) -> Result<i32> {
680 spawn(["shell", self.id.as_str()])
681 }
682
683 pub fn status(&self) -> Result<Option<SandboxInfo>> {
687 Ok(Self::list(true)?
688 .into_iter()
689 .find(|info| info.id == self.id))
690 }
691
692 pub fn is_running(&self) -> Result<bool> {
694 Ok(self.status()?.map(|info| info.running).unwrap_or(false))
695 }
696
697 pub fn stop(&self) -> Result<()> {
701 checked(&strvec(["stop", self.id.as_str()]), "bsdkrun stop")?;
702 Ok(())
703 }
704
705 pub fn start(&self) -> Result<()> {
707 checked(&strvec(["start", self.id.as_str()]), "bsdkrun start")?;
708 Ok(())
709 }
710
711 pub fn remove(&self, force: bool) -> Result<()> {
713 let mut args = vec!["rm".to_string()];
714 if force {
715 args.push("--force".to_string());
716 }
717 args.push(self.id.clone());
718 checked(&args, "bsdkrun rm")?;
719 Ok(())
720 }
721
722 pub fn update(&self) -> UpdateBuilder {
730 UpdateBuilder {
731 id: self.id.clone(),
732 cpus: None,
733 mem: None,
734 }
735 }
736
737 pub fn connect_network(&self, network: &str) -> Result<()> {
739 checked(
740 &strvec(["network", "connect", self.id.as_str(), network]),
741 "bsdkrun network connect",
742 )?;
743 Ok(())
744 }
745
746 pub fn disconnect_network(&self) -> Result<()> {
748 checked(
749 &strvec(["network", "disconnect", self.id.as_str()]),
750 "bsdkrun network disconnect",
751 )?;
752 Ok(())
753 }
754
755 pub fn ssh_setup(&self) -> SshSetupBuilder {
767 SshSetupBuilder {
768 id: self.id.clone(),
769 user: None,
770 keys: Vec::new(),
771 }
772 }
773
774 pub fn tailscale_up(&self) -> TailscaleUpBuilder {
782 TailscaleUpBuilder {
783 id: self.id.clone(),
784 authkey: None,
785 hostname: None,
786 extra: Vec::new(),
787 }
788 }
789}
790
791pub struct CommandBuilder {
793 sandbox_id: String,
794 argv: Vec<String>,
795 env: Vec<(String, String)>,
796 cwd: Option<String>,
797 stdin: Option<Vec<u8>>,
798 tty: bool,
799 log_level: u32,
800 stdout: Option<Box<dyn std::io::Write + Send>>,
801 stderr: Option<Box<dyn std::io::Write + Send>>,
802}
803
804impl CommandBuilder {
805 pub fn arg(mut self, arg: impl Into<String>) -> Self {
807 self.argv.push(arg.into());
808 self
809 }
810
811 pub fn args<I, S>(mut self, args: I) -> Self
813 where
814 I: IntoIterator<Item = S>,
815 S: Into<String>,
816 {
817 self.argv.extend(strvec(args));
818 self
819 }
820
821 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
823 self.env.push((key.into(), value.into()));
824 self
825 }
826
827 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
829 self.cwd = Some(cwd.into());
830 self
831 }
832
833 pub fn stdin(mut self, data: impl AsRef<[u8]>) -> Self {
835 self.stdin = Some(data.as_ref().to_vec());
836 self
837 }
838
839 pub fn tty(mut self, tty: bool) -> Self {
841 self.tty = tty;
842 self
843 }
844
845 pub fn log_level(mut self, level: u32) -> Self {
847 self.log_level = level;
848 self
849 }
850
851 pub fn stdout(mut self, writer: impl std::io::Write + Send + 'static) -> Self {
853 self.stdout = Some(Box::new(writer));
854 self
855 }
856
857 pub fn stderr(mut self, writer: impl std::io::Write + Send + 'static) -> Self {
859 self.stderr = Some(Box::new(writer));
860 self
861 }
862
863 pub fn run(self) -> Result<ExecResult> {
868 let mut argv = self.argv;
869 if let Some(cwd) = &self.cwd {
870 let mut wrapped = strvec([
872 "/bin/sh",
873 "-c",
874 "cd \"$1\" && shift && exec \"$@\"",
875 "sh",
876 cwd.as_str(),
877 ]);
878 wrapped.append(&mut argv);
879 argv = wrapped;
880 }
881
882 let mut cli = vec!["exec".to_string()];
883 if self.tty {
884 cli.push("-t".to_string());
885 }
886 for (key, value) in &self.env {
887 cli.push("-e".to_string());
888 cli.push(format!("{key}={value}"));
889 }
890 cli.push(self.sandbox_id.clone());
891 cli.extend(argv.iter().cloned());
892
893 let res = run_full_stream(
894 &cli,
895 &[],
896 self.stdin.as_deref(),
897 self.log_level,
898 self.stdout,
899 self.stderr,
900 )?;
901 Ok(ExecResult {
902 stdout: res.stdout,
903 stderr: res.stderr,
904 exit_code: res.exit_code,
905 command: format!("exec {}", argv.join(" ")),
906 })
907 }
908}
909
910#[derive(Debug, Clone)]
912pub struct UpdateBuilder {
913 id: String,
914 cpus: Option<u32>,
915 mem: Option<u32>,
916}
917
918impl UpdateBuilder {
919 pub fn cpus(mut self, cpus: u32) -> Self {
920 self.cpus = Some(cpus);
921 self
922 }
923
924 pub fn mem(mut self, mib: u32) -> Self {
925 self.mem = Some(mib);
926 self
927 }
928
929 pub fn apply(self) -> Result<()> {
931 let mut args = strvec(["update", self.id.as_str()]);
932 if let Some(cpus) = self.cpus {
933 args.push("--cpus".to_string());
934 args.push(cpus.to_string());
935 }
936 if let Some(mem) = self.mem {
937 args.push("--mem".to_string());
938 args.push(mem.to_string());
939 }
940 checked(&args, "bsdkrun update")?;
941 Ok(())
942 }
943}
944
945#[derive(Debug, Clone)]
947pub struct SshSetupBuilder {
948 id: String,
949 user: Option<String>,
950 keys: Vec<String>,
951}
952
953impl SshSetupBuilder {
954 pub fn user(mut self, user: impl Into<String>) -> Self {
956 self.user = Some(user.into());
957 self
958 }
959
960 pub fn key(mut self, key: impl Into<String>) -> Self {
962 self.keys.push(key.into());
963 self
964 }
965
966 pub fn run(self) -> Result<ExecResult> {
969 let mut action = vec!["setup".to_string()];
970 if let Some(user) = &self.user {
971 action.push("--user".to_string());
972 action.push(user.clone());
973 }
974 for key in &self.keys {
975 action.push("--key".to_string());
976 action.push(key.clone());
977 }
978 run_agent("ssh", &self.id, &action, &[])
979 }
980}
981
982#[derive(Debug, Clone)]
984pub struct TailscaleUpBuilder {
985 id: String,
986 authkey: Option<String>,
987 hostname: Option<String>,
988 extra: Vec<String>,
989}
990
991impl TailscaleUpBuilder {
992 pub fn authkey(mut self, authkey: impl Into<String>) -> Self {
995 self.authkey = Some(authkey.into());
996 self
997 }
998
999 pub fn hostname(mut self, hostname: impl Into<String>) -> Self {
1001 self.hostname = Some(hostname.into());
1002 self
1003 }
1004
1005 pub fn arg(mut self, arg: impl Into<String>) -> Self {
1007 self.extra.push(arg.into());
1008 self
1009 }
1010
1011 pub fn run(self) -> Result<ExecResult> {
1013 let mut action = vec!["setup".to_string()];
1014 if let Some(hostname) = &self.hostname {
1015 action.push("--hostname".to_string());
1016 action.push(hostname.clone());
1017 }
1018 action.extend(self.extra.iter().cloned());
1019 let env: Vec<(String, String)> = self
1020 .authkey
1021 .map(|key| vec![("TS_AUTHKEY".to_string(), key)])
1022 .unwrap_or_default();
1023 run_agent("tailscale", &self.id, &action, &env)
1024 }
1025}
1026
1027fn run_agent(
1028 family: &str,
1029 id: &str,
1030 action: &[String],
1031 env: &[(String, String)],
1032) -> Result<ExecResult> {
1033 let mut args = vec![family.to_string(), id.to_string()];
1034 args.extend(action.iter().cloned());
1035 let res = run_full(&args, env, None, 0)?;
1036 ExecResult {
1037 stdout: res.stdout,
1038 stderr: res.stderr,
1039 exit_code: res.exit_code,
1040 command: format!("{family} {}", action.join(" ")),
1041 }
1042 .ok_or_err()
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use super::*;
1048
1049 fn s(items: &[&str]) -> Vec<String> {
1050 items.iter().map(|s| s.to_string()).collect()
1051 }
1052
1053 #[test]
1054 fn linux_minimal() {
1055 assert_eq!(
1056 Sandbox::linux("alpine").to_args(),
1057 s(&["linux", "alpine", "-d"])
1058 );
1059 }
1060
1061 #[test]
1062 fn linux_full() {
1063 let args = Sandbox::linux("ghcr.io/owner/name:tag")
1064 .kernel("vmlinux")
1065 .kernel_version("6.6")
1066 .initramfs()
1067 .volume("web")
1068 .mount("~/project:/src")
1069 .mount("~/data:/data:ro")
1070 .entrypoint("/bin/sh")
1071 .console("hvc0")
1072 .port("8080:80")
1073 .forward(2222, 22)
1074 .network("devnet")
1075 .name("api")
1076 .cpus(2)
1077 .mem(1024)
1078 .command(["node", "server.js"])
1079 .to_args();
1080 assert_eq!(
1081 args,
1082 s(&[
1083 "linux",
1084 "ghcr.io/owner/name:tag",
1085 "-d",
1086 "--kernel",
1087 "vmlinux",
1088 "--kernel-version",
1089 "6.6",
1090 "--initramfs",
1091 "-v",
1092 "web",
1093 "--mount",
1094 "~/project:/src",
1095 "--mount",
1096 "~/data:/data:ro",
1097 "--entrypoint",
1098 "/bin/sh",
1099 "--console",
1100 "hvc0",
1101 "--port",
1102 "8080:80",
1103 "--port",
1104 "2222:22",
1105 "--network",
1106 "devnet",
1107 "--name",
1108 "api",
1109 "--cpus",
1110 "2",
1111 "--mem",
1112 "1024",
1113 "--",
1114 "node",
1115 "server.js",
1116 ])
1117 );
1118 }
1119
1120 #[test]
1121 fn net_disabled_ordering() {
1122 let args = Sandbox::linux("alpine")
1124 .no_net()
1125 .port("2222:22")
1126 .mac("de:ad:be:ef:00:01")
1127 .network("devnet")
1128 .to_args();
1129 assert_eq!(
1130 args,
1131 s(&[
1132 "linux",
1133 "alpine",
1134 "-d",
1135 "--no-net",
1136 "--port",
1137 "2222:22",
1138 "--mac",
1139 "de:ad:be:ef:00:01",
1140 "--network",
1141 "devnet",
1142 ])
1143 );
1144 }
1145
1146 #[test]
1147 fn freebsd_full() {
1148 let args = Sandbox::freebsd()
1149 .version("14.3")
1150 .firmware("KRUN_EFI.fd")
1151 .force()
1152 .persist()
1153 .volume("db")
1154 .attach_disk("extra.raw")
1155 .attach_disk("ro.raw:ro")
1156 .mem(2048)
1157 .name("bsd")
1158 .to_args();
1159 assert_eq!(
1160 args,
1161 s(&[
1162 "freebsd",
1163 "-d",
1164 "--version",
1165 "14.3",
1166 "--firmware",
1167 "KRUN_EFI.fd",
1168 "--force",
1169 "--persist",
1170 "-v",
1171 "db",
1172 "--attach-disk",
1173 "extra.raw",
1174 "--attach-disk",
1175 "ro.raw:ro",
1176 "--name",
1177 "bsd",
1178 "--mem",
1179 "2048",
1180 ])
1181 );
1182 }
1183
1184 #[test]
1185 fn netbsd_minimal() {
1186 assert_eq!(
1187 Sandbox::netbsd().version("10.1").volume("db").to_args(),
1188 s(&["netbsd", "-d", "--version", "10.1", "-v", "db"])
1189 );
1190 }
1191
1192 #[test]
1193 fn firmware_positionals() {
1194 assert_eq!(
1195 Sandbox::firmware("KRUN_EFI.fd", "disk.raw").to_args(),
1196 s(&[
1197 "firmware",
1198 "--firmware",
1199 "KRUN_EFI.fd",
1200 "--disk",
1201 "disk.raw",
1202 "-d",
1203 ])
1204 );
1205 }
1206
1207 #[test]
1208 fn kernel_initramfs_takes_a_path() {
1209 let args = Sandbox::kernel("netbsd")
1211 .format("elf")
1212 .initramfs("initrd.img")
1213 .cmdline("root=ld0a")
1214 .disk("root.raw")
1215 .to_args();
1216 assert_eq!(
1217 args,
1218 s(&[
1219 "kernel",
1220 "--kernel",
1221 "netbsd",
1222 "-d",
1223 "--format",
1224 "elf",
1225 "--initramfs",
1226 "initrd.img",
1227 "--cmdline",
1228 "root=ld0a",
1229 "--disk",
1230 "root.raw",
1231 ])
1232 );
1233 }
1234
1235 #[test]
1236 fn nanos_image_goes_last() {
1237 let args = Sandbox::nanos("hello")
1238 .cmdline("x=1")
1239 .persist()
1240 .mem(512)
1241 .to_args();
1242 assert_eq!(
1243 args,
1244 s(&[
1245 "nanos",
1246 "-d",
1247 "--cmdline",
1248 "x=1",
1249 "--persist",
1250 "--mem",
1251 "512",
1252 "hello",
1253 ])
1254 );
1255 }
1256
1257 #[test]
1258 fn osv_image_goes_last() {
1259 let args = Sandbox::osv("loader.img")
1260 .disk("root.raw")
1261 .gic("v2")
1262 .to_args();
1263 assert_eq!(
1264 args,
1265 s(&[
1266 "osv",
1267 "-d",
1268 "--disk",
1269 "root.raw",
1270 "--gic",
1271 "v2",
1272 "loader.img",
1273 ])
1274 );
1275 }
1276
1277 #[test]
1278 fn unikraft_defaults_path() {
1279 assert_eq!(
1280 Sandbox::unikraft(".").cmdline("helloworld").to_args(),
1281 s(&["unikraft", "-d", "--cmdline", "helloworld", "."])
1282 );
1283 }
1284
1285 #[test]
1286 fn solo5_trailing_args_after_separator() {
1287 let args = Sandbox::solo5("dist/hello.hvt")
1288 .block("storage=disk.img")
1289 .args(["--ipv4=10.0.0.2/24"])
1290 .to_args();
1291 assert_eq!(
1292 args,
1293 s(&[
1294 "solo5",
1295 "-d",
1296 "--block",
1297 "storage=disk.img",
1298 "dist/hello.hvt",
1299 "--",
1300 "--ipv4=10.0.0.2/24",
1301 ])
1302 );
1303 }
1304
1305 #[test]
1306 fn machine_id_lines_are_recognized() {
1307 assert!(looks_like_machine_id("fab8f81e4f91"));
1308 assert!(looks_like_machine_id("abc123"));
1309 assert!(!looks_like_machine_id("abc12")); assert!(!looks_like_machine_id("pulling alpine:3.20"));
1311 assert!(!looks_like_machine_id("FAB8F81E4F91")); assert!(!looks_like_machine_id(""));
1313 }
1314
1315 #[test]
1316 fn ssh_port_is_read_from_the_banner() {
1317 assert_eq!(
1318 parse_ssh_port(" connect with: ssh -p 2222 root@localhost\n"),
1319 Some(2222)
1320 );
1321 assert_eq!(parse_ssh_port("no banner here"), None);
1322 }
1323}