@@ -10,7 +10,7 @@ use std::time::{Duration, Instant};
1010
1111use crate :: {
1212 error:: { VmRuntimeError , VmRuntimeResult } ,
13- model:: { VmStatus , VmView } ,
13+ model:: { NetworkInterface , RateLimiter , SnapshotRef , TokenBucket , VmSpec , VmStatus , VmView } ,
1414 provider:: { VmProvider , VmQuery } ,
1515} ;
1616
@@ -186,24 +186,31 @@ impl FirecrackerVmProvider {
186186 . collect ( )
187187 }
188188
189- fn ensure_prereqs ( & self ) -> VmRuntimeResult < ( ) > {
189+ fn ensure_prereqs ( & self , spec : & VmSpec ) -> VmRuntimeResult < ( ) > {
190190 if !self . config . binary_path . exists ( ) {
191191 return Err ( VmRuntimeError :: Unsupported ( format ! (
192192 "firecracker binary not found: {}" ,
193193 self . config. binary_path. display( )
194194 ) ) ) ;
195195 }
196- if !self . config . kernel_path . exists ( ) {
197- return Err ( VmRuntimeError :: Unsupported ( format ! (
198- "kernel image not found: {}" ,
199- self . config. kernel_path. display( )
200- ) ) ) ;
201- }
202- if !self . config . rootfs_path . exists ( ) {
203- return Err ( VmRuntimeError :: Unsupported ( format ! (
204- "rootfs image not found: {}" ,
205- self . config. rootfs_path. display( )
206- ) ) ) ;
196+ // Kernel + rootfs checks are skipped when restoring — the snapshot encodes its own
197+ // boot source. Cold boot validates the spec-resolved paths (overrides if set, else
198+ // the workspace default).
199+ if spec. restore_from . is_none ( ) {
200+ let kernel = spec. kernel . as_ref ( ) . unwrap_or ( & self . config . kernel_path ) ;
201+ if !kernel. exists ( ) {
202+ return Err ( VmRuntimeError :: Unsupported ( format ! (
203+ "kernel image not found: {}" ,
204+ kernel. display( )
205+ ) ) ) ;
206+ }
207+ let rootfs = spec. rootfs . as_ref ( ) . unwrap_or ( & self . config . rootfs_path ) ;
208+ if !rootfs. exists ( ) {
209+ return Err ( VmRuntimeError :: Unsupported ( format ! (
210+ "rootfs image not found: {}" ,
211+ rootfs. display( )
212+ ) ) ) ;
213+ }
207214 }
208215 fs:: create_dir_all ( & self . config . socket_dir ) . map_err ( |e| {
209216 VmRuntimeError :: Unsupported ( format ! (
@@ -281,28 +288,111 @@ impl FirecrackerVmProvider {
281288 ) ) )
282289 }
283290
284- fn configure_vm ( & self , socket_path : & Path ) -> VmRuntimeResult < ( ) > {
291+ fn configure_vm ( & self , socket_path : & Path , spec : & VmSpec ) -> VmRuntimeResult < ( ) > {
292+ let vcpu_count = spec. vcpu_count . unwrap_or ( self . config . vcpu_count ) ;
293+ let mem_size_mib = spec. mem_size_mib . unwrap_or ( self . config . mem_size_mib ) ;
294+ let track_dirty_pages = spec. track_dirty_pages . unwrap_or ( true ) ;
285295 let machine = serde_json:: json!( {
286- "vcpu_count" : self . config . vcpu_count,
287- "mem_size_mib" : self . config . mem_size_mib,
296+ "vcpu_count" : vcpu_count,
297+ "mem_size_mib" : mem_size_mib,
288298 "smt" : false ,
289- "track_dirty_pages" : true
299+ "track_dirty_pages" : track_dirty_pages
290300 } ) ;
291301 self . firecracker_request ( socket_path, "PUT" , "/machine-config" , Some ( machine) ) ?;
292302
303+ let kernel_path = spec. kernel . as_ref ( ) . unwrap_or ( & self . config . kernel_path ) ;
304+ let boot_args = spec. boot_args . as_deref ( ) . unwrap_or ( & self . config . boot_args ) ;
293305 let boot = serde_json:: json!( {
294- "kernel_image_path" : self . config . kernel_path,
295- "boot_args" : self . config . boot_args
306+ "kernel_image_path" : kernel_path,
307+ "boot_args" : boot_args
296308 } ) ;
297309 self . firecracker_request ( socket_path, "PUT" , "/boot-source" , Some ( boot) ) ?;
298310
299- let root_drive = serde_json:: json!( {
311+ let rootfs_path = spec. rootfs . as_ref ( ) . unwrap_or ( & self . config . rootfs_path ) ;
312+ let rootfs_read_only = spec
313+ . rootfs_read_only
314+ . unwrap_or ( self . config . rootfs_read_only ) ;
315+ let mut root_drive = serde_json:: json!( {
300316 "drive_id" : "rootfs" ,
301- "path_on_host" : self . config . rootfs_path,
317+ "path_on_host" : rootfs_path,
302318 "is_root_device" : true ,
303- "is_read_only" : self . config . rootfs_read_only
319+ "is_read_only" : rootfs_read_only
304320 } ) ;
321+ if let Some ( limiter) = spec. rootfs_rate_limit . as_ref ( ) {
322+ root_drive[ "rate_limiter" ] = rate_limiter_to_json ( limiter) ;
323+ }
305324 self . firecracker_request ( socket_path, "PUT" , "/drives/rootfs" , Some ( root_drive) ) ?;
325+
326+ for iface in & spec. network_interfaces {
327+ self . put_network_interface ( socket_path, iface) ?;
328+ }
329+
330+ Ok ( ( ) )
331+ }
332+
333+ fn put_network_interface (
334+ & self ,
335+ socket_path : & Path ,
336+ iface : & NetworkInterface ,
337+ ) -> VmRuntimeResult < ( ) > {
338+ let mut body = serde_json:: json!( {
339+ "iface_id" : iface. iface_id,
340+ "host_dev_name" : iface. host_dev_name,
341+ } ) ;
342+ if let Some ( mac) = & iface. guest_mac {
343+ body[ "guest_mac" ] = serde_json:: Value :: String ( mac. clone ( ) ) ;
344+ }
345+ if let Some ( rx) = & iface. rx_rate_limiter {
346+ body[ "rx_rate_limiter" ] = rate_limiter_to_json ( rx) ;
347+ }
348+ if let Some ( tx) = & iface. tx_rate_limiter {
349+ body[ "tx_rate_limiter" ] = rate_limiter_to_json ( tx) ;
350+ }
351+ let path = format ! ( "/network-interfaces/{}" , iface. iface_id) ;
352+ self . firecracker_request ( socket_path, "PUT" , & path, Some ( body) ) ?;
353+ Ok ( ( ) )
354+ }
355+
356+ fn load_snapshot ( & self , socket_path : & Path , snapshot : & SnapshotRef ) -> VmRuntimeResult < ( ) > {
357+ let source_state_dir = self . vm_state_path ( & snapshot. vm_id ) ;
358+ let snap_dir = source_state_dir. join ( "snapshots" ) ;
359+ let vmstate_path = snap_dir. join ( format ! ( "{}.vmstate" , snapshot. snapshot_id) ) ;
360+ let mem_path = snap_dir. join ( format ! ( "{}.mem" , snapshot. snapshot_id) ) ;
361+ if !vmstate_path. exists ( ) {
362+ return Err ( VmRuntimeError :: SnapshotNotFound {
363+ vm_id : snapshot. vm_id . clone ( ) ,
364+ snapshot_id : snapshot. snapshot_id . clone ( ) ,
365+ } ) ;
366+ }
367+
368+ let mut body = serde_json:: json!( {
369+ "snapshot_path" : vmstate_path,
370+ "mem_backend" : {
371+ "backend_type" : "File" ,
372+ "backend_path" : mem_path,
373+ } ,
374+ "enable_diff_snapshots" : false ,
375+ "resume_vm" : snapshot. resume_immediately,
376+ } ) ;
377+ if !snapshot. network_overrides . is_empty ( ) {
378+ let overrides: Vec < _ > = snapshot
379+ . network_overrides
380+ . iter ( )
381+ . map ( |iface| {
382+ let mut entry = serde_json:: json!( {
383+ "iface_id" : iface. iface_id,
384+ "host_dev_name" : iface. host_dev_name,
385+ } ) ;
386+ if let Some ( mac) = & iface. guest_mac {
387+ entry[ "guest_mac" ] = serde_json:: Value :: String ( mac. clone ( ) ) ;
388+ }
389+ entry
390+ } )
391+ . collect ( ) ;
392+ body[ "network_interfaces" ] = serde_json:: Value :: Array ( overrides) ;
393+ }
394+
395+ self . firecracker_request ( socket_path, "PUT" , "/snapshot/load" , Some ( body) ) ?;
306396 Ok ( ( ) )
307397 }
308398
@@ -455,24 +545,8 @@ impl FirecrackerVmProvider {
455545 Ok ( ( ) )
456546 }
457547
458- fn kill_process ( & self , vm_id : & str ) -> VmRuntimeResult < ( ) > {
459- let child = self
460- . processes
461- . lock ( )
462- . map_err ( |_| VmRuntimeError :: StatePoisoned ) ?
463- . remove ( vm_id) ;
464-
465- if let Some ( mut child) = child {
466- let _ = child. kill ( ) ;
467- let _ = child. wait ( ) ;
468- }
469- Ok ( ( ) )
470- }
471- }
472-
473- impl VmProvider for FirecrackerVmProvider {
474- fn create_vm ( & self , vm_id : & str ) -> VmRuntimeResult < ( ) > {
475- self . ensure_prereqs ( ) ?;
548+ fn create_vm_inner ( & self , vm_id : & str , spec : & VmSpec ) -> VmRuntimeResult < ( ) > {
549+ self . ensure_prereqs ( spec) ?;
476550
477551 {
478552 let state = self
@@ -494,13 +568,18 @@ impl VmProvider for FirecrackerVmProvider {
494568 } ) ?;
495569
496570 let mut child = self . spawn_firecracker ( vm_id, & socket_path) ?;
497- let create_result = ( || -> VmRuntimeResult < ( ) > {
571+ let restoring = spec. restore_from . is_some ( ) ;
572+ let configure_result = ( || -> VmRuntimeResult < ( ) > {
498573 self . wait_for_socket_ready ( & socket_path) ?;
499- self . configure_vm ( & socket_path) ?;
574+ if let Some ( snapshot) = spec. restore_from . as_ref ( ) {
575+ self . load_snapshot ( & socket_path, snapshot) ?;
576+ } else {
577+ self . configure_vm ( & socket_path, spec) ?;
578+ }
500579 Ok ( ( ) )
501580 } ) ( ) ;
502581
503- if let Err ( err) = create_result {
582+ if let Err ( err) = configure_result {
504583 let _ = child. kill ( ) ;
505584 let _ = child. wait ( ) ;
506585 return Err ( err) ;
@@ -511,13 +590,22 @@ impl VmProvider for FirecrackerVmProvider {
511590 . map_err ( |_| VmRuntimeError :: StatePoisoned ) ?
512591 . insert ( vm_id. to_owned ( ) , child) ;
513592
593+ // Restored VMs honour the snapshot's `resume_vm` flag — if `resume_immediately`
594+ // was set, the FC API call already transitioned the VM to Running; otherwise
595+ // it stays Paused/Stopped until an explicit start_vm.
596+ let initial_status = match ( restoring, spec. restore_from . as_ref ( ) ) {
597+ ( true , Some ( snap) ) if snap. resume_immediately => VmStatus :: Running ,
598+ ( true , _) => VmStatus :: Stopped ,
599+ ( false , _) => VmStatus :: Created ,
600+ } ;
601+
514602 self . state
515603 . write ( )
516604 . map_err ( |_| VmRuntimeError :: StatePoisoned ) ?
517605 . insert (
518606 vm_id. to_owned ( ) ,
519607 VmRecord {
520- status : VmStatus :: Created ,
608+ status : initial_status ,
521609 snapshots : Vec :: new ( ) ,
522610 socket_path,
523611 state_dir,
@@ -527,6 +615,30 @@ impl VmProvider for FirecrackerVmProvider {
527615 Ok ( ( ) )
528616 }
529617
618+ fn kill_process ( & self , vm_id : & str ) -> VmRuntimeResult < ( ) > {
619+ let child = self
620+ . processes
621+ . lock ( )
622+ . map_err ( |_| VmRuntimeError :: StatePoisoned ) ?
623+ . remove ( vm_id) ;
624+
625+ if let Some ( mut child) = child {
626+ let _ = child. kill ( ) ;
627+ let _ = child. wait ( ) ;
628+ }
629+ Ok ( ( ) )
630+ }
631+ }
632+
633+ impl VmProvider for FirecrackerVmProvider {
634+ fn create_vm ( & self , vm_id : & str ) -> VmRuntimeResult < ( ) > {
635+ self . create_vm_inner ( vm_id, & VmSpec :: default ( ) )
636+ }
637+
638+ fn create_vm_with_spec ( & self , vm_id : & str , spec : & VmSpec ) -> VmRuntimeResult < ( ) > {
639+ self . create_vm_inner ( vm_id, spec)
640+ }
641+
530642 fn start_vm ( & self , vm_id : & str ) -> VmRuntimeResult < ( ) > {
531643 let mut state = self
532644 . state
@@ -671,3 +783,80 @@ impl VmQuery for FirecrackerVmProvider {
671783 Ok ( state. get ( vm_id) . map ( |record| record. snapshots . clone ( ) ) )
672784 }
673785}
786+
787+ fn rate_limiter_to_json ( limiter : & RateLimiter ) -> serde_json:: Value {
788+ let mut obj = serde_json:: Map :: new ( ) ;
789+ if let Some ( bw) = & limiter. bandwidth {
790+ obj. insert ( "bandwidth" . into ( ) , token_bucket_to_json ( bw) ) ;
791+ }
792+ if let Some ( ops) = & limiter. ops {
793+ obj. insert ( "ops" . into ( ) , token_bucket_to_json ( ops) ) ;
794+ }
795+ serde_json:: Value :: Object ( obj)
796+ }
797+
798+ fn token_bucket_to_json ( bucket : & TokenBucket ) -> serde_json:: Value {
799+ serde_json:: json!( {
800+ "size" : bucket. size,
801+ "one_time_burst" : bucket. one_time_burst. unwrap_or( bucket. size) ,
802+ "refill_time" : bucket. refill_time_ms,
803+ } )
804+ }
805+
806+ #[ cfg( test) ]
807+ mod tests {
808+ use super :: * ;
809+ use crate :: model:: { RateLimiter , TokenBucket } ;
810+
811+ #[ test]
812+ fn token_bucket_default_burst_equals_size ( ) {
813+ let json = token_bucket_to_json ( & TokenBucket {
814+ size : 1_048_576 ,
815+ one_time_burst : None ,
816+ refill_time_ms : 1_000 ,
817+ } ) ;
818+ assert_eq ! ( json[ "size" ] , 1_048_576 ) ;
819+ assert_eq ! ( json[ "one_time_burst" ] , 1_048_576 ) ;
820+ assert_eq ! ( json[ "refill_time" ] , 1_000 ) ;
821+ }
822+
823+ #[ test]
824+ fn token_bucket_explicit_burst_respected ( ) {
825+ let json = token_bucket_to_json ( & TokenBucket {
826+ size : 1_048_576 ,
827+ one_time_burst : Some ( 2_097_152 ) ,
828+ refill_time_ms : 500 ,
829+ } ) ;
830+ assert_eq ! ( json[ "one_time_burst" ] , 2_097_152 ) ;
831+ }
832+
833+ #[ test]
834+ fn rate_limiter_serialises_both_buckets ( ) {
835+ let json = rate_limiter_to_json ( & RateLimiter {
836+ bandwidth : Some ( TokenBucket {
837+ size : 10_000 ,
838+ one_time_burst : None ,
839+ refill_time_ms : 100 ,
840+ } ) ,
841+ ops : Some ( TokenBucket {
842+ size : 50 ,
843+ one_time_burst : None ,
844+ refill_time_ms : 100 ,
845+ } ) ,
846+ } ) ;
847+ assert ! ( json. get( "bandwidth" ) . is_some( ) ) ;
848+ assert ! ( json. get( "ops" ) . is_some( ) ) ;
849+ assert_eq ! ( json[ "bandwidth" ] [ "size" ] , 10_000 ) ;
850+ assert_eq ! ( json[ "ops" ] [ "size" ] , 50 ) ;
851+ }
852+
853+ #[ test]
854+ fn rate_limiter_empty_serialises_to_empty_object ( ) {
855+ let json = rate_limiter_to_json ( & RateLimiter {
856+ bandwidth : None ,
857+ ops : None ,
858+ } ) ;
859+ assert ! ( json. is_object( ) ) ;
860+ assert ! ( json. as_object( ) . unwrap( ) . is_empty( ) ) ;
861+ }
862+ }
0 commit comments