1use 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#[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 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
68pub 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
88pub 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 mut config = config.clone();
97 if !config.contains_key("size") && crate::org::disk::disk_limited(client) {
98 config.insert(
99 "size".into(),
100 crate::org::limits::DEFAULT_ROOT_SIZE.to_string(),
101 );
102 }
103 let body =
104 json!({"name": name, "type": "custom", "content_type": "filesystem", "config": config});
105 match client.mutate(
106 "POST",
107 &format!("/1.0/storage-pools/{}/volumes/custom", encode_segment(pool)),
108 Some(&body),
109 &format!("create volume {name}"),
110 client.timeouts.other,
111 ) {
112 Ok(_) => Ok(true),
113 Err(_) if get(client, pool, name)?.is_some() => Ok(false),
115 Err(e) => Err(e),
116 }
117}
118
119pub fn remove(client: &Client, pool: &str, name: &str) -> Result<()> {
121 let Some(v) = get(client, pool, name)? else {
122 return Err(Error::NotFound(format!("volume {name} in pool {pool}")));
123 };
124 if !v.used_by.is_empty() {
125 return Err(Error::invalid(format!(
126 "volume {name} is in use by {}",
127 v.used_by.join(", ")
128 )));
129 }
130 client
131 .mutate(
132 "DELETE",
133 &vol_path(pool, name),
134 None,
135 &format!("delete volume {name}"),
136 client.timeouts.other,
137 )
138 .map(|_| ())
139}