]> git.proxmox.com Git - proxmox-backup.git/blame - 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
CommitLineData
dc7a5b34 1use ::serde::{Deserialize, Serialize};
3f851d13 2use anyhow::{bail, Error};
fbbcd858 3use serde_json::json;
b757c616 4use std::os::linux::fs::MetadataExt;
d4f2397d 5
dc7a5b34 6use proxmox_router::{Permission, Router, RpcEnvironment, RpcEnvironmentType};
6ef1b649
WB
7use proxmox_schema::api;
8use proxmox_section_config::SectionConfigData;
d5790a9f 9use proxmox_sys::task_log;
d4f2397d 10
8cc3760e 11use pbs_api_types::{
dc7a5b34
TL
12 DataStoreConfig, BLOCKDEVICE_NAME_SCHEMA, DATASTORE_SCHEMA, NODE_SCHEMA, PRIV_SYS_AUDIT,
13 PRIV_SYS_MODIFY, UPID_SCHEMA,
8cc3760e
DM
14};
15
d4f2397d 16use crate::tools::disks::{
be260410
HL
17 create_file_system, create_single_linux_partition, get_fs_uuid, DiskManage, DiskUsageQuery,
18 DiskUsageType, FileSystemType,
d4f2397d
DM
19};
20use crate::tools::systemd::{self, types::*};
21
b9700a9f 22use proxmox_rest_server::WorkerTask;
d4f2397d 23
2de1b06a
DC
24const BASE_MOUNT_DIR: &str = "/mnt/datastore/";
25
d4f2397d
DM
26#[api(
27 properties: {
28 "filesystem": {
29 type: FileSystemType,
30 optional: true,
31 },
32 },
33)]
34#[derive(Debug, Serialize, Deserialize)]
dc7a5b34 35#[serde(rename_all = "kebab-case")]
d4f2397d
DM
36/// Datastore mount info.
37pub struct DatastoreMountInfo {
38 /// The path of the mount unit.
39 pub unitfile: String,
8eef3172
DC
40 /// The name of the mount
41 pub name: String,
d4f2397d
DM
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.
dc7a5b34 73pub fn list_datastore_mounts() -> Result<Vec<DatastoreMountInfo>, Error> {
d4f2397d
DM
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";
25877d05 81 for item in proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, basedir, &MOUNT_NAME_REGEX)? {
d4f2397d
DM
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
8eef3172
DC
89 let name = data
90 .Where
91 .strip_prefix(BASE_MOUNT_DIR)
e1db0670 92 .unwrap_or(&data.Where)
8eef3172
DC
93 .to_string();
94
d4f2397d
DM
95 list.push(DatastoreMountInfo {
96 unitfile,
8eef3172 97 name,
d4f2397d
DM
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)]
5116d051 139/// Create a Filesystem on an unused disk. Will be mounted under `/mnt/datastore/<name>`.
1aef491e 140pub fn create_datastore_disk(
d4f2397d
DM
141 name: String,
142 disk: String,
143 add_datastore: Option<bool>,
144 filesystem: Option<FileSystemType>,
145 rpcenv: &mut dyn RpcEnvironment,
146) -> Result<String, Error> {
39735609 147 let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI;
d4f2397d 148
049a22a3 149 let auth_id = rpcenv.get_auth_id().unwrap();
d4f2397d 150
be260410 151 let info = DiskUsageQuery::new().smart(false).find(&disk)?;
3f851d13
DM
152
153 if info.used != DiskUsageType::Unused {
154 bail!("disk '{}' is already in use.", disk);
155 }
156
2de1b06a 157 let mount_point = format!("{}{}", BASE_MOUNT_DIR, &name);
c960d2b5 158
b757c616
MF
159 // check if the default path exists already.
160 // bail if it is not empty or another filesystem mounted on top
c960d2b5
FE
161 let default_path = std::path::PathBuf::from(&mount_point);
162
163 match std::fs::metadata(&default_path) {
dc7a5b34 164 Err(_) => {} // path does not exist
b757c616
MF
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 }
c960d2b5
FE
174 }
175 }
176
d4f2397d 177 let upid_str = WorkerTask::new_thread(
dc7a5b34
TL
178 "dircreate",
179 Some(name.clone()),
180 auth_id,
181 to_stdout,
182 move |worker| {
1ec0d70d 183 task_log!(worker, "create datastore '{}' on disk {}", name, disk);
d4f2397d
DM
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
44288184 190 let disk = manager.disk_by_name(&disk)?;
d4f2397d
DM
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
dc7a5b34
TL
198 let mount_unit_name =
199 create_datastore_mount_unit(&name, &mount_point, filesystem, &uuid_path)?;
d4f2397d 200
15cc41b6
DM
201 crate::tools::systemd::reload_daemon()?;
202 crate::tools::systemd::enable_unit(&mount_unit_name)?;
203 crate::tools::systemd::start_unit(&mount_unit_name)?;
d4f2397d 204
fbbcd858 205 if add_datastore {
e7d4be9d 206 let lock = pbs_config::datastore::lock_config()?;
4708f4fc
DC
207 let datastore: DataStoreConfig =
208 serde_json::from_value(json!({ "name": name, "path": mount_point }))?;
209
e7d4be9d 210 let (config, _digest) = pbs_config::datastore::config()?;
4708f4fc
DC
211
212 if config.sections.get(&datastore.name).is_some() {
213 bail!("datastore '{}' already exists.", datastore.name);
214 }
215
dc7a5b34
TL
216 crate::api2::config::datastore::do_create_datastore(
217 lock,
218 config,
219 datastore,
220 Some(&worker),
221 )?;
fbbcd858
DM
222 }
223
d4f2397d 224 Ok(())
dc7a5b34
TL
225 },
226 )?;
d4f2397d
DM
227
228 Ok(upid_str)
229}
230
be614c62
HL
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)]
5116d051 247/// Remove a Filesystem mounted under `/mnt/datastore/<name>`.
98161fdd 248pub fn delete_datastore_disk(name: String) -> Result<(), Error> {
2de1b06a 249 let path = format!("{}{}", BASE_MOUNT_DIR, name);
be614c62 250 // path of datastore cannot be changed
e7d4be9d 251 let (config, _) = pbs_config::datastore::config()?;
be614c62 252 let datastores: Vec<DataStoreConfig> = config.convert_to_typed_array("datastore")?;
dc7a5b34
TL
253 let conflicting_datastore: Option<DataStoreConfig> =
254 datastores.into_iter().find(|ds| ds.path == path);
be614c62 255
98161fdd 256 if let Some(conflicting_datastore) = conflicting_datastore {
dc7a5b34
TL
257 bail!(
258 "Can't remove '{}' since it's required by datastore '{}'",
259 conflicting_datastore.path,
260 conflicting_datastore.name
261 );
98161fdd
DM
262 }
263
264 // disable systemd mount-unit
25877d05 265 let mut mount_unit_name = proxmox_sys::systemd::escape_unit(&path, true);
98161fdd 266 mount_unit_name.push_str(".mount");
15cc41b6 267 crate::tools::systemd::disable_unit(&mount_unit_name)?;
98161fdd
DM
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);
16f6766a 273 std::fs::remove_file(full_path)?;
98161fdd
DM
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);
25877d05 278 match proxmox_sys::command::run_command(command, None) {
98161fdd
DM
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 ),
dc7a5b34 284 Ok(_) => Ok(()),
be614c62
HL
285 }
286}
287
dc7a5b34 288const ITEM_ROUTER: Router = Router::new().delete(&API_METHOD_DELETE_DATASTORE_DISK);
be614c62 289
d4f2397d
DM
290pub const ROUTER: Router = Router::new()
291 .get(&API_METHOD_LIST_DATASTORE_MOUNTS)
be614c62
HL
292 .post(&API_METHOD_CREATE_DATASTORE_DISK)
293 .match_all("name", &ITEM_ROUTER);
d4f2397d 294
d4f2397d
DM
295fn create_datastore_mount_unit(
296 datastore_name: &str,
c960d2b5 297 mount_point: &str,
d4f2397d
DM
298 fs_type: FileSystemType,
299 what: &str,
c960d2b5 300) -> Result<String, Error> {
9a37bd6c 301 let mut mount_unit_name = proxmox_sys::systemd::escape_unit(mount_point, true);
d4f2397d
DM
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 {
dc7a5b34
TL
307 Description: format!(
308 "Mount datatstore '{}' under '{}'",
309 datastore_name, mount_point
310 ),
d4f2397d
DM
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(),
c960d2b5 321 Where: mount_point.to_string(),
d4f2397d
DM
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
c960d2b5 334 Ok(mount_unit_name)
d4f2397d 335}