diff --git a/README.md b/README.md index 0d6bfa6..424611b 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Commands: serve Start fireup HTTP API server inspect Inspect the Firecracker MicroVM details exec Execute a command inside the Firecracker MicroVM + cp Copy files to/from the Firecracker MicroVM help Print this message or the help of the given subcommand(s) Options: diff --git a/crates/firecracker-up/src/cmd/cp.rs b/crates/firecracker-up/src/cmd/cp.rs index 8b13789..c701d49 100644 --- a/crates/firecracker-up/src/cmd/cp.rs +++ b/crates/firecracker-up/src/cmd/cp.rs @@ -1 +1,78 @@ +use anyhow::Error; +use owo_colors::OwoColorize; +use crate::{command::run_command_with_stdout_inherit, ssh::get_private_key_path}; + +pub async fn cp(from: &str, to: &str) -> Result<(), Error> { + let pool = firecracker_state::create_connection_pool().await?; + + let vm_name = if from.contains(':') { + from.split(':').next().unwrap_or("") + } else if to.contains(':') { + to.split(':').next().unwrap_or("") + } else { + return Err(anyhow::anyhow!( + "Either source or destination must be in the format :" + )); + }; + + let vm = firecracker_state::repo::virtual_machine::find(&pool, vm_name).await?; + + if vm.is_none() { + println!("[-] MicroVM '{}' not found.", vm_name); + std::process::exit(1); + } + + if !firecracker_process::vm_is_running(vm_name).await? { + println!("[-] MicroVM '{}' is not running.", vm_name); + let start_cmd = format!("fireup start {}", vm_name); + println!(" Start it with {}", start_cmd.cyan()); + std::process::exit(1); + } + + let guest_ip = format!("{}.firecracker", vm_name); + let key_path = get_private_key_path()?; + + let scp_args = if from.contains(':') { + let remote_path = format!( + "root@{}:{}", + guest_ip, + from.splitn(2, ':').nth(1).unwrap() + ); + vec!["-r", remote_path.as_str(), to] + .iter() + .map(|s| s.to_string()) + .collect::>() + } else { + let remote_path = format!( + "root@{}:{}", + guest_ip, + to.splitn(2, ':').nth(1).unwrap() + ); + vec!["-r", from, remote_path.as_str()] + .iter() + .map(|s| s.to_string()) + .collect::>() + }; + + run_command_with_stdout_inherit( + "scp", + &[ + "-q", + "-i", + &key_path, + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + ] + .iter() + .copied() + .chain(scp_args.iter().map(|s| s.as_str())) + .collect::>() + .as_slice(), + false, + )?; + + Ok(()) +} diff --git a/crates/firecracker-up/src/cmd/exec.rs b/crates/firecracker-up/src/cmd/exec.rs index c9d2b50..3b8cc30 100644 --- a/crates/firecracker-up/src/cmd/exec.rs +++ b/crates/firecracker-up/src/cmd/exec.rs @@ -1,9 +1,8 @@ -use anyhow::{anyhow, Context, Error, Result}; +use anyhow::{Error, Result}; use firecracker_state::repo; use owo_colors::OwoColorize; -use std::fs; -use crate::command::run_ssh_command; +use crate::{command::run_ssh_command, ssh::get_private_key_path}; pub async fn exec(name: &str, args: Vec) -> Result<(), Error> { let pool = firecracker_state::create_connection_pool().await?; @@ -26,23 +25,3 @@ pub async fn exec(name: &str, args: Vec) -> Result<(), Error> { Ok(()) } - -fn get_private_key_path() -> Result { - let home_dir = dirs::home_dir().ok_or_else(|| anyhow!("Failed to get home directory"))?; - let app_dir = format!("{}/.fireup", home_dir.display()); - let key_name = glob::glob(format!("{}/id_rsa", app_dir).as_str()) - .with_context(|| "Failed to glob ssh key files")? - .last() - .ok_or_else(|| anyhow!("No SSH key file found"))? - .with_context(|| "Failed to get SSH key path")?; - let key_name = fs::canonicalize(&key_name) - .with_context(|| { - format!( - "Failed to resolve absolute path for SSH key: {}", - key_name.display() - ) - })? - .display() - .to_string(); - Ok(key_name) -} diff --git a/crates/firecracker-up/src/main.rs b/crates/firecracker-up/src/main.rs index df4b66b..6e7de57 100644 --- a/crates/firecracker-up/src/main.rs +++ b/crates/firecracker-up/src/main.rs @@ -4,15 +4,16 @@ use firecracker_vm::{constants::BRIDGE_DEV, mac::generate_unique_mac, types::VmO use owo_colors::OwoColorize; use crate::cmd::{ - down::down, init::init, inspect::inspect_microvm, logs::logs, ps::list_all_instances, - reset::reset, rm::remove, serve::serve, ssh::ssh, start::start, status::status, stop::stop, - up::up, + cp::cp, down::down, exec::exec, init::init, inspect::inspect_microvm, logs::logs, + ps::list_all_instances, reset::reset, rm::remove, serve::serve, ssh::ssh, start::start, + status::status, stop::stop, up::up, }; pub mod cmd; pub mod command; pub mod config; pub mod date; +pub mod ssh; fn cli() -> Command { let banner = format!( @@ -180,6 +181,12 @@ fn cli() -> Command { ) .about("Execute a command inside the Firecracker MicroVM"), ) + .subcommand( + Command::new("cp") + .arg(arg!( "Source file path").required(true)) + .arg(arg!( "Destination file path").required(true)) + .about("Copy files to/from the Firecracker MicroVM"), + ) .arg(arg!(--debian "Prepare Debian MicroVM").default_value("false")) .arg(arg!(--alpine "Prepare Alpine MicroVM").default_value("false")) .arg(arg!(--nixos "Prepare NixOS MicroVM").default_value("false")) @@ -362,7 +369,12 @@ async fn main() -> Result<()> { .unwrap() .map(|s| s.to_string()) .collect(); - cmd::exec::exec(&name, cmd_args).await?; + exec(&name, cmd_args).await?; + } + Some(("cp", args)) => { + let source = args.get_one::("source").cloned().unwrap(); + let destination = args.get_one::("destination").cloned().unwrap(); + cp(&source, &destination).await?; } _ => { let debian = matches.get_one::("debian").copied().unwrap_or(false); diff --git a/crates/firecracker-up/src/ssh.rs b/crates/firecracker-up/src/ssh.rs new file mode 100644 index 0000000..b776173 --- /dev/null +++ b/crates/firecracker-up/src/ssh.rs @@ -0,0 +1,23 @@ +use anyhow::Error; +use anyhow::{anyhow, Context, Result}; +use std::fs; + +pub fn get_private_key_path() -> Result { + let home_dir = dirs::home_dir().ok_or_else(|| anyhow!("Failed to get home directory"))?; + let app_dir = format!("{}/.fireup", home_dir.display()); + let key_name = glob::glob(format!("{}/id_rsa", app_dir).as_str()) + .with_context(|| "Failed to glob ssh key files")? + .last() + .ok_or_else(|| anyhow!("No SSH key file found"))? + .with_context(|| "Failed to get SSH key path")?; + let key_name = fs::canonicalize(&key_name) + .with_context(|| { + format!( + "Failed to resolve absolute path for SSH key: {}", + key_name.display() + ) + })? + .display() + .to_string(); + Ok(key_name) +} -- 2.51.2 From ddf31755bd11a7edbf87802a2d773ff26053ba32 Mon Sep 17 00:00:00 2001 From: Tsiry Sandratraina Date: Fri, 19 Sep 2025 21:25:14 +0300 Subject: [PATCH 2/2] run cargo fmt --- crates/firecracker-up/src/cmd/cp.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/firecracker-up/src/cmd/cp.rs b/crates/firecracker-up/src/cmd/cp.rs index c701d49..1f013e4 100644 --- a/crates/firecracker-up/src/cmd/cp.rs +++ b/crates/firecracker-up/src/cmd/cp.rs @@ -34,21 +34,13 @@ pub async fn cp(from: &str, to: &str) -> Result<(), Error> { let key_path = get_private_key_path()?; let scp_args = if from.contains(':') { - let remote_path = format!( - "root@{}:{}", - guest_ip, - from.splitn(2, ':').nth(1).unwrap() - ); + let remote_path = format!("root@{}:{}", guest_ip, from.splitn(2, ':').nth(1).unwrap()); vec!["-r", remote_path.as_str(), to] .iter() .map(|s| s.to_string()) .collect::>() } else { - let remote_path = format!( - "root@{}:{}", - guest_ip, - to.splitn(2, ':').nth(1).unwrap() - ); + let remote_path = format!("root@{}:{}", guest_ip, to.splitn(2, ':').nth(1).unwrap()); vec!["-r", from, remote_path.as_str()] .iter() .map(|s| s.to_string())