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