]> git.proxmox.com Git - proxmox-backup.git/blame - src/tools.rs
ui: datastores: hide "no datastore" box by default
[proxmox-backup.git] / src / tools.rs
CommitLineData
51b499db
DM
1//! Tools and utilities
2//!
3//! This is a collection of small and useful tools.
6100071f
WB
4use std::any::Any;
5use std::collections::HashMap;
62ee2eb4 6use std::hash::BuildHasher;
98c259b4 7use std::fs::File;
b649887e 8use std::io::{self, BufRead, Read, Seek, SeekFrom};
98c259b4 9use std::os::unix::io::RawFd;
6100071f 10use std::path::Path;
365bb90f 11
f7d4e4b5 12use anyhow::{bail, format_err, Error};
af926291 13use serde_json::Value;
c5946faf 14use openssl::hash::{hash, DigestBytes, MessageDigest};
8a1028e0 15use percent_encoding::AsciiSet;
0fe5d605 16
00ec8d16
WB
17pub use proxmox::tools::fd::Fd;
18
6100071f 19pub mod acl;
e6513bd5 20pub mod apt;
556eb70e 21pub mod async_io;
6ed25cbe 22pub mod borrow;
ec01eead 23pub mod cert;
6100071f 24pub mod daemon;
10effc98 25pub mod disks;
4939255f 26pub mod format;
fb01fd3a
WB
27pub mod fs;
28pub mod fuse_loop;
29pub mod http;
8074d2b0 30pub mod logrotate;
45f9b32e 31pub mod loopdev;
fb01fd3a
WB
32pub mod lru_cache;
33pub mod nom;
34pub mod runtime;
97168f92 35pub mod socket;
fb01fd3a 36pub mod statistics;
7b22fb25 37pub mod subscription;
fb01fd3a
WB
38pub mod systemd;
39pub mod ticket;
40pub mod xattr;
fdce52aa 41pub mod zip;
f1d99e3f 42
fb01fd3a
WB
43pub mod parallel_handler;
44pub use parallel_handler::ParallelHandler;
3c9b3702 45
f1d99e3f 46mod wrapped_reader_stream;
fb01fd3a 47pub use wrapped_reader_stream::{AsyncReaderStream, StdChannelStream, WrappedReaderStream};
dcd033a5 48
943479f5 49mod async_channel_writer;
fb01fd3a 50pub use async_channel_writer::AsyncChannelWriter;
943479f5 51
dcd033a5 52mod std_channel_writer;
fb01fd3a 53pub use std_channel_writer::StdChannelWriter;
8cf6e764 54
a650f503 55mod process_locker;
fb01fd3a 56pub use process_locker::{ProcessLocker, ProcessLockExclusiveGuard, ProcessLockSharedGuard};
a650f503 57
3b151414 58mod file_logger;
fb01fd3a 59pub use file_logger::{FileLogger, FileLogOptions};
3b151414 60
490be29e 61mod broadcast_future;
fb01fd3a 62pub use broadcast_future::{BroadcastData, BroadcastFuture};
490be29e 63
fded74d0 64/// The `BufferedRead` trait provides a single function
0a72e267
DM
65/// `buffered_read`. It returns a reference to an internal buffer. The
66/// purpose of this traid is to avoid unnecessary data copies.
fded74d0 67pub trait BufferedRead {
318564ac
DM
68 /// This functions tries to fill the internal buffers, then
69 /// returns a reference to the available data. It returns an empty
70 /// buffer if `offset` points to the end of the file.
0a72e267
DM
71 fn buffered_read(&mut self, offset: u64) -> Result<&[u8], Error>;
72}
73
f5f13ebc 74pub fn json_object_to_query(data: Value) -> Result<String, Error> {
f5f13ebc
DM
75 let mut query = url::form_urlencoded::Serializer::new(String::new());
76
77 let object = data.as_object().ok_or_else(|| {
78 format_err!("json_object_to_query: got wrong data type (expected object).")
79 })?;
80
81 for (key, value) in object {
82 match value {
6100071f
WB
83 Value::Bool(b) => {
84 query.append_pair(key, &b.to_string());
85 }
86 Value::Number(n) => {
87 query.append_pair(key, &n.to_string());
88 }
89 Value::String(s) => {
90 query.append_pair(key, &s);
91 }
f5f13ebc
DM
92 Value::Array(arr) => {
93 for element in arr {
94 match element {
6100071f
WB
95 Value::Bool(b) => {
96 query.append_pair(key, &b.to_string());
97 }
98 Value::Number(n) => {
99 query.append_pair(key, &n.to_string());
100 }
101 Value::String(s) => {
102 query.append_pair(key, &s);
103 }
104 _ => bail!(
105 "json_object_to_query: unable to handle complex array data types."
106 ),
f5f13ebc
DM
107 }
108 }
109 }
110 _ => bail!("json_object_to_query: unable to handle complex data types."),
111 }
112 }
113
114 Ok(query.finish())
115}
116
0fe5d605 117pub fn required_string_param<'a>(param: &'a Value, name: &str) -> Result<&'a str, Error> {
6100071f 118 match param[name].as_str() {
0fe5d605
DM
119 Some(s) => Ok(s),
120 None => bail!("missing parameter '{}'", name),
121 }
122}
0d38dcb4 123
e17d5d86
DM
124pub fn required_string_property<'a>(param: &'a Value, name: &str) -> Result<&'a str, Error> {
125 match param[name].as_str() {
126 Some(s) => Ok(s),
127 None => bail!("missing property '{}'", name),
128 }
129}
130
0d38dcb4 131pub fn required_integer_param<'a>(param: &'a Value, name: &str) -> Result<i64, Error> {
6100071f 132 match param[name].as_i64() {
0d38dcb4
DM
133 Some(s) => Ok(s),
134 None => bail!("missing parameter '{}'", name),
f8dfbb45
DM
135 }
136}
137
e17d5d86
DM
138pub fn required_integer_property<'a>(param: &'a Value, name: &str) -> Result<i64, Error> {
139 match param[name].as_i64() {
140 Some(s) => Ok(s),
141 None => bail!("missing property '{}'", name),
142 }
143}
144
f8dfbb45 145pub fn required_array_param<'a>(param: &'a Value, name: &str) -> Result<Vec<Value>, Error> {
6100071f 146 match param[name].as_array() {
f8dfbb45
DM
147 Some(s) => Ok(s.to_vec()),
148 None => bail!("missing parameter '{}'", name),
0d38dcb4
DM
149 }
150}
383e8577 151
e17d5d86
DM
152pub fn required_array_property<'a>(param: &'a Value, name: &str) -> Result<Vec<Value>, Error> {
153 match param[name].as_array() {
154 Some(s) => Ok(s.to_vec()),
155 None => bail!("missing property '{}'", name),
156 }
157}
158
62ee2eb4 159pub fn complete_file_name<S: BuildHasher>(arg: &str, _param: &HashMap<String, String, S>) -> Vec<String> {
383e8577
DM
160 let mut result = vec![];
161
6100071f 162 use nix::fcntl::AtFlags;
383e8577
DM
163 use nix::fcntl::OFlag;
164 use nix::sys::stat::Mode;
383e8577 165
62ee2eb4 166 let mut dirname = std::path::PathBuf::from(if arg.is_empty() { "./" } else { arg });
383e8577
DM
167
168 let is_dir = match nix::sys::stat::fstatat(libc::AT_FDCWD, &dirname, AtFlags::empty()) {
169 Ok(stat) => (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR,
170 Err(_) => false,
171 };
172
173 if !is_dir {
174 if let Some(parent) = dirname.parent() {
175 dirname = parent.to_owned();
176 }
177 }
178
6100071f
WB
179 let mut dir =
180 match nix::dir::Dir::openat(libc::AT_FDCWD, &dirname, OFlag::O_DIRECTORY, Mode::empty()) {
181 Ok(d) => d,
182 Err(_) => return result,
183 };
383e8577
DM
184
185 for item in dir.iter() {
186 if let Ok(entry) = item {
187 if let Ok(name) = entry.file_name().to_str() {
6100071f
WB
188 if name == "." || name == ".." {
189 continue;
190 }
383e8577
DM
191 let mut newpath = dirname.clone();
192 newpath.push(name);
193
6100071f
WB
194 if let Ok(stat) =
195 nix::sys::stat::fstatat(libc::AT_FDCWD, &newpath, AtFlags::empty())
196 {
383e8577
DM
197 if (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR {
198 newpath.push("");
199 if let Some(newpath) = newpath.to_str() {
200 result.push(newpath.to_owned());
201 }
202 continue;
6100071f 203 }
383e8577
DM
204 }
205 if let Some(newpath) = newpath.to_str() {
206 result.push(newpath.to_owned());
207 }
6100071f 208 }
383e8577
DM
209 }
210 }
211
212 result
213}
443f3743
DM
214
215/// Scan directory for matching file names.
216///
217/// Scan through all directory entries and call `callback()` function
218/// if the entry name matches the regular expression. This function
219/// used unix `openat()`, so you can pass absolute or relative file
220/// names. This function simply skips non-UTF8 encoded names.
221pub fn scandir<P, F>(
222 dirfd: RawFd,
121f18ef 223 path: &P,
443f3743 224 regex: &regex::Regex,
6100071f 225 mut callback: F,
443f3743 226) -> Result<(), Error>
6100071f
WB
227where
228 F: FnMut(RawFd, &str, nix::dir::Type) -> Result<(), Error>,
229 P: ?Sized + nix::NixPath,
443f3743 230{
121f18ef 231 for entry in self::fs::scan_subdir(dirfd, path, regex)? {
443f3743
DM
232 let entry = entry?;
233 let file_type = match entry.file_type() {
234 Some(file_type) => file_type,
235 None => bail!("unable to detect file type"),
236 };
443f3743 237
6100071f
WB
238 callback(
239 entry.parent_fd(),
240 unsafe { entry.file_name_utf8_unchecked() },
241 file_type,
242 )?;
443f3743
DM
243 }
244 Ok(())
245}
7e13b2d6 246
c5946faf
WB
247/// Shortcut for md5 sums.
248pub fn md5sum(data: &[u8]) -> Result<DigestBytes, Error> {
249 hash(MessageDigest::md5(), data).map_err(Error::from)
250}
251
7e13b2d6 252pub fn get_hardware_address() -> Result<String, Error> {
1631c54f 253 static FILENAME: &str = "/etc/ssh/ssh_host_rsa_key.pub";
7e13b2d6 254
72c0e102
TL
255 let contents = proxmox::tools::fs::file_get_contents(FILENAME)
256 .map_err(|e| format_err!("Error getting host key - {}", e))?;
257 let digest = md5sum(&contents)
258 .map_err(|e| format_err!("Error digesting host key - {}", e))?;
7e13b2d6 259
52fe9e8e 260 Ok(proxmox::tools::bin_to_hex(&digest).to_uppercase())
7e13b2d6 261}
22968600 262
af2fddea
DM
263pub fn assert_if_modified(digest1: &str, digest2: &str) -> Result<(), Error> {
264 if digest1 != digest2 {
6100071f 265 bail!("detected modified configuration - file changed by other user? Try again.");
af2fddea
DM
266 }
267 Ok(())
268}
b9903d63 269
09f12d1c 270/// Extract a specific cookie from cookie header.
b9903d63 271/// We assume cookie_name is already url encoded.
09f12d1c 272pub fn extract_cookie(cookie: &str, cookie_name: &str) -> Option<String> {
b9903d63 273 for pair in cookie.split(';') {
b9903d63
DM
274 let (name, value) = match pair.find('=') {
275 Some(i) => (pair[..i].trim(), pair[(i + 1)..].trim()),
276 None => return None, // Cookie format error
277 };
278
279 if name == cookie_name {
8a1028e0 280 use percent_encoding::percent_decode;
b9903d63
DM
281 if let Ok(value) = percent_decode(value.as_bytes()).decode_utf8() {
282 return Some(value.into());
283 } else {
284 return None; // Cookie format error
285 }
286 }
287 }
288
289 None
290}
af53186e
DM
291
292pub fn join(data: &Vec<String>, sep: char) -> String {
af53186e
DM
293 let mut list = String::new();
294
295 for item in data {
62ee2eb4 296 if !list.is_empty() {
6100071f
WB
297 list.push(sep);
298 }
af53186e
DM
299 list.push_str(item);
300 }
301
302 list
303}
ff7049d4 304
002a191a
DM
305/// Detect modified configuration files
306///
add5861e 307/// This function fails with a reasonable error message if checksums do not match.
002a191a
DM
308pub fn detect_modified_configuration_file(digest1: &[u8;32], digest2: &[u8;32]) -> Result<(), Error> {
309 if digest1 != digest2 {
310 bail!("detected modified configuration - file changed by other user? Try again.");
311 }
312 Ok(())
313}
314
3578d99f
DM
315/// normalize uri path
316///
317/// Do not allow ".", "..", or hidden files ".XXXX"
318/// Also remove empty path components
319pub fn normalize_uri_path(path: &str) -> Result<(String, Vec<&str>), Error> {
3578d99f
DM
320 let items = path.split('/');
321
322 let mut path = String::new();
323 let mut components = vec![];
324
325 for name in items {
6100071f
WB
326 if name.is_empty() {
327 continue;
328 }
62ee2eb4 329 if name.starts_with('.') {
3578d99f
DM
330 bail!("Path contains illegal components.");
331 }
332 path.push('/');
333 path.push_str(name);
334 components.push(name);
335 }
336
337 Ok((path, components))
338}
339
97fab7aa 340/// Helper to check result from std::process::Command output
143b6545
DM
341///
342/// The exit_code_check() function should return true if the exit code
343/// is considered successful.
344pub fn command_output(
345 output: std::process::Output,
144006fa 346 exit_code_check: Option<fn(i32) -> bool>,
e64b9f92 347) -> Result<Vec<u8>, Error> {
97fab7aa
DM
348
349 if !output.status.success() {
350 match output.status.code() {
351 Some(code) => {
143b6545
DM
352 let is_ok = match exit_code_check {
353 Some(check_fn) => check_fn(code),
354 None => code == 0,
355 };
356 if !is_ok {
97fab7aa
DM
357 let msg = String::from_utf8(output.stderr)
358 .map(|m| if m.is_empty() { String::from("no error message") } else { m })
359 .unwrap_or_else(|_| String::from("non utf8 error message (suppressed)"));
360
361 bail!("status code: {} - {}", code, msg);
362 }
363 }
364 None => bail!("terminated by signal"),
365 }
366 }
367
e64b9f92
DM
368 Ok(output.stdout)
369}
97fab7aa 370
e64b9f92
DM
371/// Helper to check result from std::process::Command output, returns String.
372///
373/// The exit_code_check() function should return true if the exit code
374/// is considered successful.
375pub fn command_output_as_string(
376 output: std::process::Output,
377 exit_code_check: Option<fn(i32) -> bool>,
378) -> Result<String, Error> {
379 let output = command_output(output, exit_code_check)?;
380 let output = String::from_utf8(output)?;
97fab7aa
DM
381 Ok(output)
382}
383
144006fa
DM
384pub fn run_command(
385 mut command: std::process::Command,
386 exit_code_check: Option<fn(i32) -> bool>,
387) -> Result<String, Error> {
388
389 let output = command.output()
390 .map_err(|err| format_err!("failed to execute {:?} - {}", command, err))?;
391
f254a270 392 let output = command_output_as_string(output, exit_code_check)
144006fa
DM
393 .map_err(|err| format_err!("command {:?} failed - {}", command, err))?;
394
395 Ok(output)
396}
97fab7aa 397
ff7049d4 398pub fn fd_change_cloexec(fd: RawFd, on: bool) -> Result<(), Error> {
6100071f 399 use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
ff7049d4
WB
400 let mut flags = FdFlag::from_bits(fcntl(fd, F_GETFD)?)
401 .ok_or_else(|| format_err!("unhandled file flags"))?; // nix crate is stupid this way...
402 flags.set(FdFlag::FD_CLOEXEC, on);
403 fcntl(fd, F_SETFD(flags))?;
404 Ok(())
405}
9136f857 406
9136f857
DM
407static mut SHUTDOWN_REQUESTED: bool = false;
408
409pub fn request_shutdown() {
6100071f
WB
410 unsafe {
411 SHUTDOWN_REQUESTED = true;
412 }
7a630df7 413 crate::server::server_shutdown();
9136f857
DM
414}
415
416#[inline(always)]
417pub fn shutdown_requested() -> bool {
418 unsafe { SHUTDOWN_REQUESTED }
419}
92da93b2
DM
420
421pub fn fail_on_shutdown() -> Result<(), Error> {
422 if shutdown_requested() {
423 bail!("Server shutdown requested - aborting task");
424 }
425 Ok(())
426}
d96bb7f1 427
c4044009
WB
428/// safe wrapper for `nix::unistd::pipe2` defaulting to `O_CLOEXEC` and guarding the file
429/// descriptors.
efd1536e
WB
430pub fn pipe() -> Result<(Fd, Fd), Error> {
431 let (pin, pout) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?;
432 Ok((Fd(pin), Fd(pout)))
433}
2edc341b 434
c4044009
WB
435/// safe wrapper for `nix::sys::socket::socketpair` defaulting to `O_CLOEXEC` and guarding the file
436/// descriptors.
437pub fn socketpair() -> Result<(Fd, Fd), Error> {
438 use nix::sys::socket;
439 let (pa, pb) = socket::socketpair(
440 socket::AddressFamily::Unix,
441 socket::SockType::Stream,
442 None,
443 socket::SockFlag::SOCK_CLOEXEC,
444 )?;
445 Ok((Fd(pa), Fd(pb)))
446}
447
448
2edc341b
DM
449/// An easy way to convert types to Any
450///
451/// Mostly useful to downcast trait objects (see RpcEnvironment).
452pub trait AsAny {
dd5495d6 453 fn as_any(&self) -> &dyn Any;
2edc341b
DM
454}
455
456impl<T: Any> AsAny for T {
6100071f
WB
457 fn as_any(&self) -> &dyn Any {
458 self
459 }
2edc341b 460}
8a1028e0
WB
461
462/// This used to be: `SIMPLE_ENCODE_SET` plus space, `"`, `#`, `<`, `>`, backtick, `?`, `{`, `}`
463pub const DEFAULT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS // 0..1f and 7e
464 // The SIMPLE_ENCODE_SET adds space and anything >= 0x7e (7e itself is already included above)
465 .add(0x20)
466 .add(0x7f)
467 // the DEFAULT_ENCODE_SET added:
468 .add(b' ')
469 .add(b'"')
470 .add(b'#')
471 .add(b'<')
472 .add(b'>')
473 .add(b'`')
474 .add(b'?')
475 .add(b'{')
476 .add(b'}');
386990ba
WB
477
478/// Get an iterator over lines of a file, skipping empty lines and comments (lines starting with a
479/// `#`).
480pub fn file_get_non_comment_lines<P: AsRef<Path>>(
481 path: P,
482) -> Result<impl Iterator<Item = io::Result<String>>, Error> {
483 let path = path.as_ref();
484
485 Ok(io::BufReader::new(
486 File::open(path).map_err(|err| format_err!("error opening {:?}: {}", path, err))?,
487 )
488 .lines()
489 .filter_map(|line| match line {
490 Ok(line) => {
491 let line = line.trim();
492 if line.is_empty() || line.starts_with('#') {
493 None
494 } else {
495 Some(Ok(line.to_string()))
496 }
497 }
498 Err(err) => Some(Err(err)),
499 }))
500}
e693818a 501
ac7513e3
DM
502pub fn setup_safe_path_env() {
503 std::env::set_var("PATH", "/sbin:/bin:/usr/sbin:/usr/bin");
504 // Make %ENV safer - as suggested by https://perldoc.perl.org/perlsec.html
505 for name in &["IFS", "CDPATH", "ENV", "BASH_ENV"] {
506 std::env::remove_var(name);
507 }
508}
cdf1da28
WB
509
510pub fn strip_ascii_whitespace(line: &[u8]) -> &[u8] {
511 let line = match line.iter().position(|&b| !b.is_ascii_whitespace()) {
512 Some(n) => &line[n..],
513 None => return &[],
514 };
515 match line.iter().rev().position(|&b| !b.is_ascii_whitespace()) {
516 Some(n) => &line[..(line.len() - n)],
517 None => &[],
518 }
519}
1bc1d81a
DM
520
521/// Seeks to start of file and computes the SHA256 hash
522pub fn compute_file_csum(file: &mut File) -> Result<([u8; 32], u64), Error> {
523
524 file.seek(SeekFrom::Start(0))?;
525
526 let mut hasher = openssl::sha::Sha256::new();
527 let mut buffer = proxmox::tools::vec::undefined(256*1024);
528 let mut size: u64 = 0;
529
530 loop {
531 let count = match file.read(&mut buffer) {
532 Ok(count) => count,
533 Err(ref err) if err.kind() == std::io::ErrorKind::Interrupted => {
534 continue;
535 }
536 Err(err) => return Err(err.into()),
537 };
538 if count == 0 {
539 break;
540 }
541 size += count as u64;
542 hasher.update(&buffer[..count]);
543 }
544
545 let csum = hasher.finish();
546
547 Ok((csum, size))
548}