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}