Skip to main content

isb_core/
volume.rs

1//! Named custom storage volumes.
2
3use serde::Serialize;
4use serde_json::{Value, json};
5
6use crate::client::{Client, encode_segment};
7use crate::error::{Error, Result};
8use crate::plan::Props;
9
10/// A custom volume as incus reports it.
11#[derive(Debug, Clone, Serialize)]
12pub struct VolumeInfo {
13    pub name: String,
14    pub pool: String,
15    pub content_type: String,
16    pub config: Props,
17    /// Instances (and profiles) using it, as incus URLs.
18    pub used_by: Vec<String>,
19}
20
21impl VolumeInfo {
22    fn from_api(pool: &str, v: &Value) -> VolumeInfo {
23        VolumeInfo {
24            name: v.get("name").and_then(Value::as_str).unwrap_or("").into(),
25            pool: pool.into(),
26            content_type: v
27                .get("content_type")
28                .and_then(Value::as_str)
29                .unwrap_or("")
30                .into(),
31            config: v
32                .get("config")
33                .and_then(Value::as_object)
34                .map(|m| {
35                    m.iter()
36                        .map(|(k, v)| {
37                            (
38                                k.clone(),
39                                v.as_str()
40                                    .map(String::from)
41                                    .unwrap_or_else(|| v.to_string()),
42                            )
43                        })
44                        .collect()
45                })
46                .unwrap_or_default(),
47            used_by: v
48                .get("used_by")
49                .and_then(Value::as_array)
50                .map(|a| {
51                    a.iter()
52                        .filter_map(|x| x.as_str().map(String::from))
53                        .collect()
54                })
55                .unwrap_or_default(),
56        }
57    }
58}
59
60fn vol_path(pool: &str, name: &str) -> String {
61    format!(
62        "/1.0/storage-pools/{}/volumes/custom/{}",
63        encode_segment(pool),
64        encode_segment(name)
65    )
66}
67
68/// All custom volumes in `pool`.
69pub fn list(client: &Client, pool: &str) -> Result<Vec<VolumeInfo>> {
70    let v = client.get(&format!(
71        "/1.0/storage-pools/{}/volumes/custom?recursion=1",
72        encode_segment(pool)
73    ))?;
74    let mut out: Vec<VolumeInfo> = v
75        .as_array()
76        .map(|a| a.iter().map(|x| VolumeInfo::from_api(pool, x)).collect())
77        .unwrap_or_default();
78    out.sort_by(|a, b| a.name.cmp(&b.name));
79    Ok(out)
80}
81
82pub fn get(client: &Client, pool: &str, name: &str) -> Result<Option<VolumeInfo>> {
83    Ok(client
84        .get_opt(&vol_path(pool, name))?
85        .map(|v| VolumeInfo::from_api(pool, &v)))
86}
87
88/// Create if missing. Returns true if this call created it.
89pub fn ensure(client: &Client, pool: &str, name: &str, config: &Props) -> Result<bool> {
90    if get(client, pool, name)?.is_some() {
91        return Ok(false);
92    }
93    let body =
94        json!({"name": name, "type": "custom", "content_type": "filesystem", "config": config});
95    match client.mutate(
96        "POST",
97        &format!("/1.0/storage-pools/{}/volumes/custom", encode_segment(pool)),
98        Some(&body),
99        &format!("create volume {name}"),
100        client.timeouts.other,
101    ) {
102        Ok(_) => Ok(true),
103        // Lost a race with another creator: fine, as long as it exists now.
104        Err(_) if get(client, pool, name)?.is_some() => Ok(false),
105        Err(e) => Err(e),
106    }
107}
108
109/// Delete a volume. Refuses while an instance uses it.
110pub fn remove(client: &Client, pool: &str, name: &str) -> Result<()> {
111    let Some(v) = get(client, pool, name)? else {
112        return Err(Error::NotFound(format!("volume {name} in pool {pool}")));
113    };
114    if !v.used_by.is_empty() {
115        return Err(Error::invalid(format!(
116            "volume {name} is in use by {}",
117            v.used_by.join(", ")
118        )));
119    }
120    client
121        .mutate(
122            "DELETE",
123            &vol_path(pool, name),
124            None,
125            &format!("delete volume {name}"),
126            client.timeouts.other,
127        )
128        .map(|_| ())
129}