1use std::process::Command;
7use anyhow::{Context, Result};
8use async_trait::async_trait;
9use tracing::{info, warn};
10use crate::compute::{ComputeBackend, ContainerConfig, NodeStatus};
11
12pub struct LxdBackend {
13 storage_pool: String,
14 network_device: String,
15}
16
17impl LxdBackend {
18 pub fn new(storage_pool: &str, network_device: &str) -> Self {
19 Self {
20 storage_pool: storage_pool.to_string(),
21 network_device: network_device.to_string(),
22 }
23 }
24
25 fn run_lxc(&self, args: &[&str]) -> Result<String> {
26 let output = Command::new("lxc")
27 .args(args)
28 .output()
29 .context("Failed to execute lxc command")?;
30
31 if !output.status.success() {
32 let stderr = String::from_utf8_lossy(&output.stderr);
33 return Err(anyhow::anyhow!("lxc command failed: {}", stderr));
34 }
35
36 Ok(String::from_utf8_lossy(&output.stdout).to_string())
37 }
38}
39
40#[async_trait]
41impl ComputeBackend for LxdBackend {
42 async fn find_available_id(&self, range_start: u32, range_end: u32) -> Result<u32> {
43 let output = self.run_lxc(&["list", "--format", "json"])?;
45 let containers: serde_json::Value = serde_json::from_str(&output)?;
46
47 let existing_ids: Vec<u32> = containers.as_array()
48 .unwrap_or(&vec![])
49 .iter()
50 .filter_map(|c| c.get("name").and_then(|n| n.as_str()))
51 .filter_map(|name| {
52 if name.starts_with("paygress-") {
53 name.replace("paygress-", "").parse::<u32>().ok()
54 } else {
55 None
56 }
57 })
58 .collect();
59
60 for id in range_start..=range_end {
61 if !existing_ids.contains(&id) {
62 return Ok(id);
63 }
64 }
65
66 Err(anyhow::anyhow!("No available IDs in range {}-{}", range_start, range_end))
67 }
68
69 async fn create_container(&self, config: &ContainerConfig) -> Result<String> {
70 let name = format!("paygress-{}", config.id);
71
72 let image = match config.image.as_str() {
75 "alpine" => "images:alpine/3.19",
76 "ubuntu" => "ubuntu:22.04", other => other,
78 };
79
80 info!("Creating LXD container {} with image {}", name, image);
81
82 let cpu_limit = format!("limits.cpu={}", config.cpu_cores);
84 let mem_limit = format!("limits.memory={}MB", config.memory_mb);
85
86 self.run_lxc(&[
87 "launch", image, &name,
88 "-c", &cpu_limit,
89 "-c", &mem_limit,
90 "-c", "security.nesting=true",
91 ])?;
92
93 let chpasswd_cmd = format!("echo 'root:{}' | chpasswd", config.password);
96
97 for _ in 0..10 {
99 match self.run_lxc(&["exec", &name, "--", "sh", "-c", &chpasswd_cmd]) {
100 Ok(_) => break,
101 Err(_) => tokio::time::sleep(std::time::Duration::from_secs(1)).await,
102 }
103 }
104
105 let setup_script = r#"
108 # Detect package manager and install SSH if missing
109 if command -v apk >/dev/null; then
110 # Alpine
111 apk add --no-cache openssh
112 rc-update add sshd default
113 service sshd start
114 elif command -v apt-get >/dev/null; then
115 # Debian/Ubuntu
116 # Usually installed, but ensure it runs
117 systemctl enable ssh
118 systemctl start ssh
119 fi
120
121 # Configure SSH for root access with password
122 # Check if config exists
123 if [ -f /etc/ssh/sshd_config ]; then
124 # Remove cloud-init config that disables password auth
125 rm -f /etc/ssh/sshd_config.d/*-cloudimg-settings.conf
126
127 sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
128 sed -i 's/PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
129 sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
130
131 # Restart service
132 service sshd restart || systemctl restart ssh || systemctl restart sshd
133 fi
134 "#;
135
136 let _ = self.run_lxc(&["exec", &name, "--", "sh", "-c", setup_script]);
137
138 if let Some(port) = config.host_port {
140 info!("Setting up port forwarding: Host {} -> Container 22", port);
141 self.run_lxc(&[
143 "config", "device", "add", &name, "ssh-proxy", "proxy",
144 &format!("listen=tcp:0.0.0.0:{}", port),
145 "connect=tcp:127.0.0.1:22",
146 ])?;
147 }
148
149 Ok(name)
150 }
151
152 async fn start_container(&self, id: u32) -> Result<()> {
153 let name = format!("paygress-{}", id);
154 self.run_lxc(&["start", &name])?;
155 Ok(())
156 }
157
158 async fn stop_container(&self, id: u32) -> Result<()> {
159 let name = format!("paygress-{}", id);
160 self.run_lxc(&["stop", &name])?;
161 Ok(())
162 }
163
164 async fn delete_container(&self, id: u32) -> Result<()> {
165 let name = format!("paygress-{}", id);
166 self.run_lxc(&["delete", &name, "--force"])?;
167 Ok(())
168 }
169
170 async fn get_node_status(&self) -> Result<NodeStatus> {
171 let mem_output = Command::new("free").arg("-b").output()?;
173 let mem_str = String::from_utf8_lossy(&mem_output.stdout);
174
175 let mut memory_total = 0;
179 let mut memory_used = 0;
180
181 for line in mem_str.lines() {
182 if line.starts_with("Mem:") {
183 let parts: Vec<&str> = line.split_whitespace().collect();
184 if parts.len() >= 3 {
185 memory_total = parts[1].parse().unwrap_or(0);
186 memory_used = parts[2].parse().unwrap_or(0);
187 }
188 }
189 }
190
191 let disk_output = Command::new("df").args(["-B1", "/"]).output()?;
193 let disk_str = String::from_utf8_lossy(&disk_output.stdout);
194
195 let mut disk_total = 0;
196 let mut disk_used = 0;
197
198 for line in disk_str.lines().skip(1) { let parts: Vec<&str> = line.split_whitespace().collect();
200 if parts.len() >= 3 {
201 disk_total = parts[1].parse().unwrap_or(0);
202 disk_used = parts[2].parse().unwrap_or(0);
203 break;
204 }
205 }
206
207 let loadavg = std::fs::read_to_string("/proc/loadavg").unwrap_or_default();
210 let load_1min: f64 = loadavg.split_whitespace().next().unwrap_or("0").parse().unwrap_or(0.0);
211 let cpu_cores = num_cpus::get() as f64;
212 let cpu_usage = (load_1min / cpu_cores).min(1.0);
213
214 Ok(NodeStatus {
215 cpu_usage,
216 memory_used,
217 memory_total,
218 disk_used,
219 disk_total,
220 })
221 }
222
223 async fn get_container_ip(&self, id: u32) -> Result<Option<String>> {
224 let name = format!("paygress-{}", id);
225 let output = self.run_lxc(&["list", &name, "--format", "json"])?;
226 let containers: serde_json::Value = serde_json::from_str(&output)?;
227
228 if let Some(container) = containers.as_array().and_then(|a| a.first()) {
229 if let Some(networks) = container.get("state").and_then(|s| s.get("network")) {
232 if let Some(eth0) = networks.get("eth0") {
233 if let Some(addrs) = eth0.get("addresses").and_then(|a| a.as_array()) {
234 for addr in addrs {
235 if addr.get("family").and_then(|f| f.as_str()) == Some("inet") {
236 if let Some(ip) = addr.get("address").and_then(|a| a.as_str()) {
237 return Ok(Some(ip.to_string()));
238 }
239 }
240 }
241 }
242 }
243 }
244 }
245
246 Ok(None)
247 }
248}