Skip to content

Commit 063e193

Browse files
AlexMikhalevclaude
andcommitted
feat(rlm): add SSH-based command execution for VMs
Implements execute_command() and execute_code() with SSH-based execution: - Add SshExecutor for remote command execution on VMs - Support for bash command execution - Support for Python code execution via here-doc - Environment variable injection and working directory support - Timeout handling with process killing - Output streaming to file for large outputs (exceeding max_output_bytes) - Shell escaping for safe command construction - Update FirecrackerExecutor to use SshExecutor - Session-to-VM affinity tracking - VM assignment/release methods for external allocation - Graceful fallback to stub when no VM is assigned - Export SshExecutor from lib.rs for external use This completes Phase 2 execute_command/execute_code implementation. Full VM pool integration requires terraphim_firecracker enhancements (GitHub issues #14-#19). 🤖 Generated with [Terraphim AI](https://terraphim.io) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 450baa5 commit 063e193

6 files changed

Lines changed: 607 additions & 31 deletions

File tree

Cargo.lock

Lines changed: 24 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/terraphim_rlm/src/executor/firecracker.rs

Lines changed: 126 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ use std::time::Instant;
2424

2525
use terraphim_firecracker::{PoolConfig, Sub2SecondOptimizer, VmPoolManager};
2626

27+
use super::ssh::SshExecutor;
2728
use super::{Capability, ExecutionContext, ExecutionResult, SnapshotId, ValidationResult};
2829
use crate::config::{BackendType, RlmConfig};
29-
use crate::error::RlmError;
30+
use crate::error::{RlmError, RlmResult};
3031
use crate::types::SessionId;
3132

3233
/// Firecracker execution backend.
@@ -40,11 +41,17 @@ pub struct FirecrackerExecutor {
4041
/// VM pool manager (will be initialized on first use).
4142
pool_manager: Option<Arc<VmPoolManager>>,
4243

44+
/// SSH executor for running commands on VMs.
45+
ssh_executor: SshExecutor,
46+
4347
/// Capabilities supported by this executor.
4448
capabilities: Vec<Capability>,
4549

4650
/// Active snapshots keyed by session.
4751
snapshots: parking_lot::RwLock<HashMap<SessionId, Vec<SnapshotId>>>,
52+
53+
/// Session to VM IP mapping for affinity.
54+
session_vms: parking_lot::RwLock<HashMap<SessionId, String>>,
4855
}
4956

5057
impl FirecrackerExecutor {
@@ -78,11 +85,18 @@ impl FirecrackerExecutor {
7885
Capability::FileOperations,
7986
];
8087

88+
// Configure SSH executor with sensible defaults for VM access
89+
let ssh_executor = SshExecutor::new()
90+
.with_user("root")
91+
.with_output_dir(std::env::temp_dir().join("terraphim_rlm_output"));
92+
8193
Ok(Self {
8294
config,
8395
pool_manager: None,
96+
ssh_executor,
8497
capabilities,
8598
snapshots: parking_lot::RwLock::new(HashMap::new()),
99+
session_vms: parking_lot::RwLock::new(HashMap::new()),
86100
})
87101
}
88102

@@ -102,7 +116,7 @@ impl FirecrackerExecutor {
102116
);
103117

104118
// Create pool configuration from RLM config
105-
let pool_config = PoolConfig {
119+
let _pool_config = PoolConfig {
106120
min_pool_size: self.config.pool_min_size,
107121
max_pool_size: self.config.pool_max_size,
108122
target_pool_size: self.config.pool_target_size,
@@ -112,7 +126,7 @@ impl FirecrackerExecutor {
112126

113127
// Create optimizer and VM manager
114128
// Note: This is a stub - actual implementation will create real VmManager
115-
let optimizer = Arc::new(Sub2SecondOptimizer::new());
129+
let _optimizer = Arc::new(Sub2SecondOptimizer::new());
116130

117131
// TODO: Create actual VmManager and VmPoolManager
118132
// For now, we'll return an error indicating initialization is incomplete
@@ -124,6 +138,53 @@ impl FirecrackerExecutor {
124138
})
125139
}
126140

141+
/// Get or allocate a VM for a session.
142+
///
143+
/// Returns the VM IP address if available, or None if no VM could be allocated.
144+
async fn get_or_allocate_vm(&self, session_id: &SessionId) -> RlmResult<Option<String>> {
145+
// Check if session already has an assigned VM
146+
{
147+
let session_vms = self.session_vms.read();
148+
if let Some(ip) = session_vms.get(session_id) {
149+
log::debug!("Using existing VM for session {}: {}", session_id, ip);
150+
return Ok(Some(ip.clone()));
151+
}
152+
}
153+
154+
// Try to allocate from pool
155+
// Note: Full pool integration requires terraphim_firecracker enhancements (GitHub #15)
156+
// For now, we check if pool_manager is initialized
157+
if self.pool_manager.is_some() {
158+
// Pool allocation would happen here
159+
// let (vm, _alloc_time) = self.pool_manager.as_ref().unwrap()
160+
// .allocate_vm("terraphim-minimal")
161+
// .await
162+
// .map_err(|e| RlmError::VmAllocationTimeout {
163+
// timeout_ms: self.config.allocation_timeout_ms,
164+
// })?;
165+
//
166+
// if let Some(ip) = vm.read().await.ip_address.clone() {
167+
// self.session_vms.write().insert(*session_id, ip.clone());
168+
// return Ok(Some(ip));
169+
// }
170+
log::debug!("VM pool available but allocation not yet implemented");
171+
}
172+
173+
log::debug!("No VM available for session {}", session_id);
174+
Ok(None)
175+
}
176+
177+
/// Assign a VM to a session (for external allocation).
178+
pub fn assign_vm_to_session(&self, session_id: SessionId, vm_ip: String) {
179+
log::info!("Assigning VM {} to session {}", vm_ip, session_id);
180+
self.session_vms.write().insert(session_id, vm_ip);
181+
}
182+
183+
/// Release VM assignment for a session.
184+
pub fn release_session_vm(&self, session_id: &SessionId) -> Option<String> {
185+
self.session_vms.write().remove(session_id)
186+
}
187+
127188
/// Execute code in a VM.
128189
async fn execute_in_vm(
129190
&self,
@@ -133,39 +194,78 @@ impl FirecrackerExecutor {
133194
) -> Result<ExecutionResult, RlmError> {
134195
let start = Instant::now();
135196

136-
// For now, return a stub result indicating the executor is not fully implemented
137197
log::debug!(
138198
"FirecrackerExecutor::execute_in_vm called (python={}, session={})",
139199
is_python,
140200
ctx.session_id
141201
);
142202

143-
// TODO: Implement actual VM execution:
144-
// 1. Allocate VM from pool (or use session-affinity VM)
145-
// 2. Copy code to VM via vsock or SSH
146-
// 3. Execute and capture output
147-
// 4. Handle timeout and cancellation
148-
// 5. Return result
203+
// Try to get a VM for this session
204+
let vm_ip = self.get_or_allocate_vm(&ctx.session_id).await?;
149205

150-
let execution_time = start.elapsed().as_millis() as u64;
206+
match vm_ip {
207+
Some(ref ip) => {
208+
// Execute via SSH on the allocated VM
209+
log::info!("Executing on VM {} for session {}", ip, ctx.session_id);
151210

152-
Ok(ExecutionResult {
153-
stdout: format!(
154-
"[FirecrackerExecutor stub] Would execute: {}",
155-
if code.len() > 100 {
156-
format!("{}...", &code[..100])
211+
let result = if is_python {
212+
self.ssh_executor.execute_python(ip, code, ctx).await
157213
} else {
158-
code.to_string()
214+
self.ssh_executor.execute_command(ip, code, ctx).await
215+
};
216+
217+
match result {
218+
Ok(mut res) => {
219+
// Add VM metadata
220+
res.metadata
221+
.insert("vm_ip".to_string(), ip.clone());
222+
res.metadata
223+
.insert("backend".to_string(), "firecracker".to_string());
224+
Ok(res)
225+
}
226+
Err(e) => {
227+
log::error!("VM execution failed: {}", e);
228+
Err(e)
229+
}
159230
}
160-
),
161-
stderr: String::new(),
162-
exit_code: 0,
163-
execution_time_ms: execution_time,
164-
output_truncated: false,
165-
output_file_path: None,
166-
timed_out: false,
167-
metadata: HashMap::new(),
168-
})
231+
}
232+
None => {
233+
// No VM available - return stub response indicating this
234+
// In production, this would be an error, but for development
235+
// we return a stub to allow testing without VMs
236+
log::warn!(
237+
"No VM available for execution (session={}), returning stub response",
238+
ctx.session_id
239+
);
240+
241+
let execution_time = start.elapsed().as_millis() as u64;
242+
243+
Ok(ExecutionResult {
244+
stdout: format!(
245+
"[FirecrackerExecutor] No VM available. Would execute: {}",
246+
if code.len() > 100 {
247+
format!("{}...", &code[..100])
248+
} else {
249+
code.to_string()
250+
}
251+
),
252+
stderr: "Warning: No VM allocated for this session. \
253+
Assign a VM using assign_vm_to_session() or ensure VM pool is initialized."
254+
.to_string(),
255+
exit_code: 0,
256+
execution_time_ms: execution_time,
257+
output_truncated: false,
258+
output_file_path: None,
259+
timed_out: false,
260+
metadata: {
261+
let mut m = HashMap::new();
262+
m.insert("stub".to_string(), "true".to_string());
263+
m.insert("backend".to_string(), "firecracker".to_string());
264+
m
265+
},
266+
})
267+
}
268+
}
169269
}
170270
}
171271

crates/terraphim_rlm/src/executor/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@
2121
2222
mod context;
2323
mod firecracker;
24+
mod ssh;
2425
mod r#trait;
2526

2627
pub use context::{Capability, ExecutionContext, ExecutionResult, SnapshotId, ValidationResult};
2728
pub use firecracker::FirecrackerExecutor;
29+
pub use ssh::SshExecutor;
2830
pub use r#trait::ExecutionEnvironment;
2931

3032
use crate::config::{BackendType, RlmConfig};

0 commit comments

Comments
 (0)