Compare commits
46 Commits
Author | SHA1 | Date |
---|---|---|
Thomas Lamprecht | a67874b6ae | |
Thomas Lamprecht | 9402e9f357 | |
Thomas Lamprecht | b75bb5434e | |
Thomas Lamprecht | ec44c3113b | |
Thomas Lamprecht | cb21bf7454 | |
Dominik Csapak | a1cffef503 | |
Wolfgang Bumiller | 9b00099ead | |
Thomas Lamprecht | d2351f1a81 | |
Thomas Lamprecht | 869e4601b4 | |
Thomas Lamprecht | 238e5b573e | |
Thomas Lamprecht | 996680a336 | |
Thomas Lamprecht | 94f6127711 | |
Thomas Lamprecht | 3841301ee9 | |
Thomas Lamprecht | f406202825 | |
Stefan Reiter | ba50f57e93 | |
Thomas Lamprecht | 61a758f67d | |
Thomas Lamprecht | 847c27fbee | |
Thomas Lamprecht | 7d79f3d5f7 | |
Thomas Lamprecht | fa3fdea590 | |
Thomas Lamprecht | aa2cd76c58 | |
Thomas Lamprecht | e2d82c7d4d | |
Thomas Lamprecht | e9c2a34def | |
Thomas Lamprecht | 0fad95f032 | |
Stoiko Ivanov | 683595940b | |
Stoiko Ivanov | 40060c1fed | |
Stoiko Ivanov | 2abee30fdd | |
Thomas Lamprecht | 7cdc53bbf7 | |
Fabian Ebner | dac877252b | |
Fabian Ebner | dd749b0e47 | |
Fabian Ebner | f98c02cbc6 | |
Thomas Lamprecht | 218d7e3ec6 | |
Stefan Reiter | acefa2bb6e | |
Dietmar Maurer | 36551172f3 | |
Wolfgang Bumiller | c26f4ef385 | |
Wolfgang Bumiller | 60816a8a82 | |
Thomas Lamprecht | d7d09712ef | |
Thomas Lamprecht | 825f019226 | |
Dominik Csapak | ca5e5bb67f | |
Dominik Csapak | 8191ff150e | |
Thomas Lamprecht | f2aeb13c68 | |
Dietmar Maurer | ce76b4b3c2 | |
Dominik Csapak | 44b9d6f162 | |
Dietmar Maurer | 53e80e8aa2 | |
Dominik Csapak | f94aa5ceb1 | |
Dominik Csapak | 3e4b9868a0 | |
Thomas Lamprecht | 4d86df04a0 |
26
Cargo.toml
26
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "proxmox-backup"
|
||||
version = "1.1.9"
|
||||
version = "1.1.14"
|
||||
authors = [
|
||||
"Dietmar Maurer <dietmar@proxmox.com>",
|
||||
"Dominik Csapak <d.csapak@proxmox.com>",
|
||||
|
@ -52,15 +52,6 @@ pam-sys = "0.5"
|
|||
percent-encoding = "2.1"
|
||||
pin-utils = "0.1.0"
|
||||
pin-project = "1.0"
|
||||
pathpatterns = "0.1.2"
|
||||
proxmox = { version = "0.11.5", features = [ "sortable-macro", "api-macro" ] }
|
||||
#proxmox = { git = "git://git.proxmox.com/git/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] }
|
||||
#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro" ] }
|
||||
proxmox-fuse = "0.1.1"
|
||||
proxmox-http = { version = "0.2.1", features = [ "client", "http-helpers", "websocket" ] }
|
||||
#proxmox-http = { version = "0.2.0", path = "../proxmox/proxmox-http", features = [ "client", "http-helpers", "websocket" ] }
|
||||
pxar = { version = "0.10.1", features = [ "tokio-io" ] }
|
||||
#pxar = { path = "../pxar", features = [ "tokio-io" ] }
|
||||
regex = "1.2"
|
||||
rustyline = "7"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -82,7 +73,20 @@ zstd = { version = "0.4", features = [ "bindgen" ] }
|
|||
nom = "5.1"
|
||||
crossbeam-channel = "0.5"
|
||||
|
||||
proxmox-acme-rs = "0.2.1"
|
||||
pathpatterns = "0.1.2"
|
||||
pxar = { version = "0.10.1", features = [ "tokio-io" ] }
|
||||
|
||||
proxmox = { version = "0.11.6", features = [ "sortable-macro", "api-macro", "cli", "router", "tfa" ] }
|
||||
proxmox-acme-rs = "0.3"
|
||||
proxmox-fuse = "0.1.1"
|
||||
proxmox-http = { version = "0.2.1", features = [ "client", "http-helpers", "websocket" ] }
|
||||
|
||||
# Local path overrides
|
||||
# NOTE: You must run `cargo update` after changing this for it to take effect!
|
||||
[patch.crates-io]
|
||||
#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro", "cli", "router", "tfa" ] }
|
||||
#proxmox-http = { path = "../proxmox/proxmox-http", features = [ "client", "http-helpers", "websocket" ] }
|
||||
#pxar = { path = "../pxar", features = [ "tokio-io" ] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
|
4
Makefile
4
Makefile
|
@ -113,7 +113,9 @@ deb: build
|
|||
lintian $(DEBS)
|
||||
|
||||
.PHONY: deb-all
|
||||
deb-all: $(DOC_DEB) $(DEBS)
|
||||
deb-all: build
|
||||
cd build; dpkg-buildpackage -b -us -uc --no-pre-clean
|
||||
lintian $(DEBS) $(DOC_DEB)
|
||||
|
||||
.PHONY: dsc
|
||||
dsc: $(DSC)
|
||||
|
|
25
build.rs
25
build.rs
|
@ -2,23 +2,22 @@
|
|||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
fn git_command(args: &[&str]) -> String {
|
||||
match Command::new("git").args(args).output() {
|
||||
Ok(output) => String::from_utf8(output.stdout).unwrap().trim_end().to_string(),
|
||||
Err(err) => {
|
||||
panic!("git {:?} failed: {}", args, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let repo_path = git_command(&["rev-parse", "--show-toplevel"]);
|
||||
let repoid = match env::var("REPOID") {
|
||||
Ok(repoid) => repoid,
|
||||
Err(_) => {
|
||||
match Command::new("git")
|
||||
.args(&["rev-parse", "HEAD"])
|
||||
.output()
|
||||
{
|
||||
Ok(output) => {
|
||||
String::from_utf8(output.stdout).unwrap()
|
||||
}
|
||||
Err(err) => {
|
||||
panic!("git rev-parse failed: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => git_command(&["rev-parse", "HEAD"]),
|
||||
};
|
||||
|
||||
println!("cargo:rustc-env=REPOID={}", repoid);
|
||||
println!("cargo:rerun-if-changed={}/.git/HEAD", repo_path);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,101 @@
|
|||
rust-proxmox-backup (1.1.9-1) unstable; urgency=medium
|
||||
rust-proxmox-backup (1.1.14-1) buster; urgency=medium
|
||||
|
||||
* drop RawWaker usage to avoid a leaking a refcount
|
||||
|
||||
* pbs-tools: LruCache: implement Drop to fix a memory leak for the cache
|
||||
|
||||
* ui: add notice for nearing PBS 1.1 End-of-Life
|
||||
|
||||
* backport "datastore: lookup: reuse ChunkStore on stale datastore re-open"
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 02 Jun 2022 18:07:54 +0200
|
||||
|
||||
rust-proxmox-backup (1.1.13-3) buster; urgency=medium
|
||||
|
||||
* fix sending log-rotation command to API daemons
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 19 Oct 2021 10:21:18 +0200
|
||||
|
||||
rust-proxmox-backup (1.1.13-2) buster; urgency=medium
|
||||
|
||||
* revert "auth: improve thread safety of 'crypt' C-library", not safe for
|
||||
Debian buster based releases.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Mon, 26 Jul 2021 16:40:07 +0200
|
||||
|
||||
rust-proxmox-backup (1.1.13-1) buster; urgency=medium
|
||||
|
||||
* auth: improve thread safety of 'crypt' C-library
|
||||
|
||||
* file-restore: increase lock timeout on QEMU map
|
||||
|
||||
* file restore daemon: log basic startup steps
|
||||
|
||||
* REST-API: set error message extension for bad-request response log to
|
||||
ensure the actual error is logged in any (access) log, making debugging
|
||||
such issues easier.
|
||||
|
||||
* restore daemon: use millisecond log resolution
|
||||
|
||||
* fix #3496: acme: plugin: actually sleep after setting the TXT record,
|
||||
ensuring DNS propagation of that record. This makes it catch up with the
|
||||
docs/web-interface, where the option was already available.
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 23 Jul 2021 12:34:29 +0200
|
||||
|
||||
rust-proxmox-backup (1.1.12-1) buster; urgency=medium
|
||||
|
||||
* subscription: set higher-level error to message instead of bailing out, to
|
||||
ensure a force-check gets through
|
||||
|
||||
* ui: dashboard: datastore stats: fix closing <i> tag
|
||||
|
||||
* ui: datastore: option view: only navigate up when we actually removed the
|
||||
datastore
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 09 Jul 2021 12:56:35 +0200
|
||||
|
||||
rust-proxmox-backup (1.1.11-1) buster; urgency=medium
|
||||
|
||||
* tape/drive: fix logging when requesting media
|
||||
|
||||
* tape: fix LTO locate_file for HP drives
|
||||
|
||||
* fix #3393 (again): pxar/create: try to read xattrs/fcaps/acls by default
|
||||
|
||||
* proxmox-backup-manager: show task log on datastore create
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 30 Jun 2021 11:24:20 +0200
|
||||
|
||||
rust-proxmox-backup (1.1.10-1) buster; urgency=medium
|
||||
|
||||
* ui: datastore list summary: catch and show errors per datastore
|
||||
|
||||
* ui: dashboard: task summary: add a 'close' tool to the header
|
||||
|
||||
* ensure that backups which are currently being restored or backed up to a
|
||||
tape won't get pruned
|
||||
|
||||
* improve error handling when locking a tape drive for a backup job
|
||||
|
||||
* client/pull: log snapshots that are skipped because of creation time being
|
||||
older than last sync time
|
||||
|
||||
* ui: datastore options: add remove button to drop a datastore from the
|
||||
configuration, without removing any actual data
|
||||
|
||||
* ui: tape: drive selector: do not autoselect the drive
|
||||
|
||||
* ui: tape: backup job: use correct default value for pbsUserSelector
|
||||
|
||||
* fix #3433: disks: port over Proxmox VE's S.M.A.R.T wearout logic
|
||||
|
||||
* backup: add helpers for async last recently used (LRU) caches for chunk
|
||||
and index reading of backup snapshot
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Wed, 16 Jun 2021 09:46:15 +0200
|
||||
|
||||
rust-proxmox-backup (1.1.9-1) stable; urgency=medium
|
||||
|
||||
* lto/sg_tape/encryption: remove non lto-4 supported byte
|
||||
|
||||
|
|
|
@ -39,10 +39,13 @@ Build-Depends: debhelper (>= 11),
|
|||
librust-percent-encoding-2+default-dev (>= 2.1-~~),
|
||||
librust-pin-project-1+default-dev,
|
||||
librust-pin-utils-0.1+default-dev,
|
||||
librust-proxmox-0.11+api-macro-dev (>= 0.11.5-~~),
|
||||
librust-proxmox-0.11+default-dev (>= 0.11.5-~~),
|
||||
librust-proxmox-0.11+sortable-macro-dev (>= 0.11.5-~~),
|
||||
librust-proxmox-acme-rs-0.2+default-dev (>= 0.2.1-~~),
|
||||
librust-proxmox-0.11+api-macro-dev (>= 0.11.6-~~),
|
||||
librust-proxmox-0.11+cli-dev (>= 0.11.6-~~),
|
||||
librust-proxmox-0.11+default-dev (>= 0.11.6-~~),
|
||||
librust-proxmox-0.11+router-dev (>= 0.11.6-~~),
|
||||
librust-proxmox-0.11+sortable-macro-dev (>= 0.11.6-~~),
|
||||
librust-proxmox-0.11+tfa-dev (>= 0.11.6-~~),
|
||||
librust-proxmox-acme-rs-0.3+default-dev,
|
||||
librust-proxmox-fuse-0.1+default-dev (>= 0.1.1-~~),
|
||||
librust-proxmox-http-0.2+client-dev (>= 0.2.1-~~),
|
||||
librust-proxmox-http-0.2+default-dev (>= 0.2.1-~~),
|
||||
|
@ -125,7 +128,7 @@ Depends: fonts-font-awesome,
|
|||
postfix | mail-transport-agent,
|
||||
proxmox-backup-docs,
|
||||
proxmox-mini-journalreader,
|
||||
proxmox-widget-toolkit (>= 2.5-6),
|
||||
proxmox-widget-toolkit (>= 2.6-2),
|
||||
pve-xtermjs (>= 4.7.0-1),
|
||||
sg3-utils,
|
||||
smartmontools,
|
||||
|
|
|
@ -12,7 +12,7 @@ Depends: fonts-font-awesome,
|
|||
postfix | mail-transport-agent,
|
||||
proxmox-backup-docs,
|
||||
proxmox-mini-journalreader,
|
||||
proxmox-widget-toolkit (>= 2.5-6),
|
||||
proxmox-widget-toolkit (>= 2.6-2),
|
||||
pve-xtermjs (>= 4.7.0-1),
|
||||
sg3-utils,
|
||||
smartmontools,
|
||||
|
|
|
@ -228,6 +228,7 @@ epub3: ${GENERATED_SYNOPSIS}
|
|||
|
||||
clean:
|
||||
rm -r -f *~ *.1 ${BUILDDIR} ${GENERATED_SYNOPSIS} api-viewer/apidata.js
|
||||
rm -f api-viewer/apidoc.js lto-barcode/lto-barcode-generator.js
|
||||
|
||||
|
||||
install_manual_pages: ${MAN1_PAGES} ${MAN5_PAGES}
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::future::Future;
|
|||
use std::pin::Pin;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use hyper::{Body, Request, Response};
|
||||
|
@ -68,7 +69,7 @@ fn extract_challenge<'a>(
|
|||
.challenges
|
||||
.iter()
|
||||
.find(|ch| ch.ty == ty)
|
||||
.ok_or_else(|| format_err!("no supported challenge type (dns-01) found"))
|
||||
.ok_or_else(|| format_err!("no supported challenge type ({}) found", ty))
|
||||
}
|
||||
|
||||
async fn pipe_to_tasklog<T: AsyncRead + Unpin>(
|
||||
|
@ -180,7 +181,21 @@ impl AcmePlugin for DnsPlugin {
|
|||
domain: &'d AcmeDomain,
|
||||
task: Arc<WorkerTask>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<&'c str, Error>> + Send + 'fut>> {
|
||||
Box::pin(self.action(client, authorization, domain, task, "setup"))
|
||||
Box::pin(async move {
|
||||
let result = self
|
||||
.action(client, authorization, domain, task.clone(), "setup")
|
||||
.await;
|
||||
let validation_delay = self.core.validation_delay.unwrap_or(30) as u64;
|
||||
|
||||
if validation_delay > 0 {
|
||||
task.log(format!(
|
||||
"Sleeping {} seconds to wait for TXT record propagation",
|
||||
validation_delay
|
||||
));
|
||||
tokio::time::sleep(Duration::from_secs(validation_delay)).await;
|
||||
}
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
fn teardown<'fut, 'a: 'fut, 'b: 'fut, 'c: 'fut, 'd: 'fut>(
|
||||
|
|
|
@ -66,6 +66,8 @@ pub fn list_disks(
|
|||
}
|
||||
}
|
||||
|
||||
list.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
|
|
|
@ -52,16 +52,20 @@ impl DataStore {
|
|||
|
||||
let mut map = DATASTORE_MAP.lock().unwrap();
|
||||
|
||||
if let Some(datastore) = map.get(name) {
|
||||
// reuse chunk store so that we keep using the same process locker instance!
|
||||
let chunk_store = if let Some(datastore) = map.get(name) {
|
||||
// Compare Config - if changed, create new Datastore object!
|
||||
if datastore.chunk_store.base == path &&
|
||||
datastore.verify_new == config.verify_new.unwrap_or(false)
|
||||
{
|
||||
return Ok(datastore.clone());
|
||||
}
|
||||
}
|
||||
Arc::clone(&datastore.chunk_store)
|
||||
} else {
|
||||
Arc::new(ChunkStore::open(name, &config.path)?)
|
||||
};
|
||||
|
||||
let datastore = DataStore::open_with_path(name, &path, config)?;
|
||||
let datastore = DataStore::open_with_path(chunk_store, config)?;
|
||||
|
||||
let datastore = Arc::new(datastore);
|
||||
map.insert(name.to_string(), datastore.clone());
|
||||
|
@ -81,9 +85,7 @@ impl DataStore {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn open_with_path(store_name: &str, path: &Path, config: DataStoreConfig) -> Result<Self, Error> {
|
||||
let chunk_store = ChunkStore::open(store_name, path)?;
|
||||
|
||||
fn open_with_path(chunk_store: Arc<ChunkStore>, config: DataStoreConfig) -> Result<Self, Error> {
|
||||
let mut gc_status_path = chunk_store.base_path();
|
||||
gc_status_path.push(".gc-status");
|
||||
|
||||
|
@ -100,7 +102,7 @@ impl DataStore {
|
|||
};
|
||||
|
||||
Ok(Self {
|
||||
chunk_store: Arc::new(chunk_store),
|
||||
chunk_store,
|
||||
gc_mutex: Mutex::new(()),
|
||||
last_gc_status: Mutex::new(gc_status),
|
||||
verify_new: config.verify_new.unwrap_or(false),
|
||||
|
|
|
@ -737,11 +737,11 @@ async fn command_reopen_logfiles() -> Result<(), Error> {
|
|||
// only care about the most recent daemon instance for each, proxy & api, as other older ones
|
||||
// should not respond to new requests anyway, but only finish their current one and then exit.
|
||||
let sock = server::our_ctrl_sock();
|
||||
let f1 = server::send_command(sock, "{\"command\":\"api-access-log-reopen\"}\n");
|
||||
let f1 = server::send_raw_command(sock, "{\"command\":\"api-access-log-reopen\"}\n");
|
||||
|
||||
let pid = server::read_pid(buildcfg::PROXMOX_BACKUP_API_PID_FN)?;
|
||||
let sock = server::ctrl_sock_from_pid(pid);
|
||||
let f2 = server::send_command(sock, "{\"command\":\"api-access-log-reopen\"}\n");
|
||||
let f2 = server::send_raw_command(sock, "{\"command\":\"api-access-log-reopen\"}\n");
|
||||
|
||||
match futures::join!(f1, f2) {
|
||||
(Err(e1), Err(e2)) => Err(format_err!("reopen commands failed, proxy: {}; api: {}", e1, e2)),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
///! Daemon binary to run inside a micro-VM for secure single file restore of disk images
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use log::error;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{info, error};
|
||||
|
||||
use std::os::unix::{
|
||||
io::{FromRawFd, RawFd},
|
||||
|
@ -37,24 +37,31 @@ lazy_static! {
|
|||
/// This is expected to be run by 'proxmox-file-restore' within a mini-VM
|
||||
fn main() -> Result<(), Error> {
|
||||
if !Path::new(VM_DETECT_FILE).exists() {
|
||||
bail!(concat!(
|
||||
"This binary is not supposed to be run manually. ",
|
||||
"Please use 'proxmox-file-restore' instead."
|
||||
));
|
||||
bail!(
|
||||
"This binary is not supposed to be run manually, use 'proxmox-file-restore' instead."
|
||||
);
|
||||
}
|
||||
|
||||
// don't have a real syslog (and no persistance), so use env_logger to print to a log file (via
|
||||
// stdout to a serial terminal attached by QEMU)
|
||||
env_logger::from_env(env_logger::Env::default().default_filter_or("info"))
|
||||
.write_style(env_logger::WriteStyle::Never)
|
||||
.format_timestamp_millis()
|
||||
.init();
|
||||
|
||||
// the API may save some stuff there, e.g., the memcon tracking file
|
||||
// we do not care much, but it's way less headache to just create it
|
||||
std::fs::create_dir_all("/run/proxmox-backup")?;
|
||||
|
||||
// scan all attached disks now, before starting the API
|
||||
// this will panic and stop the VM if anything goes wrong
|
||||
info!("scanning all disks...");
|
||||
{
|
||||
let _disk_state = DISK_STATE.lock().unwrap();
|
||||
}
|
||||
|
||||
info!("disk scan complete, starting main runtime...");
|
||||
|
||||
proxmox_backup::tools::runtime::main(run())
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,11 @@ use proxmox::api::{api, cli::*, RpcEnvironment, ApiHandler};
|
|||
|
||||
use proxmox_backup::config;
|
||||
use proxmox_backup::api2::{self, types::* };
|
||||
use proxmox_backup::client::{
|
||||
connect_to_localhost,
|
||||
view_task_result,
|
||||
};
|
||||
use proxmox_backup::config::datastore::DIR_NAME_SCHEMA;
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
|
@ -67,6 +72,81 @@ fn show_datastore(param: Value, rpcenv: &mut dyn RpcEnvironment) -> Result<Value
|
|||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[api(
|
||||
protected: true,
|
||||
input: {
|
||||
properties: {
|
||||
name: {
|
||||
schema: DATASTORE_SCHEMA,
|
||||
},
|
||||
path: {
|
||||
schema: DIR_NAME_SCHEMA,
|
||||
},
|
||||
comment: {
|
||||
optional: true,
|
||||
schema: SINGLE_LINE_COMMENT_SCHEMA,
|
||||
},
|
||||
"notify-user": {
|
||||
optional: true,
|
||||
type: Userid,
|
||||
},
|
||||
"notify": {
|
||||
optional: true,
|
||||
schema: DATASTORE_NOTIFY_STRING_SCHEMA,
|
||||
},
|
||||
"gc-schedule": {
|
||||
optional: true,
|
||||
schema: GC_SCHEDULE_SCHEMA,
|
||||
},
|
||||
"prune-schedule": {
|
||||
optional: true,
|
||||
schema: PRUNE_SCHEDULE_SCHEMA,
|
||||
},
|
||||
"keep-last": {
|
||||
optional: true,
|
||||
schema: PRUNE_SCHEMA_KEEP_LAST,
|
||||
},
|
||||
"keep-hourly": {
|
||||
optional: true,
|
||||
schema: PRUNE_SCHEMA_KEEP_HOURLY,
|
||||
},
|
||||
"keep-daily": {
|
||||
optional: true,
|
||||
schema: PRUNE_SCHEMA_KEEP_DAILY,
|
||||
},
|
||||
"keep-weekly": {
|
||||
optional: true,
|
||||
schema: PRUNE_SCHEMA_KEEP_WEEKLY,
|
||||
},
|
||||
"keep-monthly": {
|
||||
optional: true,
|
||||
schema: PRUNE_SCHEMA_KEEP_MONTHLY,
|
||||
},
|
||||
"keep-yearly": {
|
||||
optional: true,
|
||||
schema: PRUNE_SCHEMA_KEEP_YEARLY,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Create new datastore config.
|
||||
async fn create_datastore(mut param: Value) -> Result<Value, Error> {
|
||||
|
||||
let output_format = extract_output_format(&mut param);
|
||||
|
||||
let mut client = connect_to_localhost()?;
|
||||
|
||||
let result = client.post(&"api2/json/config/datastore", Some(param)).await?;
|
||||
|
||||
view_task_result(&mut client, result, &output_format).await?;
|
||||
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
pub fn datastore_commands() -> CommandLineInterface {
|
||||
|
||||
let cmd_def = CliCommandMap::new()
|
||||
|
@ -77,7 +157,7 @@ pub fn datastore_commands() -> CommandLineInterface {
|
|||
.completion_cb("name", config::datastore::complete_datastore_name)
|
||||
)
|
||||
.insert("create",
|
||||
CliCommand::new(&api2::config::datastore::API_METHOD_CREATE_DATASTORE)
|
||||
CliCommand::new(&API_METHOD_CREATE_DATASTORE)
|
||||
.arg_param(&["name", "path"])
|
||||
)
|
||||
.insert("update",
|
||||
|
|
|
@ -50,7 +50,7 @@ impl VMStateMap {
|
|||
/// Acquire a lock on the state map and retrieve a deserialized version
|
||||
fn load() -> Result<Self, Error> {
|
||||
let mut file = Self::open_file_raw(true)?;
|
||||
lock_file(&mut file, true, Some(std::time::Duration::from_secs(5)))?;
|
||||
lock_file(&mut file, true, Some(std::time::Duration::from_secs(120)))?;
|
||||
let map = serde_json::from_reader(&file).unwrap_or_default();
|
||||
Ok(Self { map, file })
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Helper to start a QEMU VM for single file restore.
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::prelude::*;
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -11,10 +11,7 @@ use tokio::time;
|
|||
use nix::sys::signal::{kill, Signal};
|
||||
use nix::unistd::Pid;
|
||||
|
||||
use proxmox::tools::{
|
||||
fd::Fd,
|
||||
fs::{create_path, file_read_string, make_tmp_file, CreateOptions},
|
||||
};
|
||||
use proxmox::tools::fs::{create_path, file_read_string, make_tmp_file, CreateOptions};
|
||||
|
||||
use proxmox_backup::backup::backup_user;
|
||||
use proxmox_backup::client::{VsockClient, DEFAULT_VSOCK_PORT};
|
||||
|
@ -83,14 +80,14 @@ pub fn try_kill_vm(pid: i32) -> Result<(), Error> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_temp_initramfs(ticket: &str, debug: bool) -> Result<(Fd, String), Error> {
|
||||
async fn create_temp_initramfs(ticket: &str, debug: bool) -> Result<(File, String), Error> {
|
||||
use std::ffi::CString;
|
||||
use tokio::fs::File;
|
||||
|
||||
let (tmp_fd, tmp_path) =
|
||||
let (tmp_file, tmp_path) =
|
||||
make_tmp_file("/tmp/file-restore-qemu.initramfs.tmp", CreateOptions::new())?;
|
||||
nix::unistd::unlink(&tmp_path)?;
|
||||
tools::fd_change_cloexec(tmp_fd.0, false)?;
|
||||
tools::fd_change_cloexec(tmp_file.as_raw_fd(), false)?;
|
||||
|
||||
let initramfs = if debug {
|
||||
buildcfg::PROXMOX_BACKUP_INITRAMFS_DBG_FN
|
||||
|
@ -98,7 +95,7 @@ async fn create_temp_initramfs(ticket: &str, debug: bool) -> Result<(Fd, String)
|
|||
buildcfg::PROXMOX_BACKUP_INITRAMFS_FN
|
||||
};
|
||||
|
||||
let mut f = File::from_std(unsafe { std::fs::File::from_raw_fd(tmp_fd.0) });
|
||||
let mut f = File::from_std(tmp_file);
|
||||
let mut base = File::open(initramfs).await?;
|
||||
|
||||
tokio::io::copy(&mut base, &mut f).await?;
|
||||
|
@ -118,11 +115,10 @@ async fn create_temp_initramfs(ticket: &str, debug: bool) -> Result<(Fd, String)
|
|||
.await?;
|
||||
tools::cpio::append_trailer(&mut f).await?;
|
||||
|
||||
// forget the tokio file, we close the file descriptor via the returned Fd
|
||||
std::mem::forget(f);
|
||||
let tmp_file = f.into_std().await;
|
||||
let path = format!("/dev/fd/{}", &tmp_file.as_raw_fd());
|
||||
|
||||
let path = format!("/dev/fd/{}", &tmp_fd.0);
|
||||
Ok((tmp_fd, path))
|
||||
Ok((tmp_file, path))
|
||||
}
|
||||
|
||||
pub async fn start_vm(
|
||||
|
@ -145,9 +141,9 @@ pub async fn start_vm(
|
|||
validate_img_existance(debug)?;
|
||||
|
||||
let pid;
|
||||
let (pid_fd, pid_path) = make_tmp_file("/tmp/file-restore-qemu.pid.tmp", CreateOptions::new())?;
|
||||
let (mut pid_file, pid_path) = make_tmp_file("/tmp/file-restore-qemu.pid.tmp", CreateOptions::new())?;
|
||||
nix::unistd::unlink(&pid_path)?;
|
||||
tools::fd_change_cloexec(pid_fd.0, false)?;
|
||||
tools::fd_change_cloexec(pid_file.as_raw_fd(), false)?;
|
||||
|
||||
let (_ramfs_pid, ramfs_path) = create_temp_initramfs(ticket, debug).await?;
|
||||
|
||||
|
@ -195,7 +191,7 @@ pub async fn start_vm(
|
|||
},
|
||||
"-daemonize",
|
||||
"-pidfile",
|
||||
&format!("/dev/fd/{}", pid_fd.as_raw_fd()),
|
||||
&format!("/dev/fd/{}", pid_file.as_raw_fd()),
|
||||
"-name",
|
||||
PBS_VM_NAME,
|
||||
];
|
||||
|
@ -282,8 +278,6 @@ pub async fn start_vm(
|
|||
// at this point QEMU is already daemonized and running, so if anything fails we
|
||||
// technically leave behind a zombie-VM... this shouldn't matter, as it will stop
|
||||
// itself soon enough (timer), and the following operations are unlikely to fail
|
||||
let mut pid_file = unsafe { File::from_raw_fd(pid_fd.as_raw_fd()) };
|
||||
std::mem::forget(pid_fd); // FD ownership is now in pid_fd/File
|
||||
let mut pidstr = String::new();
|
||||
pid_file.read_to_string(&mut pidstr)?;
|
||||
pid = pidstr.trim_end().parse().map_err(|err| {
|
||||
|
|
|
@ -72,7 +72,7 @@ pub struct DnsPluginCore {
|
|||
///
|
||||
/// Allows to cope with long TTL of DNS records.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
validation_delay: Option<u32>,
|
||||
pub(crate) validation_delay: Option<u32>,
|
||||
|
||||
/// Flag to disable the config.
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
|
|
|
@ -169,7 +169,7 @@ where
|
|||
bail!("refusing to backup a virtual file system");
|
||||
}
|
||||
|
||||
let fs_feature_flags = Flags::from_magic(fs_magic);
|
||||
let mut fs_feature_flags = Flags::from_magic(fs_magic);
|
||||
|
||||
let stat = nix::sys::stat::fstat(source_dir.as_raw_fd())?;
|
||||
let metadata = get_metadata(
|
||||
|
@ -177,6 +177,7 @@ where
|
|||
&stat,
|
||||
feature_flags & fs_feature_flags,
|
||||
fs_magic,
|
||||
&mut fs_feature_flags,
|
||||
)
|
||||
.map_err(|err| format_err!("failed to get metadata for source directory: {}", err))?;
|
||||
|
||||
|
@ -533,7 +534,7 @@ impl Archiver {
|
|||
None => return Ok(()),
|
||||
};
|
||||
|
||||
let metadata = get_metadata(fd.as_raw_fd(), &stat, self.flags(), self.fs_magic)?;
|
||||
let metadata = get_metadata(fd.as_raw_fd(), &stat, self.flags(), self.fs_magic, &mut self.fs_feature_flags)?;
|
||||
|
||||
if self
|
||||
.patterns
|
||||
|
@ -742,7 +743,7 @@ impl Archiver {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_metadata(fd: RawFd, stat: &FileStat, flags: Flags, fs_magic: i64) -> Result<Metadata, Error> {
|
||||
fn get_metadata(fd: RawFd, stat: &FileStat, flags: Flags, fs_magic: i64, fs_feature_flags: &mut Flags) -> Result<Metadata, Error> {
|
||||
// required for some of these
|
||||
let proc_path = Path::new("/proc/self/fd/").join(fd.to_string());
|
||||
|
||||
|
@ -757,14 +758,14 @@ fn get_metadata(fd: RawFd, stat: &FileStat, flags: Flags, fs_magic: i64) -> Resu
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
get_xattr_fcaps_acl(&mut meta, fd, &proc_path, flags)?;
|
||||
get_xattr_fcaps_acl(&mut meta, fd, &proc_path, flags, fs_feature_flags)?;
|
||||
get_chattr(&mut meta, fd)?;
|
||||
get_fat_attr(&mut meta, fd, fs_magic)?;
|
||||
get_quota_project_id(&mut meta, fd, flags, fs_magic)?;
|
||||
Ok(meta)
|
||||
}
|
||||
|
||||
fn get_fcaps(meta: &mut Metadata, fd: RawFd, flags: Flags) -> Result<(), Error> {
|
||||
fn get_fcaps(meta: &mut Metadata, fd: RawFd, flags: Flags, fs_feature_flags: &mut Flags) -> Result<(), Error> {
|
||||
if !flags.contains(Flags::WITH_FCAPS) {
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -775,7 +776,10 @@ fn get_fcaps(meta: &mut Metadata, fd: RawFd, flags: Flags) -> Result<(), Error>
|
|||
Ok(())
|
||||
}
|
||||
Err(Errno::ENODATA) => Ok(()),
|
||||
Err(Errno::EOPNOTSUPP) => Ok(()),
|
||||
Err(Errno::EOPNOTSUPP) => {
|
||||
fs_feature_flags.remove(Flags::WITH_FCAPS);
|
||||
Ok(())
|
||||
}
|
||||
Err(Errno::EBADF) => Ok(()), // symlinks
|
||||
Err(err) => bail!("failed to read file capabilities: {}", err),
|
||||
}
|
||||
|
@ -786,6 +790,7 @@ fn get_xattr_fcaps_acl(
|
|||
fd: RawFd,
|
||||
proc_path: &Path,
|
||||
flags: Flags,
|
||||
fs_feature_flags: &mut Flags,
|
||||
) -> Result<(), Error> {
|
||||
if !flags.contains(Flags::WITH_XATTRS) {
|
||||
return Ok(());
|
||||
|
@ -793,19 +798,22 @@ fn get_xattr_fcaps_acl(
|
|||
|
||||
let xattrs = match xattr::flistxattr(fd) {
|
||||
Ok(names) => names,
|
||||
Err(Errno::EOPNOTSUPP) => return Ok(()),
|
||||
Err(Errno::EOPNOTSUPP) => {
|
||||
fs_feature_flags.remove(Flags::WITH_XATTRS);
|
||||
return Ok(());
|
||||
},
|
||||
Err(Errno::EBADF) => return Ok(()), // symlinks
|
||||
Err(err) => bail!("failed to read xattrs: {}", err),
|
||||
};
|
||||
|
||||
for attr in &xattrs {
|
||||
if xattr::is_security_capability(&attr) {
|
||||
get_fcaps(meta, fd, flags)?;
|
||||
get_fcaps(meta, fd, flags, fs_feature_flags)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if xattr::is_acl(&attr) {
|
||||
get_acl(meta, proc_path, flags)?;
|
||||
get_acl(meta, proc_path, flags, fs_feature_flags)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -910,7 +918,7 @@ fn get_quota_project_id(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn get_acl(metadata: &mut Metadata, proc_path: &Path, flags: Flags) -> Result<(), Error> {
|
||||
fn get_acl(metadata: &mut Metadata, proc_path: &Path, flags: Flags, fs_feature_flags: &mut Flags) -> Result<(), Error> {
|
||||
if !flags.contains(Flags::WITH_ACL) {
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -919,10 +927,10 @@ fn get_acl(metadata: &mut Metadata, proc_path: &Path, flags: Flags) -> Result<()
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
get_acl_do(metadata, proc_path, acl::ACL_TYPE_ACCESS)?;
|
||||
get_acl_do(metadata, proc_path, acl::ACL_TYPE_ACCESS, fs_feature_flags)?;
|
||||
|
||||
if metadata.is_dir() {
|
||||
get_acl_do(metadata, proc_path, acl::ACL_TYPE_DEFAULT)?;
|
||||
get_acl_do(metadata, proc_path, acl::ACL_TYPE_DEFAULT, fs_feature_flags)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -932,6 +940,7 @@ fn get_acl_do(
|
|||
metadata: &mut Metadata,
|
||||
proc_path: &Path,
|
||||
acl_type: acl::ACLType,
|
||||
fs_feature_flags: &mut Flags,
|
||||
) -> Result<(), Error> {
|
||||
// In order to be able to get ACLs with type ACL_TYPE_DEFAULT, we have
|
||||
// to create a path for acl_get_file(). acl_get_fd() only allows to get
|
||||
|
@ -939,7 +948,10 @@ fn get_acl_do(
|
|||
let acl = match acl::ACL::get_file(&proc_path, acl_type) {
|
||||
Ok(acl) => acl,
|
||||
// Don't bail if underlying endpoint does not support acls
|
||||
Err(Errno::EOPNOTSUPP) => return Ok(()),
|
||||
Err(Errno::EOPNOTSUPP) => {
|
||||
fs_feature_flags.remove(Flags::WITH_ACL);
|
||||
return Ok(());
|
||||
}
|
||||
// Don't bail if the endpoint cannot carry acls
|
||||
Err(Errno::EBADF) => return Ok(()),
|
||||
// Don't bail if there is no data
|
||||
|
|
|
@ -368,7 +368,10 @@ impl Flags {
|
|||
Flags::WITH_SYMLINKS |
|
||||
Flags::WITH_DEVICE_NODES |
|
||||
Flags::WITH_FIFOS |
|
||||
Flags::WITH_SOCKETS
|
||||
Flags::WITH_SOCKETS |
|
||||
Flags::WITH_XATTRS |
|
||||
Flags::WITH_ACL |
|
||||
Flags::WITH_FCAPS
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -152,14 +152,13 @@ fn log_response(
|
|||
let path = &path_query[..MAX_URI_QUERY_LENGTH.min(path_query.len())];
|
||||
|
||||
let status = resp.status();
|
||||
|
||||
if !(status.is_success() || status.is_informational()) {
|
||||
let reason = status.canonical_reason().unwrap_or("unknown reason");
|
||||
|
||||
let mut message = "request failed";
|
||||
if let Some(data) = resp.extensions().get::<ErrorMessageExtension>() {
|
||||
message = &data.0;
|
||||
}
|
||||
let message = match resp.extensions().get::<ErrorMessageExtension>() {
|
||||
Some(data) => &data.0,
|
||||
None => "request failed",
|
||||
};
|
||||
|
||||
log::error!(
|
||||
"{} {}: {} {}: [client {}] {}",
|
||||
|
@ -254,7 +253,10 @@ impl tower_service::Service<Request<Body>> for ApiService {
|
|||
Some(apierr) => (apierr.message.clone(), apierr.code),
|
||||
_ => (err.to_string(), StatusCode::BAD_REQUEST),
|
||||
};
|
||||
Response::builder().status(code).body(err.into())?
|
||||
Response::builder()
|
||||
.status(code)
|
||||
.extension(ErrorMessageExtension(err.to_string()))
|
||||
.body(err.into())?
|
||||
}
|
||||
};
|
||||
let logger = config.get_file_log();
|
||||
|
@ -561,7 +563,8 @@ async fn simple_static_file_download(
|
|||
let mut response = match compression {
|
||||
Some(CompressionMethod::Deflate) => {
|
||||
let mut enc = DeflateEncoder::with_quality(data, Level::Default);
|
||||
enc.compress_vec(&mut file, CHUNK_SIZE_LIMIT as usize).await?;
|
||||
enc.compress_vec(&mut file, CHUNK_SIZE_LIMIT as usize)
|
||||
.await?;
|
||||
let mut response = Response::new(enc.into_inner().into());
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_ENCODING,
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::fs::{File, OpenOptions};
|
|||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::path::Path;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use endian_trait::Endian;
|
||||
|
@ -122,6 +123,7 @@ pub struct LtoTapeStatus {
|
|||
|
||||
pub struct SgTape {
|
||||
file: File,
|
||||
locate_offset: Option<i64>,
|
||||
info: InquiryInfo,
|
||||
encryption_key_loaded: bool,
|
||||
}
|
||||
|
@ -145,6 +147,7 @@ impl SgTape {
|
|||
file,
|
||||
info,
|
||||
encryption_key_loaded: false,
|
||||
locate_offset: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -300,26 +303,76 @@ impl SgTape {
|
|||
return self.rewind();
|
||||
}
|
||||
|
||||
let position = position -1;
|
||||
const SPACE_ONE_FILEMARK: &[u8] = &[0x11, 0x01, 0, 0, 1, 0];
|
||||
|
||||
// Special case for position 1, because LOCATE 0 does not work
|
||||
if position == 1 {
|
||||
self.rewind()?;
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
sg_raw.do_command(SPACE_ONE_FILEMARK)
|
||||
.map_err(|err| format_err!("locate file {} (space) failed - {}", position, err))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut sg_raw = SgRaw::new(&mut self.file, 16)?;
|
||||
sg_raw.set_timeout(Self::SCSI_TAPE_DEFAULT_TIMEOUT);
|
||||
let mut cmd = Vec::new();
|
||||
|
||||
// Note: LOCATE(16) works for LTO4 or newer
|
||||
//
|
||||
// It seems the LOCATE command behaves slightly different across vendors
|
||||
// e.g. for IBM drives, LOCATE 1 moves to File #2, but
|
||||
// for HP drives, LOCATE 1 move to File #1
|
||||
|
||||
let fixed_position = if let Some(locate_offset) = self.locate_offset {
|
||||
if locate_offset < 0 {
|
||||
position.saturating_sub((-locate_offset) as u64)
|
||||
} else {
|
||||
position.saturating_add(locate_offset as u64)
|
||||
}
|
||||
} else {
|
||||
position
|
||||
};
|
||||
// always sub(1), so that it works for IBM drives without locate_offset
|
||||
let fixed_position = fixed_position.saturating_sub(1);
|
||||
|
||||
let mut cmd = Vec::new();
|
||||
cmd.extend(&[0x92, 0b000_01_000, 0, 0]); // LOCATE(16) filemarks
|
||||
cmd.extend(&position.to_be_bytes());
|
||||
cmd.extend(&fixed_position.to_be_bytes());
|
||||
cmd.extend(&[0, 0, 0, 0]);
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
.map_err(|err| format_err!("locate file {} failed - {}", position, err))?;
|
||||
|
||||
// move to other side of filemark
|
||||
cmd.truncate(0);
|
||||
cmd.extend(&[0x11, 0x01, 0, 0, 1, 0]); // SPACE(6) one filemarks
|
||||
|
||||
sg_raw.do_command(&cmd)
|
||||
// LOCATE always position at the BOT side of the filemark, so
|
||||
// we need to move to other side of filemark
|
||||
sg_raw.do_command(SPACE_ONE_FILEMARK)
|
||||
.map_err(|err| format_err!("locate file {} (space) failed - {}", position, err))?;
|
||||
|
||||
if self.locate_offset.is_none() {
|
||||
// check if we landed at correct position
|
||||
let current_file = self.current_file_number()?;
|
||||
if current_file != position {
|
||||
let offset: i64 =
|
||||
i64::try_from((position as i128) - (current_file as i128)).map_err(|err| {
|
||||
format_err!(
|
||||
"locate_file: offset between {} and {} invalid: {}",
|
||||
position,
|
||||
current_file,
|
||||
err
|
||||
)
|
||||
})?;
|
||||
self.locate_offset = Some(offset);
|
||||
self.locate_file(position)?;
|
||||
let current_file = self.current_file_number()?;
|
||||
if current_file != position {
|
||||
bail!("locate_file: compensating offset did not work, aborting...");
|
||||
}
|
||||
} else {
|
||||
self.locate_offset = Some(0);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -321,6 +321,37 @@ pub fn open_drive(
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum TapeRequestError {
|
||||
None,
|
||||
EmptyTape,
|
||||
OpenFailed(String),
|
||||
WrongLabel(String),
|
||||
ReadFailed(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TapeRequestError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TapeRequestError::None => {
|
||||
write!(f, "no error")
|
||||
},
|
||||
TapeRequestError::OpenFailed(reason) => {
|
||||
write!(f, "tape open failed - {}", reason)
|
||||
}
|
||||
TapeRequestError::WrongLabel(label) => {
|
||||
write!(f, "wrong media label {}", label)
|
||||
}
|
||||
TapeRequestError::EmptyTape => {
|
||||
write!(f, "found empty media without label (please label all tapes first)")
|
||||
}
|
||||
TapeRequestError::ReadFailed(reason) => {
|
||||
write!(f, "tape read failed - {}", reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests a specific 'media' to be inserted into 'drive'. Within a
|
||||
/// loop, this then tries to read the media label and waits until it
|
||||
/// finds the requested media.
|
||||
|
@ -388,49 +419,62 @@ pub fn request_and_load_media(
|
|||
return Ok((handle, media_id));
|
||||
}
|
||||
|
||||
let mut last_media_uuid = None;
|
||||
let mut last_error = None;
|
||||
let mut last_error = TapeRequestError::None;
|
||||
|
||||
let mut tried = false;
|
||||
let mut failure_reason = None;
|
||||
let update_and_log_request_error =
|
||||
|old: &mut TapeRequestError, new: TapeRequestError| -> Result<(), Error>
|
||||
{
|
||||
if new != *old {
|
||||
task_log!(worker, "{}", new);
|
||||
task_log!(
|
||||
worker,
|
||||
"Please insert media '{}' into drive '{}'",
|
||||
label_text,
|
||||
drive
|
||||
);
|
||||
if let Some(to) = notify_email {
|
||||
send_load_media_email(
|
||||
drive,
|
||||
&label_text,
|
||||
to,
|
||||
Some(new.to_string()),
|
||||
)?;
|
||||
}
|
||||
*old = new;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
loop {
|
||||
worker.check_abort()?;
|
||||
|
||||
if tried {
|
||||
if let Some(reason) = failure_reason {
|
||||
task_log!(worker, "Please insert media '{}' into drive '{}'", label_text, drive);
|
||||
if let Some(to) = notify_email {
|
||||
send_load_media_email(drive, &label_text, to, Some(reason))?;
|
||||
}
|
||||
}
|
||||
|
||||
failure_reason = None;
|
||||
|
||||
if last_error != TapeRequestError::None {
|
||||
for _ in 0..50 { // delay 5 seconds
|
||||
worker.check_abort()?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
} else {
|
||||
task_log!(
|
||||
worker,
|
||||
"Checking for media '{}' in drive '{}'",
|
||||
label_text,
|
||||
drive
|
||||
);
|
||||
}
|
||||
|
||||
tried = true;
|
||||
|
||||
let mut handle = match drive_config.open() {
|
||||
Ok(handle) => handle,
|
||||
Err(err) => {
|
||||
let err = err.to_string();
|
||||
if Some(err.clone()) != last_error {
|
||||
task_log!(worker, "tape open failed - {}", err);
|
||||
last_error = Some(err);
|
||||
failure_reason = last_error.clone();
|
||||
}
|
||||
update_and_log_request_error(
|
||||
&mut last_error,
|
||||
TapeRequestError::OpenFailed(err.to_string()),
|
||||
)?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match handle.read_label() {
|
||||
Ok((Some(media_id), _)) => {
|
||||
if media_id.label.uuid == label.uuid {
|
||||
let request_error = match handle.read_label() {
|
||||
Ok((Some(media_id), _)) if media_id.label.uuid == label.uuid => {
|
||||
task_log!(
|
||||
worker,
|
||||
"found media label {} ({})",
|
||||
|
@ -438,34 +482,24 @@ pub fn request_and_load_media(
|
|||
media_id.label.uuid.to_string(),
|
||||
);
|
||||
return Ok((Box::new(handle), media_id));
|
||||
} else if Some(media_id.label.uuid.clone()) != last_media_uuid {
|
||||
let err = format!(
|
||||
"wrong media label {} ({})",
|
||||
}
|
||||
Ok((Some(media_id), _)) => {
|
||||
let label_string = format!(
|
||||
"{} ({})",
|
||||
media_id.label.label_text,
|
||||
media_id.label.uuid.to_string(),
|
||||
);
|
||||
task_log!(worker, "{}", err);
|
||||
last_media_uuid = Some(media_id.label.uuid);
|
||||
failure_reason = Some(err);
|
||||
}
|
||||
TapeRequestError::WrongLabel(label_string)
|
||||
}
|
||||
Ok((None, _)) => {
|
||||
if last_media_uuid.is_some() {
|
||||
let err = "found empty media without label (please label all tapes first)";
|
||||
task_log!(worker, "{}", err);
|
||||
last_media_uuid = None;
|
||||
failure_reason = Some(err.to_string());
|
||||
}
|
||||
TapeRequestError::EmptyTape
|
||||
}
|
||||
Err(err) => {
|
||||
let err = err.to_string();
|
||||
if Some(err.clone()) != last_error {
|
||||
task_log!(worker, "tape open failed - {}", err);
|
||||
last_error = Some(err);
|
||||
failure_reason = last_error.clone();
|
||||
}
|
||||
}
|
||||
TapeRequestError::ReadFailed(err.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
update_and_log_request_error(&mut last_error, request_error)?;
|
||||
}
|
||||
}
|
||||
_ => bail!("drive type '{}' not implemented!"),
|
||||
|
|
|
@ -46,6 +46,19 @@ pub struct DiskManage {
|
|||
mounted_devices: OnceCell<HashSet<dev_t>>,
|
||||
}
|
||||
|
||||
/// Information for a device as returned by lsblk.
|
||||
#[derive(Deserialize)]
|
||||
pub struct LsblkInfo {
|
||||
/// Path to the device.
|
||||
path: String,
|
||||
/// Partition type GUID.
|
||||
#[serde(rename = "parttype")]
|
||||
partition_type: Option<String>,
|
||||
/// File system label.
|
||||
#[serde(rename = "fstype")]
|
||||
file_system_type: Option<String>,
|
||||
}
|
||||
|
||||
impl DiskManage {
|
||||
/// Create a new disk management context.
|
||||
pub fn new() -> Arc<Self> {
|
||||
|
@ -555,32 +568,36 @@ pub struct BlockDevStat {
|
|||
pub io_ticks: u64, // milliseconds
|
||||
}
|
||||
|
||||
/// Use lsblk to read partition type uuids.
|
||||
pub fn get_partition_type_info() -> Result<HashMap<String, Vec<String>>, Error> {
|
||||
/// Use lsblk to read partition type uuids and file system types.
|
||||
pub fn get_lsblk_info() -> Result<Vec<LsblkInfo>, Error> {
|
||||
|
||||
let mut command = std::process::Command::new("lsblk");
|
||||
command.args(&["--json", "-o", "path,parttype"]);
|
||||
command.args(&["--json", "-o", "path,parttype,fstype"]);
|
||||
|
||||
let output = crate::tools::run_command(command, None)?;
|
||||
|
||||
let mut res: HashMap<String, Vec<String>> = HashMap::new();
|
||||
let mut output: serde_json::Value = output.parse()?;
|
||||
|
||||
let output: serde_json::Value = output.parse()?;
|
||||
if let Some(list) = output["blockdevices"].as_array() {
|
||||
for info in list {
|
||||
let path = match info["path"].as_str() {
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
let partition_type = match info["parttype"].as_str() {
|
||||
Some(t) => t.to_owned(),
|
||||
None => continue,
|
||||
};
|
||||
let devices = res.entry(partition_type).or_insert(Vec::new());
|
||||
devices.push(path.to_string());
|
||||
Ok(serde_json::from_value(output["blockdevices"].take())?)
|
||||
}
|
||||
|
||||
/// Get set of devices with a file system label.
|
||||
///
|
||||
/// The set is indexed by using the unix raw device number (dev_t is u64)
|
||||
fn get_file_system_devices(
|
||||
lsblk_info: &[LsblkInfo],
|
||||
) -> Result<HashSet<u64>, Error> {
|
||||
|
||||
let mut device_set: HashSet<u64> = HashSet::new();
|
||||
|
||||
for info in lsblk_info.iter() {
|
||||
if info.file_system_type.is_some() {
|
||||
let meta = std::fs::metadata(&info.path)?;
|
||||
device_set.insert(meta.rdev());
|
||||
}
|
||||
}
|
||||
Ok(res)
|
||||
|
||||
Ok(device_set)
|
||||
}
|
||||
|
||||
#[api()]
|
||||
|
@ -599,6 +616,8 @@ pub enum DiskUsageType {
|
|||
DeviceMapper,
|
||||
/// Disk has partitions
|
||||
Partitions,
|
||||
/// Disk contains a file system label
|
||||
FileSystem,
|
||||
}
|
||||
|
||||
#[api(
|
||||
|
@ -736,14 +755,16 @@ pub fn get_disks(
|
|||
|
||||
let disk_manager = DiskManage::new();
|
||||
|
||||
let partition_type_map = get_partition_type_info()?;
|
||||
let lsblk_info = get_lsblk_info()?;
|
||||
|
||||
let zfs_devices = zfs_devices(&partition_type_map, None).or_else(|err| -> Result<HashSet<u64>, Error> {
|
||||
let zfs_devices = zfs_devices(&lsblk_info, None).or_else(|err| -> Result<HashSet<u64>, Error> {
|
||||
eprintln!("error getting zfs devices: {}", err);
|
||||
Ok(HashSet::new())
|
||||
})?;
|
||||
|
||||
let lvm_devices = get_lvm_devices(&partition_type_map)?;
|
||||
let lvm_devices = get_lvm_devices(&lsblk_info)?;
|
||||
|
||||
let file_system_devices = get_file_system_devices(&lsblk_info)?;
|
||||
|
||||
// fixme: ceph journals/volumes
|
||||
|
||||
|
@ -820,6 +841,10 @@ pub fn get_disks(
|
|||
};
|
||||
}
|
||||
|
||||
if usage == DiskUsageType::Unused && file_system_devices.contains(&devnum) {
|
||||
usage = DiskUsageType::FileSystem;
|
||||
}
|
||||
|
||||
if usage == DiskUsageType::Unused && disk.has_holders()? {
|
||||
usage = DiskUsageType::DeviceMapper;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
use std::collections::{HashSet, HashMap};
|
||||
use std::collections::HashSet;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use anyhow::{Error};
|
||||
use serde_json::Value;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use super::LsblkInfo;
|
||||
|
||||
lazy_static!{
|
||||
static ref LVM_UUIDS: HashSet<&'static str> = {
|
||||
let mut set = HashSet::new();
|
||||
|
@ -17,7 +19,7 @@ lazy_static!{
|
|||
///
|
||||
/// The set is indexed by using the unix raw device number (dev_t is u64)
|
||||
pub fn get_lvm_devices(
|
||||
partition_type_map: &HashMap<String, Vec<String>>,
|
||||
lsblk_info: &[LsblkInfo],
|
||||
) -> Result<HashSet<u64>, Error> {
|
||||
|
||||
const PVS_BIN_PATH: &str = "pvs";
|
||||
|
@ -29,14 +31,14 @@ pub fn get_lvm_devices(
|
|||
|
||||
let mut device_set: HashSet<u64> = HashSet::new();
|
||||
|
||||
for device_list in partition_type_map.iter()
|
||||
.filter_map(|(uuid, list)| if LVM_UUIDS.contains(uuid.as_str()) { Some(list) } else { None })
|
||||
{
|
||||
for device in device_list {
|
||||
let meta = std::fs::metadata(device)?;
|
||||
for info in lsblk_info.iter() {
|
||||
if let Some(partition_type) = &info.partition_type {
|
||||
if LVM_UUIDS.contains(partition_type.as_str()) {
|
||||
let meta = std::fs::metadata(&info.path)?;
|
||||
device_set.insert(meta.rdev());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output: Value = output.parse()?;
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use std::path::PathBuf;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashSet;
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
|
@ -67,12 +67,11 @@ pub fn zfs_pool_stats(pool: &OsStr) -> Result<Option<BlockDevStat>, Error> {
|
|||
Ok(Some(stat))
|
||||
}
|
||||
|
||||
|
||||
/// Get set of devices used by zfs (or a specific zfs pool)
|
||||
///
|
||||
/// The set is indexed by using the unix raw device number (dev_t is u64)
|
||||
pub fn zfs_devices(
|
||||
partition_type_map: &HashMap<String, Vec<String>>,
|
||||
lsblk_info: &[LsblkInfo],
|
||||
pool: Option<String>,
|
||||
) -> Result<HashSet<u64>, Error> {
|
||||
|
||||
|
@ -86,14 +85,14 @@ pub fn zfs_devices(
|
|||
}
|
||||
}
|
||||
|
||||
for device_list in partition_type_map.iter()
|
||||
.filter_map(|(uuid, list)| if ZFS_UUIDS.contains(uuid.as_str()) { Some(list) } else { None })
|
||||
{
|
||||
for device in device_list {
|
||||
let meta = std::fs::metadata(device)?;
|
||||
for info in lsblk_info.iter() {
|
||||
if let Some(partition_type) = &info.partition_type {
|
||||
if ZFS_UUIDS.contains(partition_type.as_str()) {
|
||||
let meta = std::fs::metadata(&info.path)?;
|
||||
device_set.insert(meta.rdev());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(device_set)
|
||||
}
|
||||
|
|
|
@ -100,9 +100,25 @@ pub struct LruCache<K, V> {
|
|||
_marker: PhantomData<Box<CacheNode<K, V>>>,
|
||||
}
|
||||
|
||||
impl<K, V> Drop for LruCache<K, V> {
|
||||
fn drop (&mut self) {
|
||||
self.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// trivial: if our contents are Send, the whole cache is Send
|
||||
unsafe impl<K: Send, V: Send> Send for LruCache<K, V> {}
|
||||
|
||||
impl<K, V> LruCache<K, V> {
|
||||
/// Clear all the entries from the cache.
|
||||
pub fn clear(&mut self) {
|
||||
// This frees only the HashMap with the node pointers.
|
||||
self.map.clear();
|
||||
// This frees the actual nodes and resets the list head and tail.
|
||||
self.list.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: std::cmp::Eq + std::hash::Hash + Copy, V> LruCache<K, V> {
|
||||
/// Create LRU cache instance which holds up to `capacity` nodes at once.
|
||||
pub fn new(capacity: usize) -> Self {
|
||||
|
@ -115,14 +131,6 @@ impl<K: std::cmp::Eq + std::hash::Hash + Copy, V> LruCache<K, V> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Clear all the entries from the cache.
|
||||
pub fn clear(&mut self) {
|
||||
// This frees only the HashMap with the node pointers.
|
||||
self.map.clear();
|
||||
// This frees the actual nodes and resets the list head and tail.
|
||||
self.list.clear();
|
||||
}
|
||||
|
||||
/// Insert or update an entry identified by `key` with the given `value`.
|
||||
/// This entry is placed as the most recently used node at the head.
|
||||
pub fn insert(&mut self, key: K, value: V) {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::sync::{Arc, Weak, Mutex};
|
||||
use std::task::{Context, Poll, RawWaker, Waker};
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
use std::thread::{self, Thread};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
@ -15,8 +15,7 @@ thread_local! {
|
|||
}
|
||||
|
||||
fn is_in_tokio() -> bool {
|
||||
tokio::runtime::Handle::try_current()
|
||||
.is_ok()
|
||||
tokio::runtime::Handle::try_current().is_ok()
|
||||
}
|
||||
|
||||
fn is_blocking() -> bool {
|
||||
|
@ -49,7 +48,8 @@ lazy_static! {
|
|||
static ref RUNTIME: Mutex<Weak<Runtime>> = Mutex::new(Weak::new());
|
||||
}
|
||||
|
||||
extern {
|
||||
#[link(name = "crypto")]
|
||||
extern "C" {
|
||||
fn OPENSSL_thread_stop();
|
||||
}
|
||||
|
||||
|
@ -58,16 +58,19 @@ extern {
|
|||
/// This makes sure that tokio's worker threads are marked for us so that we know whether we
|
||||
/// can/need to use `block_in_place` in our `block_on` helper.
|
||||
pub fn get_runtime_with_builder<F: Fn() -> runtime::Builder>(get_builder: F) -> Arc<Runtime> {
|
||||
|
||||
let mut guard = RUNTIME.lock().unwrap();
|
||||
|
||||
if let Some(rt) = guard.upgrade() { return rt; }
|
||||
if let Some(rt) = guard.upgrade() {
|
||||
return rt;
|
||||
}
|
||||
|
||||
let mut builder = get_builder();
|
||||
builder.on_thread_stop(|| {
|
||||
// avoid openssl bug: https://github.com/openssl/openssl/issues/6214
|
||||
// call OPENSSL_thread_stop to avoid race with openssl cleanup handlers
|
||||
unsafe { OPENSSL_thread_stop(); }
|
||||
unsafe {
|
||||
OPENSSL_thread_stop();
|
||||
}
|
||||
});
|
||||
|
||||
let runtime = builder.build().expect("failed to spawn tokio runtime");
|
||||
|
@ -82,7 +85,6 @@ pub fn get_runtime_with_builder<F: Fn() -> runtime::Builder>(get_builder: F) ->
|
|||
///
|
||||
/// This calls get_runtime_with_builder() using the tokio default threaded scheduler
|
||||
pub fn get_runtime() -> Arc<Runtime> {
|
||||
|
||||
get_runtime_with_builder(|| {
|
||||
let mut builder = runtime::Builder::new_multi_thread();
|
||||
builder.enable_all();
|
||||
|
@ -90,7 +92,6 @@ pub fn get_runtime() -> Arc<Runtime> {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
/// Block on a synchronous piece of code.
|
||||
pub fn block_in_place<R>(fut: impl FnOnce() -> R) -> R {
|
||||
// don't double-exit the context (tokio doesn't like that)
|
||||
|
@ -155,12 +156,22 @@ pub fn main<F: Future>(fut: F) -> F::Output {
|
|||
block_on(fut)
|
||||
}
|
||||
|
||||
struct ThreadWaker(Thread);
|
||||
|
||||
impl std::task::Wake for ThreadWaker {
|
||||
fn wake(self: Arc<Self>) {
|
||||
self.0.unpark();
|
||||
}
|
||||
|
||||
fn wake_by_ref(self: &Arc<Self>) {
|
||||
self.0.unpark();
|
||||
}
|
||||
}
|
||||
|
||||
fn block_on_local_future<F: Future>(fut: F) -> F::Output {
|
||||
pin_mut!(fut);
|
||||
|
||||
let waker = Arc::new(thread::current());
|
||||
let waker = thread_waker_clone(Arc::into_raw(waker) as *const ());
|
||||
let waker = unsafe { Waker::from_raw(waker) };
|
||||
let waker = Waker::from(Arc::new(ThreadWaker(thread::current())));
|
||||
let mut context = Context::from_waker(&waker);
|
||||
loop {
|
||||
match fut.as_mut().poll(&mut context) {
|
||||
|
@ -169,34 +180,3 @@ fn block_on_local_future<F: Future>(fut: F) -> F::Output {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const THREAD_WAKER_VTABLE: std::task::RawWakerVTable = std::task::RawWakerVTable::new(
|
||||
thread_waker_clone,
|
||||
thread_waker_wake,
|
||||
thread_waker_wake_by_ref,
|
||||
thread_waker_drop,
|
||||
);
|
||||
|
||||
fn thread_waker_clone(this: *const ()) -> RawWaker {
|
||||
let this = unsafe { Arc::from_raw(this as *const Thread) };
|
||||
let cloned = Arc::clone(&this);
|
||||
let _ = Arc::into_raw(this);
|
||||
|
||||
RawWaker::new(Arc::into_raw(cloned) as *const (), &THREAD_WAKER_VTABLE)
|
||||
}
|
||||
|
||||
fn thread_waker_wake(this: *const ()) {
|
||||
let this = unsafe { Arc::from_raw(this as *const Thread) };
|
||||
this.unpark();
|
||||
}
|
||||
|
||||
fn thread_waker_wake_by_ref(this: *const ()) {
|
||||
let this = unsafe { Arc::from_raw(this as *const Thread) };
|
||||
this.unpark();
|
||||
let _ = Arc::into_raw(this);
|
||||
}
|
||||
|
||||
fn thread_waker_drop(this: *const ()) {
|
||||
let this = unsafe { Arc::from_raw(this as *const Thread) };
|
||||
drop(this);
|
||||
}
|
||||
|
|
|
@ -258,15 +258,27 @@ pub fn read_subscription() -> Result<Option<SubscriptionInfo>, Error> {
|
|||
let new_checksum = base64::encode(tools::md5sum(new_checksum.as_bytes())?);
|
||||
|
||||
if checksum != new_checksum {
|
||||
bail!("stored checksum doesn't matches computed one '{}' != '{}'", checksum, new_checksum);
|
||||
return Ok(Some( SubscriptionInfo {
|
||||
status: SubscriptionStatus::INVALID,
|
||||
message: Some("checksum mismatch".to_string()),
|
||||
..info
|
||||
}));
|
||||
}
|
||||
|
||||
let age = proxmox::tools::time::epoch_i64() - info.checktime.unwrap_or(0);
|
||||
if age < -5400 { // allow some delta for DST changes or time syncs, 1.5h
|
||||
bail!("Last check time to far in the future.");
|
||||
return Ok(Some( SubscriptionInfo {
|
||||
status: SubscriptionStatus::INVALID,
|
||||
message: Some("last check date too far in the future".to_string()),
|
||||
..info
|
||||
}));
|
||||
} else if age > MAX_LOCAL_KEY_AGE + MAX_KEY_CHECK_FAILURE_AGE {
|
||||
if let SubscriptionStatus::ACTIVE = info.status {
|
||||
bail!("subscription information too old");
|
||||
return Ok(Some( SubscriptionInfo {
|
||||
status: SubscriptionStatus::INVALID,
|
||||
message: Some("subscription information too old".to_string()),
|
||||
..info
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -218,6 +218,16 @@ Ext.define('PBS.MainView', {
|
|||
flex: 1,
|
||||
baseCls: 'x-plain',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxEOLNotice',
|
||||
product: 'Proxmox Backup Server',
|
||||
version: '1.1',
|
||||
eolDate: '2022-07-31',
|
||||
href: 'pbs.proxmox.com/docs/faq.html#how-long-will-my-proxmox-backup-server-version-be-supported',
|
||||
},
|
||||
{
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
xtype: 'button',
|
||||
baseCls: 'x-btn',
|
||||
|
|
|
@ -70,7 +70,7 @@ Ext.define('PBS.DatastoreStatistics', {
|
|||
if (err) {
|
||||
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(err)}"`;
|
||||
metaData.tdCls = 'proxmox-invalid-row';
|
||||
return `${value || ''} <i class="fa fa-fw critical fa-exclamation-circle"><i>`;
|
||||
return `${value || ''} <i class="fa fa-fw critical fa-exclamation-circle"></i>`;
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
|
|
@ -33,14 +33,12 @@ Ext.define('PBS.Datastore.Options', {
|
|||
note: gettext('Configuration change only, no data will be deleted.'),
|
||||
autoShow: true,
|
||||
taskName: 'delete-datastore',
|
||||
listeners: {
|
||||
destroy: () => {
|
||||
apiCallDone: (success) => {
|
||||
let navtree = Ext.ComponentQuery.query('navigationtree')[0];
|
||||
navtree.rstore.load();
|
||||
let mainview = me.getView().up('mainview');
|
||||
mainview.getController().redirectTo('pbsDataStores');
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue