]>
Commit | Line | Data |
---|---|---|
3f851d13 | 1 | use anyhow::{bail, Error}; |
fbbcd858 | 2 | use serde_json::json; |
d4f2397d DM |
3 | use ::serde::{Deserialize, Serialize}; |
4 | ||
6ef1b649 WB |
5 | use proxmox_router::{Router, RpcEnvironment, RpcEnvironmentType, Permission}; |
6 | use proxmox_schema::api; | |
7 | use proxmox_section_config::SectionConfigData; | |
d5790a9f | 8 | use proxmox_sys::task_log; |
d4f2397d | 9 | |
8cc3760e | 10 | use pbs_api_types::{ |
049a22a3 | 11 | DataStoreConfig, NODE_SCHEMA, BLOCKDEVICE_NAME_SCHEMA, |
e7d4be9d | 12 | DATASTORE_SCHEMA, UPID_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, |
8cc3760e DM |
13 | }; |
14 | ||
d4f2397d | 15 | use crate::tools::disks::{ |
3f851d13 DM |
16 | DiskManage, FileSystemType, DiskUsageType, |
17 | create_file_system, create_single_linux_partition, get_fs_uuid, get_disk_usage_info, | |
d4f2397d DM |
18 | }; |
19 | use crate::tools::systemd::{self, types::*}; | |
20 | ||
b9700a9f | 21 | use proxmox_rest_server::WorkerTask; |
d4f2397d | 22 | |
2de1b06a DC |
23 | const BASE_MOUNT_DIR: &str = "/mnt/datastore/"; |
24 | ||
d4f2397d DM |
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, | |
8eef3172 DC |
39 | /// The name of the mount |
40 | pub name: String, | |
d4f2397d DM |
41 | /// The mount path. |
42 | pub path: String, | |
43 | /// The mounted device. | |
44 | pub device: String, | |
45 | /// File system type | |
46 | pub filesystem: Option<String>, | |
47 | /// Mount options | |
48 | pub options: Option<String>, | |
49 | } | |
50 | ||
51 | #[api( | |
52 | protected: true, | |
53 | input: { | |
54 | properties: { | |
55 | node: { | |
56 | schema: NODE_SCHEMA, | |
57 | }, | |
58 | } | |
59 | }, | |
60 | returns: { | |
61 | description: "List of systemd datastore mount units.", | |
62 | type: Array, | |
63 | items: { | |
64 | type: DatastoreMountInfo, | |
65 | }, | |
66 | }, | |
67 | access: { | |
68 | permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_AUDIT, false), | |
69 | }, | |
70 | )] | |
71 | /// List systemd datastore mount units. | |
1aef491e | 72 | pub fn list_datastore_mounts() -> Result<Vec<DatastoreMountInfo>, Error> { |
d4f2397d DM |
73 | |
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"; | |
770a36e5 | 81 | for item in pbs_tools::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) | |
92 | .unwrap_or_else(|| &data.Where) | |
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 | )] | |
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> { | |
147 | ||
39735609 | 148 | let to_stdout = rpcenv.env_type() == RpcEnvironmentType::CLI; |
d4f2397d | 149 | |
049a22a3 | 150 | let auth_id = rpcenv.get_auth_id().unwrap(); |
d4f2397d | 151 | |
3f851d13 DM |
152 | let info = get_disk_usage_info(&disk, true)?; |
153 | ||
154 | if info.used != DiskUsageType::Unused { | |
155 | bail!("disk '{}' is already in use.", disk); | |
156 | } | |
157 | ||
2de1b06a | 158 | let mount_point = format!("{}{}", BASE_MOUNT_DIR, &name); |
c960d2b5 FE |
159 | |
160 | // check if the default path does exist already and bail if it does | |
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(_) => { | |
166 | bail!("path {:?} already exists", default_path); | |
167 | } | |
168 | } | |
169 | ||
d4f2397d | 170 | let upid_str = WorkerTask::new_thread( |
e6dc35ac | 171 | "dircreate", Some(name.clone()), auth_id, to_stdout, move |worker| |
d4f2397d | 172 | { |
1ec0d70d | 173 | task_log!(worker, "create datastore '{}' on disk {}", name, disk); |
d4f2397d DM |
174 | |
175 | let add_datastore = add_datastore.unwrap_or(false); | |
176 | let filesystem = filesystem.unwrap_or(FileSystemType::Ext4); | |
177 | ||
178 | let manager = DiskManage::new(); | |
179 | ||
44288184 | 180 | let disk = manager.disk_by_name(&disk)?; |
d4f2397d DM |
181 | |
182 | let partition = create_single_linux_partition(&disk)?; | |
183 | create_file_system(&partition, filesystem)?; | |
184 | ||
185 | let uuid = get_fs_uuid(&partition)?; | |
186 | let uuid_path = format!("/dev/disk/by-uuid/{}", uuid); | |
187 | ||
c960d2b5 | 188 | let mount_unit_name = create_datastore_mount_unit(&name, &mount_point, filesystem, &uuid_path)?; |
d4f2397d | 189 | |
15cc41b6 DM |
190 | crate::tools::systemd::reload_daemon()?; |
191 | crate::tools::systemd::enable_unit(&mount_unit_name)?; | |
192 | crate::tools::systemd::start_unit(&mount_unit_name)?; | |
d4f2397d | 193 | |
fbbcd858 | 194 | if add_datastore { |
e7d4be9d | 195 | let lock = pbs_config::datastore::lock_config()?; |
4708f4fc DC |
196 | let datastore: DataStoreConfig = |
197 | serde_json::from_value(json!({ "name": name, "path": mount_point }))?; | |
198 | ||
e7d4be9d | 199 | let (config, _digest) = pbs_config::datastore::config()?; |
4708f4fc DC |
200 | |
201 | if config.sections.get(&datastore.name).is_some() { | |
202 | bail!("datastore '{}' already exists.", datastore.name); | |
203 | } | |
204 | ||
2de4dc3a | 205 | crate::api2::config::datastore::do_create_datastore(lock, config, datastore, Some(&worker))?; |
fbbcd858 DM |
206 | } |
207 | ||
d4f2397d DM |
208 | Ok(()) |
209 | })?; | |
210 | ||
211 | Ok(upid_str) | |
212 | } | |
213 | ||
be614c62 HL |
214 | #[api( |
215 | protected: true, | |
216 | input: { | |
217 | properties: { | |
218 | node: { | |
219 | schema: NODE_SCHEMA, | |
220 | }, | |
221 | name: { | |
222 | schema: DATASTORE_SCHEMA, | |
223 | }, | |
224 | } | |
225 | }, | |
226 | access: { | |
227 | permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_MODIFY, false), | |
228 | }, | |
229 | )] | |
230 | /// Remove a Filesystem mounted under '/mnt/datastore/<name>'.". | |
98161fdd DM |
231 | pub fn delete_datastore_disk(name: String) -> Result<(), Error> { |
232 | ||
2de1b06a | 233 | let path = format!("{}{}", BASE_MOUNT_DIR, name); |
be614c62 | 234 | // path of datastore cannot be changed |
e7d4be9d | 235 | let (config, _) = pbs_config::datastore::config()?; |
be614c62 HL |
236 | let datastores: Vec<DataStoreConfig> = config.convert_to_typed_array("datastore")?; |
237 | let conflicting_datastore: Option<DataStoreConfig> = datastores.into_iter() | |
ea368a06 | 238 | .find(|ds| ds.path == path); |
be614c62 | 239 | |
98161fdd DM |
240 | if let Some(conflicting_datastore) = conflicting_datastore { |
241 | bail!("Can't remove '{}' since it's required by datastore '{}'", | |
242 | conflicting_datastore.path, conflicting_datastore.name); | |
243 | } | |
244 | ||
245 | // disable systemd mount-unit | |
81867f05 | 246 | let mut mount_unit_name = proxmox::tools::systemd::escape_unit(&path, true); |
98161fdd | 247 | mount_unit_name.push_str(".mount"); |
15cc41b6 | 248 | crate::tools::systemd::disable_unit(&mount_unit_name)?; |
98161fdd DM |
249 | |
250 | // delete .mount-file | |
251 | let mount_unit_path = format!("/etc/systemd/system/{}", mount_unit_name); | |
252 | let full_path = std::path::Path::new(&mount_unit_path); | |
253 | log::info!("removing systemd mount unit {:?}", full_path); | |
254 | std::fs::remove_file(&full_path)?; | |
255 | ||
256 | // try to unmount, if that fails tell the user to reboot or unmount manually | |
257 | let mut command = std::process::Command::new("umount"); | |
258 | command.arg(&path); | |
4c1b7761 | 259 | match pbs_tools::run_command(command, None) { |
98161fdd DM |
260 | Err(_) => bail!( |
261 | "Could not umount '{}' since it is busy. It will stay mounted \ | |
262 | until the next reboot or until unmounted manually!", | |
263 | path | |
264 | ), | |
265 | Ok(_) => Ok(()) | |
be614c62 HL |
266 | } |
267 | } | |
268 | ||
269 | const ITEM_ROUTER: Router = Router::new() | |
270 | .delete(&API_METHOD_DELETE_DATASTORE_DISK); | |
271 | ||
d4f2397d DM |
272 | pub const ROUTER: Router = Router::new() |
273 | .get(&API_METHOD_LIST_DATASTORE_MOUNTS) | |
be614c62 HL |
274 | .post(&API_METHOD_CREATE_DATASTORE_DISK) |
275 | .match_all("name", &ITEM_ROUTER); | |
d4f2397d DM |
276 | |
277 | ||
278 | fn create_datastore_mount_unit( | |
279 | datastore_name: &str, | |
c960d2b5 | 280 | mount_point: &str, |
d4f2397d DM |
281 | fs_type: FileSystemType, |
282 | what: &str, | |
c960d2b5 | 283 | ) -> Result<String, Error> { |
d4f2397d | 284 | |
81867f05 | 285 | let mut mount_unit_name = proxmox::tools::systemd::escape_unit(&mount_point, true); |
d4f2397d DM |
286 | mount_unit_name.push_str(".mount"); |
287 | ||
288 | let mount_unit_path = format!("/etc/systemd/system/{}", mount_unit_name); | |
289 | ||
290 | let unit = SystemdUnitSection { | |
291 | Description: format!("Mount datatstore '{}' under '{}'", datastore_name, mount_point), | |
292 | ..Default::default() | |
293 | }; | |
294 | ||
295 | let install = SystemdInstallSection { | |
296 | WantedBy: Some(vec!["multi-user.target".to_string()]), | |
297 | ..Default::default() | |
298 | }; | |
299 | ||
300 | let mount = SystemdMountSection { | |
301 | What: what.to_string(), | |
c960d2b5 | 302 | Where: mount_point.to_string(), |
d4f2397d DM |
303 | Type: Some(fs_type.to_string()), |
304 | Options: Some(String::from("defaults")), | |
305 | ..Default::default() | |
306 | }; | |
307 | ||
308 | let mut config = SectionConfigData::new(); | |
309 | config.set_data("Unit", "Unit", unit)?; | |
310 | config.set_data("Install", "Install", install)?; | |
311 | config.set_data("Mount", "Mount", mount)?; | |
312 | ||
313 | systemd::config::save_systemd_mount(&mount_unit_path, &config)?; | |
314 | ||
c960d2b5 | 315 | Ok(mount_unit_name) |
d4f2397d | 316 | } |