1use std::io::Read;
5use std::time::Duration;
6
7use serde::Serialize;
8use serde_json::{Value, json};
9
10use crate::client::{Client, encode_segment};
11use crate::error::{Error, Result};
12use crate::volume::VolumeInfo;
13
14pub const SLOW: Duration = Duration::from_secs(3600);
17pub const IDLE: Duration = Duration::from_secs(600);
19
20fn vol(pool: &str, name: &str) -> String {
21 format!(
22 "/1.0/storage-pools/{}/volumes/custom/{}",
23 encode_segment(pool),
24 encode_segment(name)
25 )
26}
27
28#[derive(Debug, Clone, Serialize)]
30pub struct Snapshot {
31 pub name: String,
32 pub created_at: String,
33 #[serde(skip_serializing_if = "String::is_empty")]
34 pub description: String,
35 pub kind: &'static str,
38}
39
40pub fn instances_using(v: &VolumeInfo) -> Vec<String> {
42 v.used_by
43 .iter()
44 .filter_map(|u| {
45 let rest = u.strip_prefix("/1.0/instances/")?;
46 let name = rest.split(['?', '/']).next()?;
47 (!name.is_empty()).then(|| name.to_string())
48 })
49 .collect()
50}
51
52pub fn is_running(c: &Client, instance: &str) -> Result<bool> {
53 let v = c.get(&format!("/1.0/instances/{}", encode_segment(instance)))?;
54 Ok(v.get("status").and_then(Value::as_str) == Some("Running"))
55}
56
57pub fn volume(c: &Client, pool: &str, name: &str) -> Result<VolumeInfo> {
59 crate::volume::get(c, pool, name)?
60 .ok_or_else(|| Error::NotFound(format!("volume {name} in pool {pool}")))
61}
62
63pub fn snapshots(c: &Client, pool: &str, name: &str) -> Result<Vec<Snapshot>> {
65 let v = c.get(&format!("{}/snapshots?recursion=1", vol(pool, name)))?;
66 let mut out: Vec<Snapshot> = v
67 .as_array()
68 .into_iter()
69 .flatten()
70 .filter_map(|s| {
71 let full = s.get("name")?.as_str()?;
72 let name = full.rsplit('/').next().unwrap_or(full).to_string();
73 let kind = if super::model::auto_time(&name).is_some() {
74 "auto"
75 } else if name.starts_with(super::model::MANUAL) {
76 "manual"
77 } else {
78 "other"
79 };
80 Some(Snapshot {
81 created_at: s
82 .get("created_at")
83 .and_then(Value::as_str)
84 .unwrap_or("")
85 .into(),
86 description: s
87 .get("description")
88 .and_then(Value::as_str)
89 .unwrap_or("")
90 .into(),
91 name,
92 kind,
93 })
94 })
95 .collect();
96 out.sort_by(|a, b| a.created_at.cmp(&b.created_at));
97 Ok(out)
98}
99
100pub fn snapshot_create(c: &Client, pool: &str, name: &str, snap: &str, desc: &str) -> Result<()> {
101 c.mutate(
102 "POST",
103 &format!("{}/snapshots", vol(pool, name)),
104 Some(&json!({"name": snap, "description": desc})),
105 &format!("snapshot {name}/{snap}"),
106 SLOW,
107 )
108 .map(|_| ())
109}
110
111pub fn snapshot_delete(c: &Client, pool: &str, name: &str, snap: &str) -> Result<()> {
112 c.mutate(
113 "DELETE",
114 &format!("{}/snapshots/{}", vol(pool, name), encode_segment(snap)),
115 None,
116 &format!("delete snapshot {name}/{snap}"),
117 SLOW,
118 )
119 .map(|_| ())
120}
121
122pub fn copy_from_snapshot(
124 c: &Client,
125 pool: &str,
126 (name, snap): (&str, &str),
127 new: &str,
128 config: &[(&str, String)],
129) -> Result<()> {
130 let body = json!({
131 "name": new, "type": "custom", "content_type": "filesystem",
132 "source": {"type": "copy", "pool": pool, "name": format!("{name}/{snap}"), "volume_only": true},
133 });
134 c.mutate(
135 "POST",
136 &format!("/1.0/storage-pools/{}/volumes/custom", encode_segment(pool)),
137 Some(&body),
138 &format!("copy {name}/{snap} to {new}"),
139 SLOW,
140 )?;
141 set_config(c, pool, new, config)
142}
143
144pub fn set_config(c: &Client, pool: &str, name: &str, config: &[(&str, String)]) -> Result<()> {
146 if config.is_empty() {
147 return Ok(());
148 }
149 let m: serde_json::Map<String, Value> = config
150 .iter()
151 .map(|(k, v)| ((*k).to_string(), json!(v)))
152 .collect();
153 c.mutate(
154 "PATCH",
155 &vol(pool, name),
156 Some(&json!({"config": m})),
157 &format!("label volume {name}"),
158 c.get_timeouts().other,
159 )
160 .map(|_| ())
161}
162
163pub fn delete_volume(c: &Client, pool: &str, name: &str) -> Result<()> {
165 let started = std::time::Instant::now();
166 loop {
167 match crate::volume::remove(c, pool, name) {
168 Err(e) if !e.is_not_found() && started.elapsed() < Duration::from_secs(30) => {
169 std::thread::sleep(Duration::from_secs(1));
170 }
171 Err(e) if e.is_not_found() => return Ok(()),
172 r => return r,
173 }
174 }
175}
176
177pub fn export(c: &Client, pool: &str, name: &str, backup: &str) -> Result<impl Read + use<>> {
180 let expires = chrono_free_expiry();
181 c.mutate(
182 "POST",
183 &format!("{}/backups", vol(pool, name)),
184 Some(&json!({
185 "name": backup, "volume_only": true, "optimized_storage": false,
186 "compression_algorithm": "none", "expires_at": expires,
187 })),
188 &format!("back up {name}"),
189 SLOW,
190 )?;
191 c.get_stream(
192 &format!(
193 "{}/backups/{}/export",
194 vol(pool, name),
195 encode_segment(backup)
196 ),
197 IDLE,
198 )
199}
200
201fn chrono_free_expiry() -> String {
203 let now = std::time::SystemTime::now()
204 .duration_since(std::time::UNIX_EPOCH)
205 .map(|d| d.as_secs() as i64)
206 .unwrap_or(0);
207 crate::cron::rfc3339(now + 86_400)
208}
209
210pub fn backup_delete(c: &Client, pool: &str, name: &str, backup: &str) -> Result<()> {
211 c.mutate(
212 "DELETE",
213 &format!("{}/backups/{}", vol(pool, name), encode_segment(backup)),
214 None,
215 &format!("delete the export of {name}"),
216 c.get_timeouts().other,
217 )
218 .map(|_| ())
219}
220
221pub fn import(c: &Client, pool: &str, new: &str, body: &mut dyn Read) -> Result<()> {
223 let reply = c.post_stream(
224 &format!("/1.0/storage-pools/{}/volumes/custom", encode_segment(pool)),
225 &[
226 ("Content-Type", "application/octet-stream".into()),
227 ("X-Incus-name", new.into()),
228 ],
229 body,
230 IDLE,
231 )?;
232 match reply {
233 crate::client::Reply::Sync(_) => Ok(()),
234 crate::client::Reply::Async { operation, .. } => c
235 .wait_operation(&operation, &format!("import {new}"), SLOW)
236 .map(|_| ()),
237 }
238}
239
240pub fn attach(
242 c: &Client,
243 instance: &str,
244 device: &str,
245 (pool, source): (&str, &str),
246 path: &str,
247) -> Result<()> {
248 let props = json!({"type": "disk", "pool": pool, "source": source, "path": path});
249 crate::sandbox::update_instance(
250 c,
251 instance,
252 &format!("attach {source} to {instance}"),
253 &mut |_, devices| {
254 devices.insert(device.to_string(), props.clone());
255 Ok(())
256 },
257 )
258}
259
260pub fn detach(c: &Client, instance: &str, source: &str) -> Result<()> {
262 crate::sandbox::update_instance(
263 c,
264 instance,
265 &format!("detach {source} from {instance}"),
266 &mut |_, devices| {
267 devices.retain(|_, d| {
268 !(d.get("type").and_then(Value::as_str) == Some("disk")
269 && d.get("source").and_then(Value::as_str) == Some(source))
270 });
271 Ok(())
272 },
273 )
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn instances_from_used_by() {
282 let v = VolumeInfo {
283 name: "v".into(),
284 pool: "p".into(),
285 content_type: "filesystem".into(),
286 config: Default::default(),
287 used_by: vec![
288 "/1.0/instances/web-1?project=isb-acme".into(),
289 "/1.0/instances/ws".into(),
290 "/1.0/profiles/default".into(),
291 ],
292 };
293 assert_eq!(instances_using(&v), ["web-1", "ws"]);
294 }
295}