]>
Commit | Line | Data |
---|---|---|
dc7a5b34 | 1 | use ::serde::{Deserialize, Serialize}; |
3f851d13 | 2 | use anyhow::{bail, Error}; |
fbbcd858 | 3 | use serde_json::json; |
b757c616 | 4 | use std::os::linux::fs::MetadataExt; |
d4f2397d | 5 | |
dc7a5b34 | 6 | use proxmox_router::{Permission, Router, RpcEnvironment, RpcEnvironmentType}; |
6ef1b649 WB |
7 | use proxmox_schema::api; |
8 | use proxmox_section_config::SectionConfigData; | |
d5790a9f | 9 | use proxmox_sys::task_log; |
d4f2397d | 10 | |
8cc3760e | 11 | use 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 | 16 | use crate::tools::disks::{ |
be260410 HL |
17 | create_file_system, create_single_linux_partition, get_fs_uuid, DiskManage, DiskUsageQuery, |
18 | DiskUsageType, FileSystemType, | |
d4f2397d DM |
19 | }; |
20 | use crate::tools::systemd::{self, types::*}; | |
21 | ||
b9700a9f | 22 | use proxmox_rest_server::WorkerTask; |
d4f2397d | 23 | |
2de1b06a DC |
24 | const 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. |
37 | pub 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 | 73 | pub 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 | 140 | pub 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 | 248 | pub 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 | 288 | const ITEM_ROUTER: Router = Router::new().delete(&API_METHOD_DELETE_DATASTORE_DISK); |
be614c62 | 289 | |
d4f2397d DM |
290 | pub 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 |
295 | fn 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 | } |