Skip to content

Commit 3879d26

Browse files
committed
feat(vmm): added one-shot
1 parent 9316569 commit 3879d26

5 files changed

Lines changed: 1050 additions & 69 deletions

File tree

vmm/src/app/qemu.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ fn create_hd(
6868
image_file: impl AsRef<Path>,
6969
backing_file: Option<impl AsRef<Path>>,
7070
size: &str,
71+
work_dir: Option<impl AsRef<Path>>,
7172
) -> Result<()> {
7273
let mut command = Command::new("qemu-img");
7374
command.arg("create").arg("-f").arg("qcow2");
@@ -79,6 +80,11 @@ fn create_hd(
7980
}
8081
command.arg(image_file.as_ref());
8182
command.arg(size);
83+
84+
// Set working directory if provided
85+
if let Some(work_dir) = work_dir {
86+
command.current_dir(work_dir.as_ref());
87+
}
8288
let output = command.output()?;
8389
if !output.status.success() {
8490
bail!(
@@ -233,7 +239,12 @@ impl VmConfig {
233239
let disk_size = format!("{}G", self.manifest.disk_size);
234240
let hda_path = workdir.hda_path();
235241
if !hda_path.exists() {
236-
create_hd(&hda_path, self.image.hda.as_ref(), &disk_size)?;
242+
create_hd(
243+
&hda_path,
244+
self.image.hda.as_ref(),
245+
&disk_size,
246+
Some(&workdir),
247+
)?;
237248
}
238249
if !cfg.user.is_empty() {
239250
fs_err::set_permissions(&hda_path, Permissions::from_mode(0o660))?;

vmm/src/main.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ mod guest_api_service;
2424
mod host_api_service;
2525
mod main_routes;
2626
mod main_service;
27+
mod one_shot;
28+
mod validation;
2729

2830
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
2931
const GIT_REV: &str = git_version::git_version!(
@@ -42,6 +44,18 @@ struct Args {
4244
/// Path to the configuration file
4345
#[arg(short, long)]
4446
config: Option<String>,
47+
/// One-shot mode: setup VM and execute QEMU command from VM configuration file
48+
#[arg(long)]
49+
one_shot: Option<String>,
50+
/// Validate JSON configuration file without executing (useful for debugging)
51+
#[arg(long)]
52+
validate_only: Option<String>,
53+
/// Working directory for one-shot mode (default: create in current directory)
54+
#[arg(long)]
55+
workdir: Option<String>,
56+
/// Dry run: only output QEMU command without executing (use with --one-shot)
57+
#[arg(long)]
58+
dry_run: bool,
4559
}
4660

4761
async fn run_external_api(app: App, figment: Figment, api_auth: ApiToken) -> Result<()> {
@@ -130,6 +144,17 @@ async fn main() -> Result<()> {
130144
let args = Args::parse();
131145
let figment = config::load_config_figment(args.config.as_deref());
132146
let config = Config::extract_or_default(&figment)?.abs_path()?;
147+
148+
// Handle one-shot mode
149+
if let Some(vm_config_path) = args.one_shot {
150+
return one_shot::run_one_shot(&vm_config_path, config, args.workdir, args.dry_run).await;
151+
}
152+
153+
// Handle validate-only mode
154+
if let Some(vm_config_path) = args.validate_only {
155+
return validation::validate_config_file(&vm_config_path).await;
156+
}
157+
133158
let api_auth = ApiToken::new(config.auth.tokens.clone(), config.auth.enabled);
134159
let supervisor = {
135160
let cfg = &config.supervisor;

vmm/src/main_service.rs

Lines changed: 86 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,21 @@ fn validate_label(label: &str) -> Result<()> {
5858
Ok(())
5959
}
6060

61-
fn resolve_gpus(gpu_cfg: &rpc::GpuConfig) -> Result<GpuConfig> {
61+
pub fn resolve_gpus_with_config(
62+
gpu_cfg: &rpc::GpuConfig,
63+
cvm_config: &crate::config::CvmConfig,
64+
) -> Result<GpuConfig> {
65+
if !cvm_config.gpu.enabled {
66+
bail!("GPU is not enabled");
67+
}
68+
let gpus = resolve_gpus(gpu_cfg)?;
69+
if !cvm_config.gpu.allow_attach_all && gpus.attach_mode.is_all() {
70+
bail!("Attaching all GPUs is not allowed");
71+
}
72+
Ok(gpus)
73+
}
74+
75+
pub fn resolve_gpus(gpu_cfg: &rpc::GpuConfig) -> Result<GpuConfig> {
6276
// Check the attach mode to determine how to handle GPUs
6377
match gpu_cfg.attach_mode.as_str() {
6478
"listed" => {
@@ -106,80 +120,84 @@ fn resolve_gpus(gpu_cfg: &rpc::GpuConfig) -> Result<GpuConfig> {
106120
}
107121
}
108122

123+
// Shared function to create manifest from VM configuration
124+
pub fn create_manifest_from_vm_config(
125+
request: VmConfiguration,
126+
cvm_config: &crate::config::CvmConfig,
127+
) -> Result<Manifest> {
128+
validate_label(&request.name)?;
129+
130+
let pm_cfg = &cvm_config.port_mapping;
131+
if !(request.ports.is_empty() || pm_cfg.enabled) {
132+
bail!("Port mapping is disabled");
133+
}
134+
let port_map = request
135+
.ports
136+
.iter()
137+
.map(|p| {
138+
let from = p.host_port.try_into().context("Invalid host port")?;
139+
let to = p.vm_port.try_into().context("Invalid vm port")?;
140+
if !pm_cfg.is_allowed(&p.protocol, from) {
141+
bail!("Port mapping is not allowed for {}:{}", p.protocol, from);
142+
}
143+
let protocol = p.protocol.parse().context("Invalid protocol")?;
144+
let address = if !p.host_address.is_empty() {
145+
p.host_address.parse().context("Invalid host address")?
146+
} else {
147+
pm_cfg.address
148+
};
149+
Ok(PortMapping {
150+
address,
151+
protocol,
152+
from,
153+
to,
154+
})
155+
})
156+
.collect::<Result<Vec<_>>>()?;
157+
158+
let app_id = match &request.app_id {
159+
Some(id) => id.strip_prefix("0x").unwrap_or(id).to_lowercase(),
160+
None => app_id_of(&request.compose_file),
161+
};
162+
let id = uuid::Uuid::new_v4().to_string();
163+
let now = SystemTime::now()
164+
.duration_since(UNIX_EPOCH)
165+
.unwrap_or_default()
166+
.as_millis() as u64;
167+
let gpus = match &request.gpus {
168+
Some(gpus) => resolve_gpus_with_config(gpus, cvm_config)?,
169+
None => GpuConfig::default(),
170+
};
171+
172+
Ok(Manifest::builder()
173+
.id(id)
174+
.name(request.name.clone())
175+
.app_id(app_id)
176+
.image(request.image.clone())
177+
.vcpu(request.vcpu)
178+
.memory(request.memory)
179+
.disk_size(request.disk_size)
180+
.port_map(port_map)
181+
.created_at_ms(now)
182+
.hugepages(request.hugepages)
183+
.pin_numa(request.pin_numa)
184+
.gpus(gpus)
185+
.kms_urls(request.kms_urls.clone())
186+
.gateway_urls(request.gateway_urls.clone())
187+
.build())
188+
}
189+
109190
impl RpcHandler {
110191
fn resolve_gpus(&self, gpu_cfg: &rpc::GpuConfig) -> Result<GpuConfig> {
111-
let gpus = resolve_gpus(gpu_cfg)?;
112-
if !self.app.config.cvm.gpu.enabled {
113-
bail!("GPU is not enabled");
114-
}
115-
if !self.app.config.cvm.gpu.allow_attach_all && gpus.attach_mode.is_all() {
116-
bail!("Attaching all GPUs is not allowed");
117-
}
118-
Ok(gpus)
192+
resolve_gpus_with_config(gpu_cfg, &self.app.config.cvm)
119193
}
120194
}
121195

122196
impl VmmRpc for RpcHandler {
123197
async fn create_vm(self, request: VmConfiguration) -> Result<Id> {
124-
validate_label(&request.name)?;
125-
126-
let pm_cfg = &self.app.config.cvm.port_mapping;
127-
if !(request.ports.is_empty() || pm_cfg.enabled) {
128-
bail!("Port mapping is disabled");
129-
}
130-
let port_map = request
131-
.ports
132-
.iter()
133-
.map(|p| {
134-
let from = p.host_port.try_into().context("Invalid host port")?;
135-
let to = p.vm_port.try_into().context("Invalid vm port")?;
136-
if !pm_cfg.is_allowed(&p.protocol, from) {
137-
bail!("Port mapping is not allowed for {}:{}", p.protocol, from);
138-
}
139-
let protocol = p.protocol.parse().context("Invalid protocol")?;
140-
let address = if !p.host_address.is_empty() {
141-
p.host_address.parse().context("Invalid host address")?
142-
} else {
143-
pm_cfg.address
144-
};
145-
Ok(PortMapping {
146-
address,
147-
protocol,
148-
from,
149-
to,
150-
})
151-
})
152-
.collect::<Result<Vec<_>>>()?;
153-
154-
let app_id = match &request.app_id {
155-
Some(id) => id.strip_prefix("0x").unwrap_or(id).to_lowercase(),
156-
None => app_id_of(&request.compose_file),
157-
};
158-
let id = uuid::Uuid::new_v4().to_string();
159-
let now = SystemTime::now()
160-
.duration_since(UNIX_EPOCH)
161-
.unwrap_or_default()
162-
.as_millis() as u64;
163-
let gpus = match &request.gpus {
164-
Some(gpus) => self.resolve_gpus(gpus)?,
165-
None => GpuConfig::default(),
166-
};
167-
let manifest = Manifest::builder()
168-
.id(id.clone())
169-
.name(request.name.clone())
170-
.app_id(app_id.clone())
171-
.image(request.image.clone())
172-
.vcpu(request.vcpu)
173-
.memory(request.memory)
174-
.disk_size(request.disk_size)
175-
.port_map(port_map)
176-
.created_at_ms(now)
177-
.hugepages(request.hugepages)
178-
.pin_numa(request.pin_numa)
179-
.gpus(gpus)
180-
.kms_urls(request.kms_urls.clone())
181-
.gateway_urls(request.gateway_urls.clone())
182-
.build();
198+
let manifest = create_manifest_from_vm_config(request.clone(), &self.app.config.cvm)?;
199+
let id = manifest.id.clone();
200+
let app_id = manifest.app_id.clone();
183201
let vm_work_dir = self.app.work_dir(&id);
184202
vm_work_dir
185203
.put_manifest(&manifest)

0 commit comments

Comments
 (0)