]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/node/disks/directory.rs
api: datastore create: allow re-using existing dirs if empty & not a mountpoint
[proxmox-backup.git] / src / api2 / node / disks / directory.rs
1 use ::serde::{Deserialize, Serialize};
2 use anyhow::{bail, Error};
3 use serde_json::json;
4 use std::os::linux::fs::MetadataExt;
5
6 use proxmox_router::{Permission, Router, RpcEnvironment, RpcEnvironmentType};
7 use proxmox_schema::api;
8 use proxmox_section_config::SectionConfigData;
9 use proxmox_sys::task_log;
10
11 use pbs_api_types::{
12 DataStoreConfig, BLOCKDEVICE_NAME_SCHEMA, DATASTORE_SCHEMA, NODE_SCHEMA, PRIV_SYS_AUDIT,
13 PRIV_SYS_MODIFY, UPID_SCHEMA,
14 };
15
16 use crate::tools::disks::{
17 create_file_system, create_single_linux_partition, get_fs_uuid, DiskManage, DiskUsageQuery,
18 DiskUsageType, FileSystemType,
19 };
20 use crate::tools::systemd::{self, types::*};
21
22 use proxmox_rest_server::WorkerTask;
23
24 const BASE_MOUNT_DIR: &str = "/mnt/datastore/";
25
26 #[api(
27 properties: {
28 "filesystem": {
29 type: FileSystemType,
30 optional: true,
31 },
32 },
33 )]
34 #[derive(Debug, Serialize, Deserialize)]
35 #[serde(rename_all = "kebab-case")]
36 /// Datastore mount info.
37 pub struct DatastoreMountInfo {
38 /// The path of the mount unit.
39 pub unitfile: String,
40 /// The name of the mount
41 pub name: String,
42 /// The mount path.
43 pub path: String,
44 /// The mounted device.
45 pub device: String,
46 /// File system type
47 pub filesystem: Option<String>,
48 /// Mount options
49 pub options: Option<String>,
50 }
51
52 #[api(
53 protected: true,
54 input: {
55 properties: {
56 node: {
57 schema: NODE_SCHEMA,
58 },
59 }
60 },
61 returns: {
62 description: "List of systemd datastore mount units.",
63 type: Array,
64 items: {
65 type: DatastoreMountInfo,
66 },
67 },
68 access: {
69 permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_AUDIT, false),
70 },
71 )]
72 /// List systemd datastore mount units.
73 pub fn list_datastore_mounts() -> Result<Vec<DatastoreMountInfo>, Error> {
74 lazy_static::lazy_static! {
75 static ref MOUNT_NAME_REGEX: regex::Regex = regex::Regex::new(r"^mnt-datastore-(.+)\.mount$").unwrap();
76 }
77
78 let mut list = Vec::new();
79
80 let basedir = "/etc/systemd/system";
81 for item in proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, basedir, &MOUNT_NAME_REGEX)? {
82 let item = item?;
83 let name = item.file_name().to_string_lossy().to_string();
84
85 let unitfile = format!("{}/{}", basedir, name);
86 let config = systemd::config::parse_systemd_mount(&unitfile)?;
87 let data: SystemdMountSection = config.lookup("Mount", "Mount")?;
88
89 let name = data
90 .Where
91 .strip_prefix(BASE_MOUNT_DIR)
92 .unwrap_or(&data.Where)
93 .to_string();
94
95 list.push(DatastoreMountInfo {
96 unitfile,
97 name,
98 device: data.What,
99 path: data.Where,
100 filesystem: data.Type,
101 options: data.Options,
102 });
103 }
104
105 Ok(list)
106 }
107
108 #[api(
109 protected: true,
110 input: {
111 properties: {
112 node: {
113 schema: NODE_SCHEMA,
114 },
115 name: {
116 schema: DATASTORE_SCHEMA,
117 },
118 disk: {
119 schema: BLOCKDEVICE_NAME_SCHEMA,
120 },
121 "add-datastore": {
122 description: "Configure a datastore using the directory.",
123 type: bool,
124 optional: true,
125 },
126 filesystem: {
127 type: FileSystemType,
128 optional: true,
129 },
130 }
131 },
132 returns: {
133 schema: UPID_SCHEMA,
134 },
135 access: {
136 permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_MODIFY, false),
137 },
138 )]
139 /// Create a Filesystem on an unused disk. Will be mounted under `/mnt/datastore/<name>`.
140 pub fn create_datastore_disk(
141 name: String,
142 disk: String,
143 add_datastore: Option<bool>,
144 filesystem: Option<FileSystemType>,
145 rpcenv: &mut dyn RpcEnvironment,
146 ) -> Result<String, Error> {
147 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
148
149 let auth_id = rpcenv.get_auth_id().unwrap();
150
151 let info = DiskUsageQuery::new().smart(false).find(&disk)?;
152
153 if info.used != DiskUsageType::Unused {
154 bail!("disk '{}' is already in use.", disk);
155 }
156
157 let mount_point = format!("{}{}", BASE_MOUNT_DIR, &name);
158
159 // check if the default path exists already.
160 // bail if it is not empty or another filesystem mounted on top
161 let default_path = std::path::PathBuf::from(&mount_point);
162
163 match std::fs::metadata(&default_path) {
164 Err(_) => {} // path does not exist
165 Ok(stat) => {
166 let basedir_dev = std::fs::metadata(BASE_MOUNT_DIR)?.st_dev();
167 if stat.st_dev() != basedir_dev {
168 bail!("path {default_path:?} already exists and is mountpoint");
169 }
170 let is_empty = default_path.read_dir()?.next().is_none();
171 if !is_empty {
172 bail!("path {default_path:?} already exists and is not empty");
173 }
174 }
175 }
176
177 let upid_str = WorkerTask::new_thread(
178 "dircreate",
179 Some(name.clone()),
180 auth_id,
181 to_stdout,
182 move |worker| {
183 task_log!(worker, "create datastore '{}' on disk {}", name, disk);
184
185 let add_datastore = add_datastore.unwrap_or(false);
186 let filesystem = filesystem.unwrap_or(FileSystemType::Ext4);
187
188 let manager = DiskManage::new();
189
190 let disk = manager.disk_by_name(&disk)?;
191
192 let partition = create_single_linux_partition(&disk)?;
193 create_file_system(&partition, filesystem)?;
194
195 let uuid = get_fs_uuid(&partition)?;
196 let uuid_path = format!("/dev/disk/by-uuid/{}", uuid);
197
198 let mount_unit_name =
199 create_datastore_mount_unit(&name, &mount_point, filesystem, &uuid_path)?;
200
201 crate::tools::systemd::reload_daemon()?;
202 crate::tools::systemd::enable_unit(&mount_unit_name)?;
203 crate::tools::systemd::start_unit(&mount_unit_name)?;
204
205 if add_datastore {
206 let lock = pbs_config::datastore::lock_config()?;
207 let datastore: DataStoreConfig =
208 serde_json::from_value(json!({ "name": name, "path": mount_point }))?;
209
210 let (config, _digest) = pbs_config::datastore::config()?;
211
212 if config.sections.get(&datastore.name).is_some() {
213 bail!("datastore '{}' already exists.", datastore.name);
214 }
215
216 crate::api2::config::datastore::do_create_datastore(
217 lock,
218 config,
219 datastore,
220 Some(&worker),
221 )?;
222 }
223
224 Ok(())
225 },
226 )?;
227
228 Ok(upid_str)
229 }
230
231 #[api(
232 protected: true,
233 input: {
234 properties: {
235 node: {
236 schema: NODE_SCHEMA,
237 },
238 name: {
239 schema: DATASTORE_SCHEMA,
240 },
241 }
242 },
243 access: {
244 permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_MODIFY, false),
245 },
246 )]
247 /// Remove a Filesystem mounted under `/mnt/datastore/<name>`.
248 pub fn delete_datastore_disk(name: String) -> Result<(), Error> {
249 let path = format!("{}{}", BASE_MOUNT_DIR, name);
250 // path of datastore cannot be changed
251 let (config, _) = pbs_config::datastore::config()?;
252 let datastores: Vec<DataStoreConfig> = config.convert_to_typed_array("datastore")?;
253 let conflicting_datastore: Option<DataStoreConfig> =
254 datastores.into_iter().find(|ds| ds.path == path);
255
256 if let Some(conflicting_datastore) = conflicting_datastore {
257 bail!(
258 "Can't remove '{}' since it's required by datastore '{}'",
259 conflicting_datastore.path,
260 conflicting_datastore.name
261 );
262 }
263
264 // disable systemd mount-unit
265 let mut mount_unit_name = proxmox_sys::systemd::escape_unit(&path, true);
266 mount_unit_name.push_str(".mount");
267 crate::tools::systemd::disable_unit(&mount_unit_name)?;
268
269 // delete .mount-file
270 let mount_unit_path = format!("/etc/systemd/system/{}", mount_unit_name);
271 let full_path = std::path::Path::new(&mount_unit_path);
272 log::info!("removing systemd mount unit {:?}", full_path);
273 std::fs::remove_file(full_path)?;
274
275 // try to unmount, if that fails tell the user to reboot or unmount manually
276 let mut command = std::process::Command::new("umount");
277 command.arg(&path);
278 match proxmox_sys::command::run_command(command, None) {
279 Err(_) => bail!(
280 "Could not umount '{}' since it is busy. It will stay mounted \
281 until the next reboot or until unmounted manually!",
282 path
283 ),
284 Ok(_) => Ok(()),
285 }
286 }
287
288 const ITEM_ROUTER: Router = Router::new().delete(&API_METHOD_DELETE_DATASTORE_DISK);
289
290 pub const ROUTER: Router = Router::new()
291 .get(&API_METHOD_LIST_DATASTORE_MOUNTS)
292 .post(&API_METHOD_CREATE_DATASTORE_DISK)
293 .match_all("name", &ITEM_ROUTER);
294
295 fn create_datastore_mount_unit(
296 datastore_name: &str,
297 mount_point: &str,
298 fs_type: FileSystemType,
299 what: &str,
300 ) -> Result<String, Error> {
301 let mut mount_unit_name = proxmox_sys::systemd::escape_unit(mount_point, true);
302 mount_unit_name.push_str(".mount");
303
304 let mount_unit_path = format!("/etc/systemd/system/{}", mount_unit_name);
305
306 let unit = SystemdUnitSection {
307 Description: format!(
308 "Mount datatstore '{}' under '{}'",
309 datastore_name, mount_point
310 ),
311 ..Default::default()
312 };
313
314 let install = SystemdInstallSection {
315 WantedBy: Some(vec!["multi-user.target".to_string()]),
316 ..Default::default()
317 };
318
319 let mount = SystemdMountSection {
320 What: what.to_string(),
321 Where: mount_point.to_string(),
322 Type: Some(fs_type.to_string()),
323 Options: Some(String::from("defaults")),
324 ..Default::default()
325 };
326
327 let mut config = SectionConfigData::new();
328 config.set_data("Unit", "Unit", unit)?;
329 config.set_data("Install", "Install", install)?;
330 config.set_data("Mount", "Mount", mount)?;
331
332 systemd::config::save_systemd_mount(&mount_unit_path, &config)?;
333
334 Ok(mount_unit_name)
335 }