microsandbox_core/vm/
builder.rs

1use std::net::Ipv4Addr;
2
3use ipnetwork::Ipv4Network;
4use microsandbox_utils::{DEFAULT_MEMORY_MIB, DEFAULT_NUM_VCPUS};
5use typed_path::Utf8UnixPathBuf;
6
7use crate::{
8    config::{EnvPair, NetworkScope, PathPair, PortPair},
9    MicrosandboxResult,
10};
11
12use super::{LinuxRlimit, LogLevel, MicroVm, MicroVmConfig, Rootfs};
13
14//--------------------------------------------------------------------------------------------------
15// Types
16//--------------------------------------------------------------------------------------------------
17
18/// The builder for a MicroVm configuration.
19///
20/// ## Required Fields
21/// - `rootfs`: The root filesystem to use for the MicroVm.
22/// - `exec_path`: The path to the executable to run in the MicroVm.
23///
24/// ## Optional Fields
25/// - `num_vcpus`: The number of virtual CPUs to use for the MicroVm.
26/// - `memory_mib`: The amount of memory in MiB to use for the MicroVm.
27/// - `mapped_dirs`: The directories to mount in the MicroVm.
28/// - `port_map`: The ports to map in the MicroVm.
29/// - `rlimits`: The resource limits to use for the MicroVm.
30/// - `workdir_path`: The working directory to use for the MicroVm.
31/// - `args`: The arguments to pass to the executable.
32/// - `env`: The environment variables to use for the MicroVm.
33/// - `console_output`: The path to the file to write the console output to.
34#[derive(Debug)]
35pub struct MicroVmConfigBuilder<R, E> {
36    log_level: LogLevel,
37    rootfs: R,
38    num_vcpus: u8,
39    memory_mib: u32,
40    mapped_dirs: Vec<PathPair>,
41    port_map: Vec<PortPair>,
42    scope: NetworkScope,
43    ip: Option<Ipv4Addr>,
44    subnet: Option<Ipv4Network>,
45    rlimits: Vec<LinuxRlimit>,
46    workdir_path: Option<Utf8UnixPathBuf>,
47    exec_path: E,
48    args: Vec<String>,
49    env: Vec<EnvPair>,
50    console_output: Option<Utf8UnixPathBuf>,
51}
52
53/// The builder for a MicroVm.
54///
55/// This struct provides a fluent interface for configuring and creating a `MicroVm` instance.
56/// It allows you to set various parameters such as the log level, root path, number of vCPUs,
57/// memory size, virtio-fs mounts, port mappings, resource limits, working directory, executable path,
58/// arguments, environment variables, and console output.
59///
60/// ## Required Fields
61/// - `rootfs`: The root filesystem to use for the MicroVm.
62/// - `exec_path`: The path to the executable to run in the MicroVm.
63///
64/// ## Optional Fields
65/// - `num_vcpus`: The number of virtual CPUs to use for the MicroVm.
66/// - `memory_mib`: The amount of memory in MiB to use for the MicroVm.
67/// - `mapped_dirs`: The directories to mount in the MicroVm.
68/// - `port_map`: The ports to map in the MicroVm.
69/// - `scope`: The network scope to use for the MicroVm.
70/// - `ip`: The IP address to use for the MicroVm.
71/// - `subnet`: The subnet to use for the MicroVm.
72/// - `rlimits`: The resource limits to use for the MicroVm.
73/// - `workdir_path`: The working directory to use for the MicroVm.
74/// - `args`: The arguments to pass to the executable.
75/// - `env`: The environment variables to use for the MicroVm.
76/// - `console_output`: The path to the file to write the console output to.
77///
78/// ## Examples
79///
80/// ```rust
81/// use microsandbox_core::vm::{MicroVmBuilder, LogLevel, Rootfs};
82/// use microsandbox_core::config::NetworkScope;
83/// use std::path::PathBuf;
84///
85/// # fn main() -> anyhow::Result<()> {
86/// let vm = MicroVmBuilder::default()
87///     .log_level(LogLevel::Debug)
88///     .rootfs(Rootfs::Native(PathBuf::from("/tmp")))
89///     .num_vcpus(2)
90///     .memory_mib(1024)
91///     .mapped_dirs(["/home:/guest/mount".parse()?])
92///     .port_map(["8080:80".parse()?])
93///     .scope(NetworkScope::Public)
94///     .ip("192.168.1.100".parse()?)
95///     .subnet("192.168.1.0/24".parse()?)
96///     .rlimits(["RLIMIT_NOFILE=1024:1024".parse()?])
97///     .workdir_path("/workdir")
98///     .exec_path("/bin/example")
99///     .args(["arg1", "arg2"])
100///     .env(["KEY1=VALUE1".parse()?, "KEY2=VALUE2".parse()?])
101///     .console_output("/tmp/console.log")
102///     .build()?;
103/// # Ok(())
104/// # }
105/// ```
106#[derive(Debug)]
107pub struct MicroVmBuilder<R, E> {
108    inner: MicroVmConfigBuilder<R, E>,
109}
110
111//--------------------------------------------------------------------------------------------------
112// Methods
113//--------------------------------------------------------------------------------------------------
114
115impl<R, M> MicroVmConfigBuilder<R, M> {
116    /// Sets the log level for the MicroVm.
117    ///
118    /// The log level controls the verbosity of the MicroVm's logging output.
119    ///
120    /// ## Examples
121    ///
122    /// ```rust
123    /// use microsandbox_core::vm::{MicroVmConfigBuilder, LogLevel};
124    ///
125    /// let config = MicroVmConfigBuilder::default()
126    ///     .log_level(LogLevel::Debug);  // Enable debug logging
127    /// ```
128    ///
129    /// ## Log Levels
130    /// - `Off` - No logging (default)
131    /// - `Error` - Only error messages
132    /// - `Warn` - Warnings and errors
133    /// - `Info` - Informational messages, warnings, and errors
134    /// - `Debug` - Debug information and all above
135    /// - `Trace` - Detailed trace information and all above
136    pub fn log_level(mut self, log_level: LogLevel) -> Self {
137        self.log_level = log_level;
138        self
139    }
140
141    /// Sets the root filesystem sharing mode for the MicroVm.
142    ///
143    /// This determines how the root filesystem is shared with the guest system, with two options:
144    /// - `Rootfs::Native`: Direct passthrough of a directory as the root filesystem
145    /// - `Rootfs::Overlayfs`: Use overlayfs with multiple layers as the root filesystem
146    ///
147    /// ## Examples
148    ///
149    /// ```rust
150    /// use microsandbox_core::vm::{MicroVmConfigBuilder, Rootfs};
151    /// use std::path::PathBuf;
152    ///
153    /// let config = MicroVmConfigBuilder::default()
154    ///     // Option 1: Direct passthrough of a directory
155    ///     .rootfs(Rootfs::Native(PathBuf::from("/path/to/rootfs")));
156    ///
157    /// let config = MicroVmConfigBuilder::default()
158    ///     // Option 2: Overlayfs with multiple layers
159    ///     .rootfs(Rootfs::Overlayfs(vec![
160    ///         PathBuf::from("/path/to/layer1"),
161    ///         PathBuf::from("/path/to/layer2")
162    ///     ]));
163    /// ```
164    ///
165    /// ## Notes
166    /// - For Passthrough: The directory must exist and contain a valid root filesystem structure
167    /// - For Overlayfs: The layers are stacked in order, with later layers taking precedence
168    /// - Common choices include Alpine Linux or Ubuntu root filesystems
169    pub fn rootfs(self, rootfs: Rootfs) -> MicroVmConfigBuilder<Rootfs, M> {
170        MicroVmConfigBuilder {
171            log_level: self.log_level,
172            rootfs,
173            num_vcpus: self.num_vcpus,
174            memory_mib: self.memory_mib,
175            mapped_dirs: self.mapped_dirs,
176            port_map: self.port_map,
177            scope: self.scope,
178            ip: self.ip,
179            subnet: self.subnet,
180            rlimits: self.rlimits,
181            workdir_path: self.workdir_path,
182            exec_path: self.exec_path,
183            args: self.args,
184            env: self.env,
185            console_output: self.console_output,
186        }
187    }
188
189    /// Sets the number of virtual CPUs (vCPUs) for the MicroVm.
190    ///
191    /// This determines how many CPU cores are available to the guest system.
192    ///
193    /// ## Examples
194    ///
195    /// ```rust
196    /// use microsandbox_core::vm::MicroVmConfigBuilder;
197    ///
198    /// let config = MicroVmConfigBuilder::default()
199    ///     .num_vcpus(2);  // Allocate 2 virtual CPU cores
200    /// ```
201    ///
202    /// ## Notes
203    /// - The default is 1 vCPU if not specified
204    /// - The number of vCPUs should not exceed the host's physical CPU cores
205    /// - More vCPUs aren't always better - consider the workload's needs
206    pub fn num_vcpus(mut self, num_vcpus: u8) -> Self {
207        self.num_vcpus = num_vcpus;
208        self
209    }
210
211    /// Sets the amount of memory in MiB for the MicroVm.
212    ///
213    /// This determines how much memory is available to the guest system.
214    ///
215    /// ## Examples
216    ///
217    /// ```rust
218    /// use microsandbox_core::vm::MicroVmConfigBuilder;
219    ///
220    /// let config = MicroVmConfigBuilder::default()
221    ///     .memory_mib(1024);  // Allocate 1 GiB of memory
222    /// ```
223    ///
224    /// ## Notes
225    /// - The value is in MiB (1 GiB = 1024 MiB)
226    /// - Consider the host's available memory when setting this value
227    /// - Common values: 512 MiB for minimal systems, 1024-2048 MiB for typical workloads
228    pub fn memory_mib(mut self, memory_mib: u32) -> Self {
229        self.memory_mib = memory_mib;
230        self
231    }
232
233    /// Sets the directory mappings for the MicroVm using virtio-fs.
234    ///
235    /// Each mapping follows Docker's volume mapping convention using the format `host:guest`.
236    ///
237    /// ## Examples
238    ///
239    /// ```rust
240    /// use microsandbox_core::vm::MicroVmConfigBuilder;
241    ///
242    /// # fn main() -> anyhow::Result<()> {
243    /// let config = MicroVmConfigBuilder::default()
244    ///     .mapped_dirs([
245    ///         // Share host's /data directory as /mnt/data in guest
246    ///         "/data:/mnt/data".parse()?,
247    ///         // Share current directory as /app in guest
248    ///         "./:/app".parse()?,
249    ///         // Use same path in both host and guest
250    ///         "/shared".parse()?
251    ///     ]);
252    /// # Ok(())
253    /// # }
254    /// ```
255    ///
256    /// ## Notes
257    /// - Host paths must exist and be accessible
258    /// - Guest paths will be created if they don't exist
259    /// - Changes in shared directories are immediately visible to both systems
260    /// - Useful for development, configuration files, and data sharing
261    pub fn mapped_dirs(mut self, mapped_dirs: impl IntoIterator<Item = PathPair>) -> Self {
262        self.mapped_dirs = mapped_dirs.into_iter().collect();
263        self
264    }
265
266    /// Sets the port mappings between host and guest for the MicroVm.
267    ///
268    /// Port mappings follow Docker's convention using the format `host:guest`, where:
269    /// - `host` is the port number on the host machine
270    /// - `guest` is the port number inside the MicroVm
271    ///
272    /// ## Examples
273    ///
274    /// ```rust
275    /// use microsandbox_core::vm::MicroVmConfigBuilder;
276    /// use microsandbox_core::config::PortPair;
277    ///
278    /// # fn main() -> anyhow::Result<()> {
279    /// let config = MicroVmConfigBuilder::default()
280    ///     .port_map([
281    ///         // Map host port 8080 to guest port 80
282    ///         "8080:80".parse()?,
283    ///         // Map host port 2222 to guest port 22
284    ///         "2222:22".parse()?,
285    ///         // Use same port (3000) on both host and guest
286    ///         "3000".parse()?
287    ///     ]);
288    /// # Ok(())
289    /// # }
290    /// ```
291    ///
292    /// ## Notes
293    ///
294    /// - If you don't call this method, no ports will be mapped between host and guest
295    /// - The guest application will need to use the guest port number to listen for connections
296    /// - External connections should use the host port number to connect to the service
297    pub fn port_map(mut self, port_map: impl IntoIterator<Item = PortPair>) -> Self {
298        self.port_map = port_map.into_iter().collect();
299        self
300    }
301
302    /// Sets the network scope for the MicroVm.
303    ///
304    /// The network scope controls the MicroVm's level of network isolation and connectivity.
305    ///
306    /// ## Examples
307    ///
308    /// ```rust
309    /// use microsandbox_core::vm::MicroVmConfigBuilder;
310    /// use microsandbox_core::config::NetworkScope;
311    ///
312    /// let config = MicroVmConfigBuilder::default()
313    ///     .scope(NetworkScope::Public);  // Allow access to public networks
314    /// ```
315    ///
316    /// ## Network Scope Options
317    /// - `None` - Sandboxes cannot communicate with any other sandboxes
318    /// - `Group` - Sandboxes can only communicate within their subnet (default)
319    /// - `Public` - Sandboxes can communicate with any other non-private address
320    /// - `Any` - Sandboxes can communicate with any address
321    ///
322    /// ## Notes
323    /// - Choose the appropriate scope based on your security requirements
324    /// - More restrictive scopes provide better isolation
325    /// - The default scope is `Group` if not specified
326    pub fn scope(mut self, scope: NetworkScope) -> Self {
327        self.scope = scope;
328        self
329    }
330
331    /// Sets the IP address for the MicroVm.
332    ///
333    /// This sets a specific IPv4 address for the guest system's network interface.
334    ///
335    /// ## Examples
336    ///
337    /// ```rust
338    /// use microsandbox_core::vm::MicroVmConfigBuilder;
339    /// use std::net::Ipv4Addr;
340    ///
341    /// let config = MicroVmConfigBuilder::default()
342    ///     .ip(Ipv4Addr::new(192, 168, 1, 100));  // Assign IP 192.168.1.100 to the MicroVm
343    /// ```
344    ///
345    /// ## Notes
346    /// - The IP address should be within the subnet assigned to the MicroVm
347    /// - If not specified, an IP address may be assigned automatically
348    /// - Useful for predictable addressing when running multiple MicroVms
349    /// - Consider using with the `subnet` method to define the network
350    pub fn ip(mut self, ip: Ipv4Addr) -> Self {
351        self.ip = Some(ip);
352        self
353    }
354
355    /// Sets the subnet for the MicroVm.
356    ///
357    /// This defines the IPv4 network and mask for the guest system's network interface.
358    ///
359    /// ## Examples
360    ///
361    /// ```rust
362    /// use microsandbox_core::vm::MicroVmConfigBuilder;
363    /// use ipnetwork::Ipv4Network;
364    ///
365    /// # fn main() -> anyhow::Result<()> {
366    /// let config = MicroVmConfigBuilder::default()
367    ///     .subnet("192.168.1.0/24".parse()?);  // Set subnet to 192.168.1.0/24
368    /// # Ok(())
369    /// # }
370    /// ```
371    ///
372    /// ## Notes
373    /// - The subnet defines the range of IP addresses available to the MicroVm
374    /// - Common subnet masks: /24 (256 addresses), /16 (65536 addresses)
375    /// - IP addresses assigned to the MicroVm should be within this subnet
376    /// - Important for networking between multiple MicroVms in the same group
377    pub fn subnet(mut self, subnet: Ipv4Network) -> Self {
378        self.subnet = Some(subnet);
379        self
380    }
381
382    /// Sets the resource limits for processes in the MicroVm.
383    ///
384    /// Resource limits control various system resources available to processes running
385    /// in the guest system, following Linux's rlimit convention.
386    ///
387    /// ## Format
388    /// Resource limits use the format `RESOURCE=SOFT:HARD` or `NUMBER=SOFT:HARD`, where:
389    /// - `RESOURCE` is the resource name (e.g., RLIMIT_NOFILE)
390    /// - `NUMBER` is the resource number (e.g., 7 for RLIMIT_NOFILE)
391    /// - `SOFT` is the soft limit (enforced limit)
392    /// - `HARD` is the hard limit (ceiling for soft limit)
393    ///
394    /// ## Examples
395    ///
396    /// ```rust
397    /// use microsandbox_core::vm::MicroVmConfigBuilder;
398    ///
399    /// # fn main() -> anyhow::Result<()> {
400    /// let config = MicroVmConfigBuilder::default()
401    ///     .rlimits([
402    ///         // Limit number of open files
403    ///         "RLIMIT_NOFILE=1024:2048".parse()?,
404    ///         // Limit process memory
405    ///         "RLIMIT_AS=1073741824:2147483648".parse()?,  // 1GB:2GB
406    ///         // Can also use resource numbers
407    ///         "7=1024:2048".parse()?  // Same as RLIMIT_NOFILE
408    ///     ]);
409    /// # Ok(())
410    /// # }
411    /// ```
412    ///
413    /// ## Common Resource Limits
414    /// - `RLIMIT_NOFILE` (7) - Maximum number of open files
415    /// - `RLIMIT_AS` (9) - Maximum size of process's virtual memory
416    /// - `RLIMIT_NPROC` (6) - Maximum number of processes
417    /// - `RLIMIT_CPU` (0) - CPU time limit in seconds
418    /// - `RLIMIT_FSIZE` (1) - Maximum file size
419    pub fn rlimits(mut self, rlimits: impl IntoIterator<Item = LinuxRlimit>) -> Self {
420        self.rlimits = rlimits.into_iter().collect();
421        self
422    }
423
424    /// Sets the working directory for processes in the MicroVm.
425    ///
426    /// This directory will be the current working directory (cwd) for any processes
427    /// started in the guest system.
428    ///
429    /// ## Examples
430    ///
431    /// ```rust
432    /// use microsandbox_core::vm::MicroVmConfigBuilder;
433    ///
434    /// let config = MicroVmConfigBuilder::default()
435    ///     .workdir_path("/app")  // Set working directory to /app
436    ///     .exec_path("/app/myapp")  // Run executable from /app
437    ///     .args(["--config", "config.json"]);  // Config file will be looked up in /app
438    /// ```
439    ///
440    /// ## Notes
441    /// - The path must be absolute
442    /// - The directory must exist in the guest filesystem
443    /// - Useful for applications that need to access files relative to their location
444    pub fn workdir_path(mut self, workdir_path: impl Into<Utf8UnixPathBuf>) -> Self {
445        self.workdir_path = Some(workdir_path.into());
446        self
447    }
448
449    /// Sets the path to the executable to run in the MicroVm.
450    ///
451    /// This specifies the program that will be executed when the MicroVm starts.
452    ///
453    /// ## Examples
454    ///
455    /// ```rust
456    /// use microsandbox_core::vm::MicroVmConfigBuilder;
457    ///
458    /// let config = MicroVmConfigBuilder::default()
459    ///     .exec_path("/usr/local/bin/nginx")  // Run nginx web server
460    ///     .args(["-c", "/etc/nginx/nginx.conf"]);  // With specific config
461    /// ```
462    ///
463    /// ## Notes
464    /// - The path must be absolute
465    /// - The executable must exist and be executable in the guest filesystem
466    /// - The path is relative to the guest's root filesystem
467    pub fn exec_path(
468        self,
469        exec_path: impl Into<Utf8UnixPathBuf>,
470    ) -> MicroVmConfigBuilder<R, Utf8UnixPathBuf> {
471        MicroVmConfigBuilder {
472            log_level: self.log_level,
473            rootfs: self.rootfs,
474            num_vcpus: self.num_vcpus,
475            memory_mib: self.memory_mib,
476            mapped_dirs: self.mapped_dirs,
477            port_map: self.port_map,
478            scope: self.scope,
479            ip: self.ip,
480            subnet: self.subnet,
481            rlimits: self.rlimits,
482            workdir_path: self.workdir_path,
483            exec_path: exec_path.into(),
484            args: self.args,
485            env: self.env,
486            console_output: self.console_output,
487        }
488    }
489
490    /// Sets the command-line arguments for the executable.
491    ///
492    /// These arguments will be passed to the program specified by `exec_path`.
493    ///
494    /// ## Examples
495    ///
496    /// ```rust
497    /// use microsandbox_core::vm::MicroVmConfigBuilder;
498    ///
499    /// let config = MicroVmConfigBuilder::default()
500    ///     .exec_path("/usr/bin/python3")
501    ///     .args([
502    ///         "-m", "http.server",  // Run Python's HTTP server module
503    ///         "8080",               // Listen on port 8080
504    ///         "--directory", "/data" // Serve files from /data
505    ///     ]);
506    /// ```
507    ///
508    /// ## Notes
509    /// - Arguments are passed in the order they appear in the iterator
510    /// - The program name (argv[0]) is automatically set from exec_path
511    /// - Each argument should be a separate string
512    pub fn args<'a>(mut self, args: impl IntoIterator<Item = &'a str>) -> Self {
513        self.args = args.into_iter().map(|s| s.to_string()).collect();
514        self
515    }
516
517    /// Sets environment variables for processes in the MicroVm.
518    ///
519    /// Environment variables follow the standard format `KEY=VALUE` and are available
520    /// to all processes in the guest system.
521    ///
522    /// ## Examples
523    ///
524    /// ```rust
525    /// use microsandbox_core::vm::MicroVmConfigBuilder;
526    ///
527    /// # fn main() -> anyhow::Result<()> {
528    /// let config = MicroVmConfigBuilder::default()
529    ///     .env([
530    ///         // Set application environment
531    ///         "APP_ENV=production".parse()?,
532    ///         // Configure logging
533    ///         "LOG_LEVEL=info".parse()?,
534    ///         // Set timezone
535    ///         "TZ=UTC".parse()?,
536    ///         // Multiple values are OK
537    ///         "ALLOWED_HOSTS=localhost,127.0.0.1".parse()?
538    ///     ]);
539    /// # Ok(())
540    /// # }
541    /// ```
542    ///
543    /// ## Notes
544    /// - Variables are available to all processes in the guest
545    /// - Values should be properly escaped if they contain special characters
546    /// - Common uses include configuration and runtime settings
547    /// - Some programs expect specific environment variables to function
548    pub fn env(mut self, env: impl IntoIterator<Item = EnvPair>) -> Self {
549        self.env = env.into_iter().collect();
550        self
551    }
552
553    /// Sets the path for capturing console output from the MicroVm.
554    ///
555    /// This allows redirecting and saving all console output (stdout/stderr) from
556    /// the guest system to a file on the host.
557    ///
558    /// ## Examples
559    ///
560    /// ```rust
561    /// use microsandbox_core::vm::MicroVmConfigBuilder;
562    ///
563    /// let config = MicroVmConfigBuilder::default()
564    ///     .console_output("/var/log/microvm.log")  // Save output to log file
565    ///     .exec_path("/usr/local/bin/myapp");      // Run application
566    /// ```
567    ///
568    /// ## Notes
569    /// - The path must be writable on the host system
570    /// - The file will be created if it doesn't exist
571    /// - Useful for debugging and logging
572    /// - Captures both stdout and stderr
573    pub fn console_output(mut self, console_output: impl Into<Utf8UnixPathBuf>) -> Self {
574        self.console_output = Some(console_output.into());
575        self
576    }
577}
578
579impl<R, M> MicroVmBuilder<R, M> {
580    /// Sets the log level for the MicroVm.
581    ///
582    /// The log level controls the verbosity of the MicroVm's logging output.
583    ///
584    /// ## Examples
585    ///
586    /// ```rust
587    /// use microsandbox_core::vm::{LogLevel, MicroVmBuilder, Rootfs};
588    /// use tempfile::TempDir;
589    ///
590    /// # fn main() -> anyhow::Result<()> {
591    /// let temp_dir = TempDir::new()?;
592    /// let vm = MicroVmBuilder::default()
593    ///     .log_level(LogLevel::Debug)  // Enable debug logging
594    ///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
595    ///     .memory_mib(1024)
596    ///     .exec_path("/bin/echo")
597    ///     .build()?;
598    /// # Ok(())
599    /// # }
600    /// ```
601    ///
602    /// ## Log Levels
603    /// - `Off` - No logging (default)
604    /// - `Error` - Only error messages
605    /// - `Warn` - Warnings and errors
606    /// - `Info` - Informational messages, warnings, and errors
607    /// - `Debug` - Debug information and all above
608    /// - `Trace` - Detailed trace information and all above
609    pub fn log_level(mut self, log_level: LogLevel) -> Self {
610        self.inner = self.inner.log_level(log_level);
611        self
612    }
613
614    /// Sets the root filesystem sharing mode for the MicroVm.
615    ///
616    /// This determines how the root filesystem is shared with the guest system, with two options:
617    /// - `Rootfs::Native`: Direct passthrough of a directory as the root filesystem
618    /// - `Rootfs::Overlayfs`: Use overlayfs with multiple layers as the root filesystem
619    ///
620    /// ## Examples
621    ///
622    /// ```rust
623    /// use microsandbox_core::vm::{MicroVmBuilder, Rootfs};
624    /// use std::path::PathBuf;
625    ///
626    /// # fn main() -> anyhow::Result<()> {
627    /// // Option 1: Direct passthrough
628    /// let vm = MicroVmBuilder::default()
629    ///     .rootfs(Rootfs::Native(PathBuf::from("/path/to/rootfs")));
630    ///
631    /// // Option 2: Overlayfs with layers
632    /// let vm = MicroVmBuilder::default()
633    ///     .rootfs(Rootfs::Overlayfs(vec![
634    ///         PathBuf::from("/path/to/layer1"),
635    ///         PathBuf::from("/path/to/layer2")
636    ///     ]));
637    /// # Ok(())
638    /// # }
639    /// ```
640    ///
641    /// ## Notes
642    /// - For Passthrough: The directory must exist and contain a valid root filesystem structure
643    /// - For Overlayfs: The layers are stacked in order, with later layers taking precedence
644    /// - Common choices include Alpine Linux or Ubuntu root filesystems
645    /// - This is a required field - the build will fail if not set
646    pub fn rootfs(self, rootfs: Rootfs) -> MicroVmBuilder<Rootfs, M> {
647        MicroVmBuilder {
648            inner: self.inner.rootfs(rootfs),
649        }
650    }
651
652    /// Sets the number of virtual CPUs (vCPUs) for the MicroVm.
653    ///
654    /// This determines how many CPU cores are available to the guest system.
655    ///
656    /// ## Examples
657    ///
658    /// ```rust
659    /// use microsandbox_core::vm::{MicroVmBuilder, Rootfs};
660    /// use tempfile::TempDir;
661    ///
662    /// # fn main() -> anyhow::Result<()> {
663    /// let temp_dir = TempDir::new()?;
664    /// let vm = MicroVmBuilder::default()
665    ///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
666    ///     .memory_mib(1024)
667    ///     .num_vcpus(2)  // Allocate 2 virtual CPU cores
668    ///     .exec_path("/bin/echo")
669    ///     .build()?;
670    /// # Ok(())
671    /// # }
672    /// ```
673    ///
674    /// ## Notes
675    /// - The default is 1 vCPU if not specified
676    /// - More vCPUs aren't always better - consider the workload's needs
677    pub fn num_vcpus(mut self, num_vcpus: u8) -> Self {
678        self.inner = self.inner.num_vcpus(num_vcpus);
679        self
680    }
681
682    /// Sets the amount of memory in MiB for the MicroVm.
683    ///
684    /// This determines how much memory is available to the guest system.
685    ///
686    /// ## Examples
687    ///
688    /// ```rust
689    /// use microsandbox_core::vm::{MicroVmBuilder, Rootfs};
690    /// use tempfile::TempDir;
691    ///
692    /// # fn main() -> anyhow::Result<()> {
693    /// let temp_dir = TempDir::new()?;
694    /// let vm = MicroVmBuilder::default()
695    ///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
696    ///     .memory_mib(1024)  // Allocate 1 GiB of memory
697    ///     .exec_path("/bin/echo")
698    ///     .build()?;
699    /// # Ok(())
700    /// # }
701    /// ```
702    ///
703    /// ## Notes
704    /// - The value is in MiB (1 GiB = 1024 MiB)
705    /// - Consider the host's available memory when setting this value
706    /// - This is a required field - the build will fail if not set
707    pub fn memory_mib(mut self, memory_mib: u32) -> Self {
708        self.inner = self.inner.memory_mib(memory_mib);
709        self
710    }
711
712    /// Sets the directory mappings for the MicroVm using virtio-fs.
713    ///
714    /// Each mapping follows Docker's volume mapping convention using the format `host:guest`.
715    ///
716    /// ## Examples
717    ///
718    /// ```rust
719    /// use microsandbox_core::vm::MicroVmConfigBuilder;
720    ///
721    /// # fn main() -> anyhow::Result<()> {
722    /// let config = MicroVmConfigBuilder::default()
723    ///     .mapped_dirs([
724    ///         // Share host's /data directory as /mnt/data in guest
725    ///         "/data:/mnt/data".parse()?,
726    ///         // Share current directory as /app in guest
727    ///         "./:/app".parse()?,
728    ///         // Use same path in both host and guest
729    ///         "/shared".parse()?
730    ///     ]);
731    /// # Ok(())
732    /// # }
733    /// ```
734    pub fn mapped_dirs(mut self, mapped_dirs: impl IntoIterator<Item = PathPair>) -> Self {
735        self.inner = self.inner.mapped_dirs(mapped_dirs);
736        self
737    }
738
739    /// Sets the port mappings between host and guest for the MicroVm.
740    ///
741    /// Port mappings follow Docker's convention using the format `host:guest`, where:
742    /// - `host` is the port number on the host machine
743    /// - `guest` is the port number inside the MicroVm
744    ///
745    /// ## Examples
746    ///
747    /// ```rust
748    /// use microsandbox_core::vm::MicroVmBuilder;
749    /// use microsandbox_core::config::PortPair;
750    ///
751    /// # fn main() -> anyhow::Result<()> {
752    /// let vm = MicroVmBuilder::default()
753    ///     .port_map([
754    ///         // Map host port 8080 to guest port 80 (for web server)
755    ///         "8080:80".parse()?,
756    ///         // Map host port 2222 to guest port 22 (for SSH)
757    ///         "2222:22".parse()?,
758    ///         // Use same port (3000) on both host and guest
759    ///         "3000".parse()?
760    ///     ]);
761    /// # Ok(())
762    /// # }
763    /// ```
764    ///
765    /// ## Notes
766    ///
767    /// - If you don't call this method, no ports will be mapped between host and guest
768    /// - The guest application will need to use the guest port number to listen for connections
769    /// - External connections should use the host port number to connect to the service
770    /// - Port mapping is not supported when using passt networking mode
771    pub fn port_map(mut self, port_map: impl IntoIterator<Item = PortPair>) -> Self {
772        self.inner = self.inner.port_map(port_map);
773        self
774    }
775
776    /// Sets the network scope for the MicroVm.
777    ///
778    /// The network scope controls the MicroVm's level of network isolation and connectivity.
779    ///
780    /// ## Examples
781    ///
782    /// ```rust
783    /// use microsandbox_core::vm::{MicroVmBuilder, Rootfs};
784    /// use microsandbox_core::config::NetworkScope;
785    /// use std::path::PathBuf;
786    ///
787    /// # fn main() -> anyhow::Result<()> {
788    /// let vm = MicroVmBuilder::default()
789    ///     .scope(NetworkScope::Public)  // Allow access to public networks
790    ///     .rootfs(Rootfs::Native(PathBuf::from("/path/to/rootfs")))
791    ///     .exec_path("/bin/echo");
792    /// # Ok(())
793    /// # }
794    /// ```
795    ///
796    /// ## Network Scope Options
797    /// - `None` - Sandboxes cannot communicate with any other sandboxes
798    /// - `Group` - Sandboxes can only communicate within their subnet (default)
799    /// - `Public` - Sandboxes can communicate with any other non-private address
800    /// - `Any` - Sandboxes can communicate with any address
801    pub fn scope(mut self, scope: NetworkScope) -> Self {
802        self.inner = self.inner.scope(scope);
803        self
804    }
805
806    /// Sets the IP address for the MicroVm.
807    ///
808    /// This sets a specific IPv4 address for the guest system's network interface.
809    ///
810    /// ## Examples
811    ///
812    /// ```rust
813    /// use microsandbox_core::vm::{MicroVmBuilder, Rootfs};
814    /// use std::path::PathBuf;
815    /// use std::net::Ipv4Addr;
816    ///
817    /// # fn main() -> anyhow::Result<()> {
818    /// let vm = MicroVmBuilder::default()
819    ///     .ip(Ipv4Addr::new(192, 168, 1, 100))  // Assign IP 192.168.1.100 to the MicroVm
820    ///     .rootfs(Rootfs::Native(PathBuf::from("/path/to/rootfs")))
821    ///     .exec_path("/bin/echo");
822    /// # Ok(())
823    /// # }
824    /// ```
825    ///
826    /// ## Notes
827    /// - The IP address should be within the subnet assigned to the MicroVm
828    /// - If not specified, an IP address may be assigned automatically
829    pub fn ip(mut self, ip: Ipv4Addr) -> Self {
830        self.inner = self.inner.ip(ip);
831        self
832    }
833
834    /// Sets the subnet for the MicroVm.
835    ///
836    /// This defines the IPv4 network and mask for the guest system's network interface.
837    ///
838    /// ## Examples
839    ///
840    /// ```rust
841    /// use microsandbox_core::vm::{MicroVmBuilder, Rootfs};
842    /// use std::path::PathBuf;
843    /// use ipnetwork::Ipv4Network;
844    ///
845    /// # fn main() -> anyhow::Result<()> {
846    /// let vm = MicroVmBuilder::default()
847    ///     .subnet("192.168.1.0/24".parse()?)  // Set subnet to 192.168.1.0/24
848    ///     .rootfs(Rootfs::Native(PathBuf::from("/path/to/rootfs")))
849    ///     .exec_path("/bin/echo");
850    /// # Ok(())
851    /// # }
852    /// ```
853    ///
854    /// ## Notes
855    /// - The subnet defines the range of IP addresses available to the MicroVm
856    /// - Used for networking between multiple MicroVms in the same group
857    pub fn subnet(mut self, subnet: Ipv4Network) -> Self {
858        self.inner = self.inner.subnet(subnet);
859        self
860    }
861
862    /// Sets the resource limits for the MicroVm.
863    ///
864    /// ## Examples
865    ///
866    /// ```rust
867    /// use microsandbox_core::vm::MicroVmBuilder;
868    ///
869    /// # fn main() -> anyhow::Result<()> {
870    /// MicroVmBuilder::default().rlimits(["RLIMIT_NOFILE=1024:1024".parse()?]);
871    /// # Ok(())
872    /// # }
873    /// ```
874    pub fn rlimits(mut self, rlimits: impl IntoIterator<Item = LinuxRlimit>) -> Self {
875        self.inner = self.inner.rlimits(rlimits);
876        self
877    }
878
879    /// Sets the working directory path for the MicroVm.
880    ///
881    /// ## Examples
882    ///
883    /// ```rust
884    /// use microsandbox_core::vm::MicroVmBuilder;
885    ///
886    /// MicroVmBuilder::default().workdir_path("/path/to/workdir");
887    /// ```
888    pub fn workdir_path(mut self, workdir_path: impl Into<Utf8UnixPathBuf>) -> Self {
889        self.inner = self.inner.workdir_path(workdir_path);
890        self
891    }
892
893    /// Sets the executable path for the MicroVm.
894    ///
895    /// ## Examples
896    ///
897    /// ```rust
898    /// use microsandbox_core::vm::MicroVmBuilder;
899    ///
900    /// MicroVmBuilder::default().exec_path("/path/to/exec");
901    /// ```
902    pub fn exec_path(
903        self,
904        exec_path: impl Into<Utf8UnixPathBuf>,
905    ) -> MicroVmBuilder<R, Utf8UnixPathBuf> {
906        MicroVmBuilder {
907            inner: self.inner.exec_path(exec_path),
908        }
909    }
910
911    /// Sets the command-line arguments for the executable.
912    ///
913    /// These arguments will be passed to the program specified by `exec_path`.
914    ///
915    /// ## Examples
916    ///
917    /// ```rust
918    /// use microsandbox_core::vm::MicroVmBuilder;
919    ///
920    /// # fn main() -> anyhow::Result<()> {
921    /// let vm = MicroVmBuilder::default()
922    ///     .args([
923    ///         "-m", "http.server",  // Run Python's HTTP server module
924    ///         "8080",               // Listen on port 8080
925    ///         "--directory", "/data" // Serve files from /data
926    ///     ]);
927    /// # Ok(())
928    /// # }
929    /// ```
930    ///
931    /// ## Notes
932    /// - Arguments are passed in the order they appear in the iterator
933    /// - The program name (argv[0]) is automatically set from exec_path
934    /// - Each argument should be a separate string
935    pub fn args<'a>(mut self, args: impl IntoIterator<Item = &'a str>) -> Self {
936        self.inner = self.inner.args(args);
937        self
938    }
939
940    /// Sets environment variables for processes in the MicroVm.
941    ///
942    /// Environment variables follow the standard format `KEY=VALUE` and are available
943    /// to all processes in the guest system.
944    ///
945    /// ## Examples
946    ///
947    /// ```rust
948    /// use microsandbox_core::vm::MicroVmBuilder;
949    ///
950    /// # fn main() -> anyhow::Result<()> {
951    /// let vm = MicroVmBuilder::default()
952    ///     .env([
953    ///         // Set application environment
954    ///         "APP_ENV=production".parse()?,
955    ///         // Configure logging
956    ///         "LOG_LEVEL=info".parse()?,
957    ///         // Set timezone
958    ///         "TZ=UTC".parse()?,
959    ///         // Multiple values are OK
960    ///         "ALLOWED_HOSTS=localhost,127.0.0.1".parse()?
961    ///     ]);
962    /// # Ok(())
963    /// # }
964    /// ```
965    ///
966    /// ## Notes
967    /// - Variables are available to all processes in the guest
968    /// - Values should be properly escaped if they contain special characters
969    /// - Common uses include configuration and runtime settings
970    /// - Some programs expect specific environment variables to function
971    pub fn env(mut self, env: impl IntoIterator<Item = EnvPair>) -> Self {
972        self.inner = self.inner.env(env);
973        self
974    }
975
976    /// Sets the path for capturing console output from the MicroVm.
977    ///
978    /// This allows redirecting and saving all console output (stdout/stderr) from
979    /// the guest system to a file on the host.
980    ///
981    /// ## Examples
982    ///
983    /// ```rust
984    /// use microsandbox_core::vm::MicroVmBuilder;
985    ///
986    /// # fn main() -> anyhow::Result<()> {
987    /// let vm = MicroVmBuilder::default()
988    ///     .console_output("/var/log/microvm.log")  // Save output to log file
989    ///     .exec_path("/usr/local/bin/myapp");      // Run application
990    /// # Ok(())
991    /// # }
992    /// ```
993    ///
994    /// ## Notes
995    /// - The path must be writable on the host system
996    /// - The file will be created if it doesn't exist
997    /// - Useful for debugging and logging
998    /// - Captures both stdout and stderr
999    pub fn console_output(mut self, console_output: impl Into<Utf8UnixPathBuf>) -> Self {
1000        self.inner = self.inner.console_output(console_output);
1001        self
1002    }
1003}
1004
1005impl MicroVmConfigBuilder<Rootfs, Utf8UnixPathBuf> {
1006    /// Builds the MicroVm configuration.
1007    pub fn build(self) -> MicroVmConfig {
1008        MicroVmConfig {
1009            log_level: self.log_level,
1010            rootfs: self.rootfs,
1011            num_vcpus: self.num_vcpus,
1012            memory_mib: self.memory_mib,
1013            mapped_dirs: self.mapped_dirs,
1014            port_map: self.port_map,
1015            scope: self.scope,
1016            ip: self.ip,
1017            subnet: self.subnet,
1018            rlimits: self.rlimits,
1019            workdir_path: self.workdir_path,
1020            exec_path: self.exec_path,
1021            args: self.args,
1022            env: self.env,
1023            console_output: self.console_output,
1024        }
1025    }
1026}
1027
1028impl MicroVmBuilder<Rootfs, Utf8UnixPathBuf> {
1029    /// Builds the MicroVm.
1030    ///
1031    /// This method creates a `MicroVm` instance based on the configuration set in the builder.
1032    /// The MicroVm will be ready to start but won't be running until you call `start()`.
1033    ///
1034    /// ## Examples
1035    ///
1036    /// ```no_run
1037    /// use microsandbox_core::vm::{MicroVmBuilder, Rootfs};
1038    /// use tempfile::TempDir;
1039    ///
1040    /// # fn main() -> anyhow::Result<()> {
1041    /// let temp_dir = TempDir::new()?;
1042    /// let vm = MicroVmBuilder::default()
1043    ///     .rootfs(Rootfs::Native(temp_dir.path().to_path_buf()))
1044    ///     .memory_mib(1024)
1045    ///     .exec_path("/usr/bin/python3")
1046    ///     .args(["-c", "print('Hello from MicroVm!')"])
1047    ///     .build()?;
1048    ///
1049    /// // Start the MicroVm
1050    /// vm.start()?;  // This would actually run the VM
1051    /// # Ok(())
1052    /// # }
1053    /// ```
1054    ///
1055    /// ## Notes
1056    /// - The build will fail if required configuration is missing
1057    /// - The build will fail if the root path doesn't exist
1058    /// - The build will fail if memory value is invalid
1059    /// - After building, use `start()` to run the MicroVm
1060    pub fn build(self) -> MicrosandboxResult<MicroVm> {
1061        MicroVm::from_config(MicroVmConfig {
1062            log_level: self.inner.log_level,
1063            rootfs: self.inner.rootfs,
1064            num_vcpus: self.inner.num_vcpus,
1065            memory_mib: self.inner.memory_mib,
1066            mapped_dirs: self.inner.mapped_dirs,
1067            port_map: self.inner.port_map,
1068            scope: self.inner.scope,
1069            ip: self.inner.ip,
1070            subnet: self.inner.subnet,
1071            rlimits: self.inner.rlimits,
1072            workdir_path: self.inner.workdir_path,
1073            exec_path: self.inner.exec_path,
1074            args: self.inner.args,
1075            env: self.inner.env,
1076            console_output: self.inner.console_output,
1077        })
1078    }
1079}
1080
1081//--------------------------------------------------------------------------------------------------
1082// Trait Implementations
1083//--------------------------------------------------------------------------------------------------
1084
1085impl Default for MicroVmConfigBuilder<(), ()> {
1086    fn default() -> Self {
1087        Self {
1088            log_level: LogLevel::default(),
1089            rootfs: (),
1090            num_vcpus: DEFAULT_NUM_VCPUS,
1091            memory_mib: DEFAULT_MEMORY_MIB,
1092            mapped_dirs: vec![],
1093            port_map: vec![],
1094            scope: NetworkScope::default(),
1095            ip: None,
1096            subnet: None,
1097            rlimits: vec![],
1098            workdir_path: None,
1099            exec_path: (),
1100            args: vec![],
1101            env: vec![],
1102            console_output: None,
1103        }
1104    }
1105}
1106
1107impl Default for MicroVmBuilder<(), ()> {
1108    fn default() -> Self {
1109        Self {
1110            inner: MicroVmConfigBuilder::default(),
1111        }
1112    }
1113}
1114
1115//--------------------------------------------------------------------------------------------------
1116// Tests
1117//--------------------------------------------------------------------------------------------------
1118
1119#[cfg(test)]
1120mod tests {
1121    use std::path::PathBuf;
1122
1123    use super::*;
1124
1125    #[test]
1126    fn test_microvm_builder() -> anyhow::Result<()> {
1127        let rootfs = Rootfs::Overlayfs(vec![PathBuf::from("/tmp")]);
1128        let workdir_path = "/workdir";
1129        let exec_path = "/bin/example";
1130
1131        let builder = MicroVmBuilder::default()
1132            .log_level(LogLevel::Debug)
1133            .rootfs(rootfs.clone())
1134            .num_vcpus(2)
1135            .memory_mib(1024)
1136            .mapped_dirs(["/guest/mount:/host/mount".parse()?])
1137            .port_map(["8080:80".parse()?])
1138            .rlimits(["RLIMIT_NOFILE=1024:1024".parse()?])
1139            .workdir_path(workdir_path)
1140            .exec_path(exec_path)
1141            .args(["arg1", "arg2"])
1142            .env(["KEY1=VALUE1".parse()?, "KEY2=VALUE2".parse()?])
1143            .console_output("/tmp/console.log");
1144
1145        assert_eq!(builder.inner.log_level, LogLevel::Debug);
1146        assert_eq!(builder.inner.rootfs, rootfs);
1147        assert_eq!(builder.inner.num_vcpus, 2);
1148        assert_eq!(builder.inner.memory_mib, 1024);
1149        assert_eq!(
1150            builder.inner.mapped_dirs,
1151            ["/guest/mount:/host/mount".parse()?]
1152        );
1153        assert_eq!(builder.inner.port_map, ["8080:80".parse()?]);
1154        assert_eq!(builder.inner.rlimits, ["RLIMIT_NOFILE=1024:1024".parse()?]);
1155        assert_eq!(
1156            builder.inner.workdir_path,
1157            Some(Utf8UnixPathBuf::from(workdir_path))
1158        );
1159        assert_eq!(builder.inner.exec_path, Utf8UnixPathBuf::from(exec_path));
1160        assert_eq!(builder.inner.args, ["arg1", "arg2"]);
1161        assert_eq!(
1162            builder.inner.env,
1163            ["KEY1=VALUE1".parse()?, "KEY2=VALUE2".parse()?]
1164        );
1165        assert_eq!(
1166            builder.inner.console_output,
1167            Some(Utf8UnixPathBuf::from("/tmp/console.log"))
1168        );
1169        Ok(())
1170    }
1171
1172    #[test]
1173    fn test_microvm_builder_minimal() -> anyhow::Result<()> {
1174        let rootfs = Rootfs::Native(PathBuf::from("/tmp"));
1175        let memory_mib = 1024;
1176
1177        let builder = MicroVmBuilder::default()
1178            .rootfs(rootfs.clone())
1179            .exec_path("/bin/echo");
1180
1181        assert_eq!(builder.inner.rootfs, rootfs);
1182        assert_eq!(builder.inner.memory_mib, memory_mib);
1183
1184        // Check that other fields have default values
1185        assert_eq!(builder.inner.log_level, LogLevel::default());
1186        assert_eq!(builder.inner.num_vcpus, DEFAULT_NUM_VCPUS);
1187        assert_eq!(builder.inner.memory_mib, DEFAULT_MEMORY_MIB);
1188        assert!(builder.inner.mapped_dirs.is_empty());
1189        assert!(builder.inner.port_map.is_empty());
1190        assert!(builder.inner.rlimits.is_empty());
1191        assert_eq!(builder.inner.workdir_path, None);
1192        assert_eq!(builder.inner.exec_path, Utf8UnixPathBuf::from("/bin/echo"));
1193        assert!(builder.inner.args.is_empty());
1194        assert!(builder.inner.env.is_empty());
1195        assert_eq!(builder.inner.console_output, None);
1196        Ok(())
1197    }
1198}