Compare commits

..

38 Commits

Author SHA1 Message Date
d80d1f9a2b bump version to 0.2.1-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-28 17:39:41 +02:00
6161ac18a4 ui: remotes: fix remote remove buttons base url
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-28 17:29:54 +02:00
6bba120d14 ui: fix RemoteEdit password change
we have to remove the password from the submitvalues if it did not
change

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-28 17:24:06 +02:00
91e5bb49f5 src/bin/proxmox-backup-proxy.rs: simplify code
and gather all stats for the root disk
2020-05-28 12:30:54 +02:00
547e3c2f6c src/tools/disks/zfs.rs: use wtime + rtime (wait + run time) 2020-05-28 11:45:34 +02:00
4bf26be3bb www/DataStoreStatistic.js: add transfer rate 2020-05-28 10:20:29 +02:00
25c550bc28 src/bin/proxmox-backup-proxy.rs: gather zpool io stats 2020-05-28 10:09:13 +02:00
0146133b4b src/tools/disks/zfs.rs: helper to read zfs pool io stats 2020-05-28 10:07:52 +02:00
3eeba68785 depend on proxmox 0.1.38, use new fs helper functions 2020-05-28 10:06:44 +02:00
f5056656b2 use the sync id for the scheduled sync worker task
this way, multiple sync jobs with the same local store, can get scheduled

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-28 06:26:03 +02:00
8c87743642 fix 'remove_vanished' cli arg again
since the target side wants this to be a boolean and
serde interprets a None Value as 'null' we have to only
add this when it is really set via cli

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-28 06:25:30 +02:00
05d755b282 fix inserting of worker tasks
when starting a new task, we do two things to keep track of tasks
(in that order):
* updating the 'active' file with a list of tasks with
  'update_active_workers'
* updating the WORKER_TASK_LIST

the second also updates the status of running tasks in the file by
checking if it is still running by checking the WORKER_TASK_LIST

since those two things are not locked, it can happend that
we update the file, and before updating the WORKER_TASK_LIST,
another thread calls update_active_workers and tries to
get the status from the task log, which won't have any data yet
so the status is 'unknown'

(we do not update that status ever, likely for performance reasons,
so we have to fix this here)

by switching the order of the two operations, we make sure that only
tasks reach the 'active' file which are inserted in the WORKER_TASK_LIST

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-28 06:24:42 +02:00
143b654550 src/tools.rs - command_output: add parameter to check exit code 2020-05-27 07:25:39 +02:00
97fab7aa11 src/tools.rs: new helper to handle command_output (std::process::Output) 2020-05-27 06:53:25 +02:00
ed216fd773 ui: acl view: only update if component is activated
Avoid triggering non-required background updates during browsing a
datastores content or statistics panels. They're not expensive, but I
do not like such behavior at all (having traveled with trains and
spotty network to often)

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-26 18:58:21 +02:00
0f13623443 ui: tasks: add sync description+
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-26 18:36:58 +02:00
dbd959d43f ui: tasks: render reader with full info
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-26 18:36:58 +02:00
f68ae22cc0 ui: factor out render_datetime_utc
will be reused in the next patch

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-26 18:36:48 +02:00
06c3dc8a8e ui: task: improve rendering of backup/prune worker entries
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-26 13:37:57 +02:00
a6fbbd03c8 depend on proxmox 0.1.37 2020-05-26 13:00:34 +02:00
26956d73a2 ui: datastore prune: remove debug logging
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-26 12:50:06 +02:00
3f98b34705 ui: rework datastore content panel controller
Mostly refactoring, but actually fixes an issue where one seldom run
into a undefined dereference due to the store onLoad callback getting
triggered after some of the componet was destroyed - on quick
switching through the datastores.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-26 12:46:48 +02:00
40dc103103 fix cli pull api call
there is no 'delete' parameter, only 'remove-vanished', so fix that

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 12:39:19 +02:00
12710fd3c3 ui: add missing monStoreErrors
to actually show api errors on the list call

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 12:38:57 +02:00
9e2a4653b4 ui: add crud for remotes
listing/adding/editing/removing

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 12:38:39 +02:00
de4db62c57 remotes: save passwords as base64
to avoid having arbitrary characters in the config (e.g. newlines)
note that this breaks existings configs

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 12:38:06 +02:00
1a0d3d11d2 src/api2/admin/datastore.rs: add rrd api 2020-05-26 12:26:14 +02:00
8c03041a2c src/bin/proxmox-backup-proxy.rs: gather block device stats on datastore 2020-05-26 11:20:59 +02:00
3fcc4b4e5c src/tools/disks.rs: add helper to read block device stats 2020-05-26 11:20:22 +02:00
3ed07ed2cd src/tools/disks.rs: export read_sys 2020-05-26 09:49:13 +02:00
75410d65ef d/control: proxmox-backup-server: depend on proxmox-backup-docs
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-26 09:37:03 +02:00
83fd4b3b1b remote: try to use Struct for api
with a catch: password is in the struct but we do not want it to return
via the api, so we only 'serialize' it when the string is not empty
(this can only happen when the format is not checked by us, iow.
when its returned from the api) and setting it manually to ""
when we return remotes from the api

this way we can still use the type but do not return the password

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 08:55:07 +02:00
bfa0146c00 ui: acls: include roleid into id and sort by it
this fixes missing acls on the gui

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 08:49:59 +02:00
5dcdcea293 api2/config/remote: remove password from read_remote
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 08:49:12 +02:00
99f443c6ae api2/config/remote: lock and use digest for removal
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 08:48:45 +02:00
4f966d0592 api2/config/remote: use rpcenv for digest for read_remote
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 08:48:28 +02:00
db0c228719 config/remote: add 'name' to Remote struct
and use it as section id, like with User

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 08:48:05 +02:00
880fa939d1 gui: move system stat RRDs to ServerStatus panel. 2020-05-26 07:33:00 +02:00
31 changed files with 973 additions and 351 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "proxmox-backup" name = "proxmox-backup"
version = "0.2.0" version = "0.2.1"
authors = ["Dietmar Maurer <dietmar@proxmox.com>"] authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
edition = "2018" edition = "2018"
license = "AGPL-3" license = "AGPL-3"
@ -36,7 +36,7 @@ pam = "0.7"
pam-sys = "0.5" pam-sys = "0.5"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-utils = "0.1.0" pin-utils = "0.1.0"
proxmox = { version = "0.1.36", features = [ "sortable-macro", "api-macro" ] } proxmox = { version = "0.1.38", features = [ "sortable-macro", "api-macro" ] }
#proxmox = { git = "ssh://gitolite3@proxdev.maurer-it.com/rust/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] } #proxmox = { git = "ssh://gitolite3@proxdev.maurer-it.com/rust/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] }
#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro" ] } #proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro" ] }
regex = "1.2" regex = "1.2"

18
debian/changelog vendored
View File

@ -1,3 +1,21 @@
rust-proxmox-backup (0.2.1-1) unstable; urgency=medium
* ui: move server RRD statistics to 'Server Status' panel
* ui/api: add more server statistics
* ui/api: add per-datastore usage and performance statistics over time
* ui: add initial remote config management panel
* remotes: save passwords as base64
* gather zpool io stats
* various fixes/improvements
-- Proxmox Support Team <support@proxmox.com> Thu, 28 May 2020 17:39:33 +0200
rust-proxmox-backup (0.2.0-1) unstable; urgency=medium rust-proxmox-backup (0.2.0-1) unstable; urgency=medium
* see git changelog (too many changes) * see git changelog (too many changes)

1
debian/control.in vendored
View File

@ -3,6 +3,7 @@ Architecture: any
Depends: fonts-font-awesome, Depends: fonts-font-awesome,
libjs-extjs (>= 6.0.1), libjs-extjs (>= 6.0.1),
libzstd1 (>= 1.3.8), libzstd1 (>= 1.3.8),
proxmox-backup-docs,
proxmox-mini-journalreader, proxmox-mini-journalreader,
proxmox-widget-toolkit (>= 2.2-4), proxmox-widget-toolkit (>= 2.2-4),
${misc:Depends}, ${misc:Depends},

View File

@ -843,6 +843,46 @@ fn upload_backup_log(
}.boxed() }.boxed()
} }
#[api(
input: {
properties: {
store: {
schema: DATASTORE_SCHEMA,
},
timeframe: {
type: RRDTimeFrameResolution,
},
cf: {
type: RRDMode,
},
},
},
access: {
permission: &Permission::Privilege(&["datastore", "{store}"], PRIV_DATASTORE_AUDIT | PRIV_DATASTORE_BACKUP, true),
},
)]
/// Read datastore stats
fn get_rrd_stats(
store: String,
timeframe: RRDTimeFrameResolution,
cf: RRDMode,
_param: Value,
) -> Result<Value, Error> {
let rrd_dir = format!("datastore/{}", store);
crate::rrd::extract_data(
&rrd_dir,
&[
"total", "used",
"read_ios", "read_bytes", "read_ticks",
"write_ios", "write_bytes", "write_ticks",
],
timeframe,
cf,
)
}
#[sortable] #[sortable]
const DATASTORE_INFO_SUBDIRS: SubdirMap = &[ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
( (
@ -871,6 +911,11 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
&Router::new() &Router::new()
.post(&API_METHOD_PRUNE) .post(&API_METHOD_PRUNE)
), ),
(
"rrd",
&Router::new()
.get(&API_METHOD_GET_RRD_STATS)
),
( (
"snapshots", "snapshots",
&Router::new() &Router::new()

View File

@ -1,6 +1,7 @@
use anyhow::{bail, Error}; use anyhow::{bail, Error};
use serde_json::Value; use serde_json::Value;
use ::serde::{Deserialize, Serialize}; use ::serde::{Deserialize, Serialize};
use base64;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission}; use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
@ -16,27 +17,8 @@ use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY};
description: "The list of configured remotes (with config digest).", description: "The list of configured remotes (with config digest).",
type: Array, type: Array,
items: { items: {
type: Object, type: remote::Remote,
description: "Remote configuration (without password).", description: "Remote configuration (without password).",
properties: {
name: {
schema: REMOTE_ID_SCHEMA,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
host: {
schema: DNS_NAME_OR_IP_SCHEMA,
},
userid: {
schema: PROXMOX_USER_ID_SCHEMA,
},
fingerprint: {
optional: true,
schema: CERT_FINGERPRINT_SHA256_SCHEMA,
},
},
}, },
}, },
access: { access: {
@ -47,14 +29,20 @@ use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY};
pub fn list_remotes( pub fn list_remotes(
_param: Value, _param: Value,
_info: &ApiMethod, _info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment, mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> { ) -> Result<Vec<remote::Remote>, Error> {
let (config, digest) = remote::config()?; let (config, digest) = remote::config()?;
let value = config.convert_to_array("name", Some(&digest), &["password"]); let mut list: Vec<remote::Remote> = config.convert_to_typed_array("remote")?;
Ok(value.into()) // don't return password in api
for remote in &mut list {
remote.password = "".to_string();
}
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list)
} }
#[api( #[api(
@ -88,19 +76,21 @@ pub fn list_remotes(
}, },
)] )]
/// Create new remote. /// Create new remote.
pub fn create_remote(name: String, param: Value) -> Result<(), Error> { pub fn create_remote(password: String, param: Value) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?; let _lock = crate::tools::open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let remote: remote::Remote = serde_json::from_value(param.clone())?; let mut data = param.clone();
data["password"] = Value::from(base64::encode(password.as_bytes()));
let remote: remote::Remote = serde_json::from_value(data)?;
let (mut config, _digest) = remote::config()?; let (mut config, _digest) = remote::config()?;
if let Some(_) = config.sections.get(&name) { if let Some(_) = config.sections.get(&remote.name) {
bail!("remote '{}' already exists.", name); bail!("remote '{}' already exists.", remote.name);
} }
config.set_data(&name, "remote", &remote)?; config.set_data(&remote.name, "remote", &remote)?;
remote::save_config(&config)?; remote::save_config(&config)?;
@ -124,11 +114,15 @@ pub fn create_remote(name: String, param: Value) -> Result<(), Error> {
} }
)] )]
/// Read remote configuration data. /// Read remote configuration data.
pub fn read_remote(name: String) -> Result<Value, Error> { pub fn read_remote(
name: String,
_info: &ApiMethod,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<remote::Remote, Error> {
let (config, digest) = remote::config()?; let (config, digest) = remote::config()?;
let mut data = config.lookup_json("remote", &name)?; let mut data: remote::Remote = config.lookup("remote", &name)?;
data.as_object_mut().unwrap() data.password = "".to_string(); // do not return password in api
.insert("digest".into(), proxmox::tools::digest_to_hex(&digest).into()); rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(data) Ok(data)
} }
@ -248,6 +242,10 @@ pub fn update_remote(
name: { name: {
schema: REMOTE_ID_SCHEMA, schema: REMOTE_ID_SCHEMA,
}, },
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
}, },
}, },
access: { access: {
@ -255,12 +253,16 @@ pub fn update_remote(
}, },
)] )]
/// Remove a remote from the configuration file. /// Remove a remote from the configuration file.
pub fn delete_remote(name: String) -> Result<(), Error> { pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error> {
// fixme: locking ? let _lock = crate::tools::open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
// fixme: check digest ?
let (mut config, _digest) = remote::config()?; let (mut config, expected_digest) = remote::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
match config.sections.get(&name) { match config.sections.get(&name) {
Some(_) => { config.sections.remove(&name); }, Some(_) => { config.sections.remove(&name); },

View File

@ -34,9 +34,11 @@ fn get_node_stats(
"memtotal", "memused", "memtotal", "memused",
"swaptotal", "swapused", "swaptotal", "swapused",
"netin", "netout", "netin", "netout",
"roottotal", "rootused",
"loadavg", "loadavg",
], "total", "used",
"read_ios", "read_bytes", "read_ticks",
"write_ios", "write_bytes", "write_ticks",
],
timeframe, timeframe,
cf, cf,
) )

View File

@ -260,11 +260,9 @@ fn task_mgmt_cli() -> CommandLineInterface {
"remote-store": { "remote-store": {
schema: DATASTORE_SCHEMA, schema: DATASTORE_SCHEMA,
}, },
delete: { "remove-vanished": {
description: "Delete vanished backups. This remove the local copy if the remote backup was deleted.", schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
type: Boolean,
optional: true, optional: true,
default: true,
}, },
"output-format": { "output-format": {
schema: OUTPUT_FORMAT, schema: OUTPUT_FORMAT,
@ -278,7 +276,7 @@ async fn pull_datastore(
remote: String, remote: String,
remote_store: String, remote_store: String,
local_store: String, local_store: String,
delete: Option<bool>, remove_vanished: Option<bool>,
param: Value, param: Value,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
@ -292,8 +290,8 @@ async fn pull_datastore(
"remote-store": remote_store, "remote-store": remote_store,
}); });
if let Some(delete) = delete { if let Some(remove_vanished) = remove_vanished {
args["delete"] = delete.into(); args["remove-vanished"] = Value::from(remove_vanished);
} }
let result = client.post("api2/json/pull", Some(args)).await?; let result = client.post("api2/json/pull", Some(args)).await?;

View File

@ -1,4 +1,6 @@
use std::sync::Arc; use std::sync::Arc;
use std::ffi::OsString;
use std::path::Path;
use anyhow::{bail, format_err, Error}; use anyhow::{bail, format_err, Error};
use futures::*; use futures::*;
@ -7,6 +9,7 @@ use openssl::ssl::{SslMethod, SslAcceptor, SslFiletype};
use proxmox::try_block; use proxmox::try_block;
use proxmox::api::RpcEnvironmentType; use proxmox::api::RpcEnvironmentType;
use proxmox::sys::linux::procfs::mountinfo::{Device, MountInfo};
use proxmox_backup::configdir; use proxmox_backup::configdir;
use proxmox_backup::buildcfg; use proxmox_backup::buildcfg;
@ -14,6 +17,7 @@ use proxmox_backup::server;
use proxmox_backup::tools::daemon; use proxmox_backup::tools::daemon;
use proxmox_backup::server::{ApiConfig, rest::*}; use proxmox_backup::server::{ApiConfig, rest::*};
use proxmox_backup::auth_helpers::*; use proxmox_backup::auth_helpers::*;
use proxmox_backup::tools::disks::{ DiskManage, zfs::zfs_pool_stats };
fn main() { fn main() {
if let Err(err) = proxmox_backup::tools::runtime::main(run()) { if let Err(err) = proxmox_backup::tools::runtime::main(run()) {
@ -558,7 +562,7 @@ async fn schedule_datastore_sync_jobs() {
if let Err(err) = WorkerTask::spawn( if let Err(err) = WorkerTask::spawn(
worker_type, worker_type,
Some(job_config.store.clone()), Some(job_id.clone()),
&username.clone(), &username.clone(),
false, false,
move |worker| async move { move |worker| async move {
@ -619,6 +623,7 @@ async fn generate_host_stats() {
read_meminfo, read_proc_stat, read_proc_net_dev, read_loadavg}; read_meminfo, read_proc_stat, read_proc_net_dev, read_loadavg};
use proxmox_backup::config::datastore; use proxmox_backup::config::datastore;
proxmox_backup::tools::runtime::block_in_place(move || { proxmox_backup::tools::runtime::block_in_place(move || {
match read_proc_stat() { match read_proc_stat() {
@ -670,15 +675,9 @@ async fn generate_host_stats() {
} }
} }
match disk_usage(std::path::Path::new("/")) { let disk_manager = DiskManage::new();
Ok((total, used, _avail)) => {
rrd_update_gauge("host/roottotal", total as f64); gather_disk_stats(disk_manager.clone(), Path::new("/"), "host");
rrd_update_gauge("host/rootused", used as f64);
}
Err(err) => {
eprintln!("read root disk_usage failed - {}", err);
}
}
match datastore::config() { match datastore::config() {
Ok((config, _)) => { Ok((config, _)) => {
@ -686,16 +685,10 @@ async fn generate_host_stats() {
config.convert_to_typed_array("datastore").unwrap_or(Vec::new()); config.convert_to_typed_array("datastore").unwrap_or(Vec::new());
for config in datastore_list { for config in datastore_list {
match disk_usage(std::path::Path::new(&config.path)) {
Ok((total, used, _avail)) => { let rrd_prefix = format!("datastore/{}", config.name);
let rrd_key = format!("datastore/{}", config.name); let path = std::path::Path::new(&config.path);
rrd_update_gauge(&rrd_key, total as f64); gather_disk_stats(disk_manager.clone(), path, &rrd_prefix);
rrd_update_gauge(&rrd_key, used as f64);
}
Err(err) => {
eprintln!("read disk_usage on {:?} failed - {}", config.path, err);
}
}
} }
} }
Err(err) => { Err(err) => {
@ -706,6 +699,66 @@ async fn generate_host_stats() {
}); });
} }
fn gather_disk_stats(disk_manager: Arc<DiskManage>, path: &Path, rrd_prefix: &str) {
match disk_usage(path) {
Ok((total, used, _avail)) => {
let rrd_key = format!("{}/total", rrd_prefix);
rrd_update_gauge(&rrd_key, total as f64);
let rrd_key = format!("{}/used", rrd_prefix);
rrd_update_gauge(&rrd_key, used as f64);
}
Err(err) => {
eprintln!("read disk_usage on {:?} failed - {}", path, err);
}
}
match disk_manager.mount_info() {
Ok(mountinfo) => {
if let Some((fs_type, device, source)) = find_mounted_device(mountinfo, path) {
let mut device_stat = None;
match fs_type.as_str() {
"zfs" => {
if let Some(pool) = source {
match zfs_pool_stats(&pool) {
Ok(stat) => device_stat = stat,
Err(err) => eprintln!("zfs_pool_stats({:?}) failed - {}", pool, err),
}
}
}
_ => {
if let Ok(disk) = disk_manager.clone().disk_by_dev_num(device.into_dev_t()) {
match disk.read_stat() {
Ok(stat) => device_stat = stat,
Err(err) => eprintln!("disk.read_stat {:?} failed - {}", path, err),
}
}
}
}
if let Some(stat) = device_stat {
let rrd_key = format!("{}/read_ios", rrd_prefix);
rrd_update_derive(&rrd_key, stat.read_ios as f64);
let rrd_key = format!("{}/read_bytes", rrd_prefix);
rrd_update_derive(&rrd_key, (stat.read_sectors*512) as f64);
let rrd_key = format!("{}/read_ticks", rrd_prefix);
rrd_update_derive(&rrd_key, (stat.read_ticks as f64)/1000.0);
let rrd_key = format!("{}/write_ios", rrd_prefix);
rrd_update_derive(&rrd_key, stat.write_ios as f64);
let rrd_key = format!("{}/write_bytes", rrd_prefix);
rrd_update_derive(&rrd_key, (stat.write_sectors*512) as f64);
let rrd_key = format!("{}/write_ticks", rrd_prefix);
rrd_update_derive(&rrd_key, (stat.write_ticks as f64)/1000.0);
}
}
}
Err(err) => {
eprintln!("disk_manager mount_info() failed - {}", err);
}
}
}
// Returns (total, used, avail) // Returns (total, used, avail)
fn disk_usage(path: &std::path::Path) -> Result<(u64, u64, u64), Error> { fn disk_usage(path: &std::path::Path) -> Result<(u64, u64, u64), Error> {
@ -720,3 +773,26 @@ fn disk_usage(path: &std::path::Path) -> Result<(u64, u64, u64), Error> {
Ok((stat.f_blocks*bsize, (stat.f_blocks-stat.f_bfree)*bsize, stat.f_bavail*bsize)) Ok((stat.f_blocks*bsize, (stat.f_blocks-stat.f_bfree)*bsize, stat.f_bavail*bsize))
} }
// Returns (fs_type, device, mount_source)
pub fn find_mounted_device(
mountinfo: &MountInfo,
path: &std::path::Path,
) -> Option<(String, Device, Option<OsString>)> {
let mut result = None;
let mut match_len = 0;
let root_path = std::path::Path::new("/");
for (_id, entry) in mountinfo {
if entry.root == root_path && path.starts_with(&entry.mount_point) {
let len = entry.mount_point.as_path().as_os_str().len();
if len > match_len {
match_len = len;
result = Some((entry.fs_type.clone(), entry.device, entry.mount_source.clone()));
}
}
}
result
}

View File

@ -1,4 +1,4 @@
use anyhow::{bail, Error}; use anyhow::{Error};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -113,16 +113,9 @@ pub const DATASTORE_CFG_FILENAME: &str = "/etc/proxmox-backup/datastore.cfg";
pub const DATASTORE_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.datastore.lck"; pub const DATASTORE_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.datastore.lck";
pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
let content = match std::fs::read_to_string(DATASTORE_CFG_FILENAME) {
Ok(c) => c, let content = proxmox::tools::fs::file_read_optional_string(DATASTORE_CFG_FILENAME)?;
Err(err) => { let content = content.unwrap_or(String::from(""));
if err.kind() == std::io::ErrorKind::NotFound {
String::from("")
} else {
bail!("unable to read '{}' - {}", DATASTORE_CFG_FILENAME, err);
}
}
};
let digest = openssl::sha::sha256(content.as_bytes()); let digest = openssl::sha::sha256(content.as_bytes());
let data = CONFIG.parse(DATASTORE_CFG_FILENAME, &content)?; let data = CONFIG.parse(DATASTORE_CFG_FILENAME, &content)?;

View File

@ -477,24 +477,15 @@ pub const NETWORK_INTERFACES_FILENAME: &str = "/etc/network/interfaces";
pub const NETWORK_INTERFACES_NEW_FILENAME: &str = "/etc/network/interfaces.new"; pub const NETWORK_INTERFACES_NEW_FILENAME: &str = "/etc/network/interfaces.new";
pub const NETWORK_LOCKFILE: &str = "/var/lock/pve-network.lck"; pub const NETWORK_LOCKFILE: &str = "/var/lock/pve-network.lck";
pub fn config() -> Result<(NetworkConfig, [u8;32]), Error> { pub fn config() -> Result<(NetworkConfig, [u8;32]), Error> {
let content = std::fs::read(NETWORK_INTERFACES_NEW_FILENAME)
.or_else(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
std::fs::read(NETWORK_INTERFACES_FILENAME)
.or_else(|err| {
if err.kind() == std::io::ErrorKind::NotFound {
Ok(Vec::new())
} else {
bail!("unable to read '{}' - {}", NETWORK_INTERFACES_FILENAME, err);
}
})
} else {
bail!("unable to read '{}' - {}", NETWORK_INTERFACES_NEW_FILENAME, err);
}
})?;
let content = match proxmox::tools::fs::file_get_optional_contents(NETWORK_INTERFACES_NEW_FILENAME)? {
Some(content) => content,
None => {
let content = proxmox::tools::fs::file_get_optional_contents(NETWORK_INTERFACES_FILENAME)?;
content.unwrap_or(Vec::new())
}
};
let digest = openssl::sha::sha256(&content); let digest = openssl::sha::sha256(&content);

View File

@ -149,23 +149,8 @@ pub fn compute_file_diff(filename: &str, shadow: &str) -> Result<String, Error>
.output() .output()
.map_err(|err| format_err!("failed to execute diff - {}", err))?; .map_err(|err| format_err!("failed to execute diff - {}", err))?;
if !output.status.success() { let diff = crate::tools::command_output(output, Some(|c| c == 0 || c == 1))
match output.status.code() { .map_err(|err| format_err!("diff failed: {}", err))?;
Some(code) => {
if code == 0 { return Ok(String::new()); }
if code != 1 {
let msg = String::from_utf8(output.stderr)
.map(|m| if m.is_empty() { String::from("no error message") } else { m })
.unwrap_or_else(|_| String::from("non utf8 error message (suppressed)"));
bail!("diff failed with status code: {} - {}", code, msg);
}
}
None => bail!("diff terminated by signal"),
}
}
let diff = String::from_utf8(output.stdout)?;
Ok(diff) Ok(diff)
} }
@ -180,17 +165,14 @@ pub fn assert_ifupdown2_installed() -> Result<(), Error> {
pub fn network_reload() -> Result<(), Error> { pub fn network_reload() -> Result<(), Error> {
let status = Command::new("/sbin/ifreload") let output = Command::new("/sbin/ifreload")
.arg("-a") .arg("-a")
.status() .output()
.map_err(|err| format_err!("failed to execute ifreload: - {}", err))?; .map_err(|err| format_err!("failed to execute '/sbin/ifreload' - {}", err))?;
crate::tools::command_output(output, None)
.map_err(|err| format_err!("ifreload failed: {}", err))?;
if !status.success() {
match status.code() {
Some(code) => bail!("ifreload failed with status code: {}", code),
None => bail!("ifreload terminated by signal")
}
}
Ok(()) Ok(())
} }

View File

@ -1,4 +1,4 @@
use anyhow::{bail, Error}; use anyhow::{Error};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -29,6 +29,9 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
#[api( #[api(
properties: { properties: {
name: {
schema: REMOTE_ID_SCHEMA,
},
comment: { comment: {
optional: true, optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA, schema: SINGLE_LINE_COMMENT_SCHEMA,
@ -51,10 +54,13 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
#[derive(Serialize,Deserialize)] #[derive(Serialize,Deserialize)]
/// Remote properties. /// Remote properties.
pub struct Remote { pub struct Remote {
pub name: String,
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub comment: Option<String>, pub comment: Option<String>,
pub host: String, pub host: String,
pub userid: String, pub userid: String,
#[serde(skip_serializing_if="String::is_empty")]
#[serde(with = "proxmox::tools::serde::string_as_base64")]
pub password: String, pub password: String,
#[serde(skip_serializing_if="Option::is_none")] #[serde(skip_serializing_if="Option::is_none")]
pub fingerprint: Option<String>, pub fingerprint: Option<String>,
@ -66,7 +72,7 @@ fn init() -> SectionConfig {
_ => unreachable!(), _ => unreachable!(),
}; };
let plugin = SectionConfigPlugin::new("remote".to_string(), None, obj_schema); let plugin = SectionConfigPlugin::new("remote".to_string(), Some("name".to_string()), obj_schema);
let mut config = SectionConfig::new(&REMOTE_ID_SCHEMA); let mut config = SectionConfig::new(&REMOTE_ID_SCHEMA);
config.register_plugin(plugin); config.register_plugin(plugin);
@ -77,16 +83,9 @@ pub const REMOTE_CFG_FILENAME: &str = "/etc/proxmox-backup/remote.cfg";
pub const REMOTE_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.remote.lck"; pub const REMOTE_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.remote.lck";
pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
let content = match std::fs::read_to_string(REMOTE_CFG_FILENAME) {
Ok(c) => c, let content = proxmox::tools::fs::file_read_optional_string(REMOTE_CFG_FILENAME)?;
Err(err) => { let content = content.unwrap_or(String::from(""));
if err.kind() == std::io::ErrorKind::NotFound {
String::from("")
} else {
bail!("unable to read '{}' - {}", REMOTE_CFG_FILENAME, err);
}
}
};
let digest = openssl::sha::sha256(content.as_bytes()); let digest = openssl::sha::sha256(content.as_bytes());
let data = CONFIG.parse(REMOTE_CFG_FILENAME, &content)?; let data = CONFIG.parse(REMOTE_CFG_FILENAME, &content)?;

View File

@ -1,4 +1,4 @@
use anyhow::{bail, Error}; use anyhow::{Error};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::collections::HashMap; use std::collections::HashMap;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -83,16 +83,9 @@ pub const SYNC_CFG_FILENAME: &str = "/etc/proxmox-backup/sync.cfg";
pub const SYNC_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.sync.lck"; pub const SYNC_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.sync.lck";
pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
let content = match std::fs::read_to_string(SYNC_CFG_FILENAME) {
Ok(c) => c, let content = proxmox::tools::fs::file_read_optional_string(SYNC_CFG_FILENAME)?;
Err(err) => { let content = content.unwrap_or(String::from(""));
if err.kind() == std::io::ErrorKind::NotFound {
String::from("")
} else {
bail!("unable to read '{}' - {}", SYNC_CFG_FILENAME, err);
}
}
};
let digest = openssl::sha::sha256(content.as_bytes()); let digest = openssl::sha::sha256(content.as_bytes());
let data = CONFIG.parse(SYNC_CFG_FILENAME, &content)?; let data = CONFIG.parse(SYNC_CFG_FILENAME, &content)?;

View File

@ -120,16 +120,9 @@ pub const USER_CFG_FILENAME: &str = "/etc/proxmox-backup/user.cfg";
pub const USER_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.user.lck"; pub const USER_CFG_LOCKFILE: &str = "/etc/proxmox-backup/.user.lck";
pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> { pub fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
let content = match std::fs::read_to_string(USER_CFG_FILENAME) {
Ok(c) => c, let content = proxmox::tools::fs::file_read_optional_string(USER_CFG_FILENAME)?;
Err(err) => { let content = content.unwrap_or(String::from(""));
if err.kind() == std::io::ErrorKind::NotFound {
String::from("")
} else {
bail!("unable to read '{}' - {}", USER_CFG_FILENAME, err);
}
}
};
let digest = openssl::sha::sha256(content.as_bytes()); let digest = openssl::sha::sha256(content.as_bytes());
let mut data = CONFIG.parse(USER_CFG_FILENAME, &content)?; let mut data = CONFIG.parse(USER_CFG_FILENAME, &content)?;

View File

@ -418,10 +418,8 @@ impl WorkerTask {
let logger = FileLogger::new(&path, to_stdout)?; let logger = FileLogger::new(&path, to_stdout)?;
nix::unistd::chown(&path, Some(backup_user.uid), Some(backup_user.gid))?; nix::unistd::chown(&path, Some(backup_user.uid), Some(backup_user.gid))?;
update_active_workers(Some(&upid))?;
let worker = Arc::new(Self { let worker = Arc::new(Self {
upid, upid: upid.clone(),
abort_requested: AtomicBool::new(false), abort_requested: AtomicBool::new(false),
data: Mutex::new(WorkerTaskData { data: Mutex::new(WorkerTaskData {
logger, logger,
@ -430,10 +428,14 @@ impl WorkerTask {
}), }),
}); });
let mut hash = WORKER_TASK_LIST.lock().unwrap(); // scope to drop the lock again after inserting
{
let mut hash = WORKER_TASK_LIST.lock().unwrap();
hash.insert(task_id, worker.clone());
super::set_worker_count(hash.len());
}
hash.insert(task_id, worker.clone()); update_active_workers(Some(&upid))?;
super::set_worker_count(hash.len());
Ok(worker) Ok(worker)
} }

View File

@ -474,6 +474,40 @@ pub fn normalize_uri_path(path: &str) -> Result<(String, Vec<&str>), Error> {
Ok((path, components)) Ok((path, components))
} }
/// Helper to check result from std::process::Command output
///
/// The exit_code_check() function should return true if the exit code
/// is considered successful.
pub fn command_output(
output: std::process::Output,
exit_code_check: Option<fn(i32) -> bool>
) -> Result<String, Error> {
if !output.status.success() {
match output.status.code() {
Some(code) => {
let is_ok = match exit_code_check {
Some(check_fn) => check_fn(code),
None => code == 0,
};
if !is_ok {
let msg = String::from_utf8(output.stderr)
.map(|m| if m.is_empty() { String::from("no error message") } else { m })
.unwrap_or_else(|_| String::from("non utf8 error message (suppressed)"));
bail!("status code: {} - {}", code, msg);
}
}
None => bail!("terminated by signal"),
}
}
let output = String::from_utf8(output.stdout)?;
Ok(output)
}
pub fn fd_change_cloexec(fd: RawFd, on: bool) -> Result<(), Error> { pub fn fd_change_cloexec(fd: RawFd, on: bool) -> Result<(), Error> {
use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD}; use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
let mut flags = FdFlag::from_bits(fcntl(fd, F_GETFD)?) let mut flags = FdFlag::from_bits(fcntl(fd, F_GETFD)?)

View File

@ -16,6 +16,8 @@ use proxmox::sys::error::io_err_other;
use proxmox::sys::linux::procfs::MountInfo; use proxmox::sys::linux::procfs::MountInfo;
use proxmox::{io_bail, io_format_err}; use proxmox::{io_bail, io_format_err};
pub mod zfs;
bitflags! { bitflags! {
/// Ways a device is being used. /// Ways a device is being used.
pub struct DiskUse: u32 { pub struct DiskUse: u32 {
@ -222,7 +224,7 @@ impl Disk {
/// Read from a file in this device's sys path. /// Read from a file in this device's sys path.
/// ///
/// Note: path must be a relative path! /// Note: path must be a relative path!
fn read_sys(&self, path: &Path) -> io::Result<Option<Vec<u8>>> { pub fn read_sys(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
assert!(path.is_relative()); assert!(path.is_relative());
std::fs::read(self.syspath().join(path)) std::fs::read(self.syspath().join(path))
@ -423,6 +425,30 @@ impl Disk {
.is_mounted .is_mounted
.get_or_try_init(|| self.manager.is_devnum_mounted(self.devnum()?))?) .get_or_try_init(|| self.manager.is_devnum_mounted(self.devnum()?))?)
} }
/// Read block device stats
pub fn read_stat(&self) -> std::io::Result<Option<BlockDevStat>> {
if let Some(stat) = self.read_sys(Path::new("stat"))? {
let stat = unsafe { std::str::from_utf8_unchecked(&stat) };
let stat: Vec<u64> = stat.split_ascii_whitespace().map(|s| {
u64::from_str_radix(s, 10).unwrap_or(0)
}).collect();
if stat.len() < 8 { return Ok(None); }
return Ok(Some(BlockDevStat {
read_ios: stat[0],
read_merges: stat[1],
read_sectors: stat[2],
read_ticks: stat[3],
write_ios: stat[4],
write_merges: stat[5],
write_sectors: stat[6],
write_ticks: stat[7],
}));
}
Ok(None)
}
} }
/// This is just a rough estimate for a "type" of disk. /// This is just a rough estimate for a "type" of disk.
@ -439,3 +465,16 @@ pub enum DiskType {
/// Some kind of USB disk, but we don't know more than that. /// Some kind of USB disk, but we don't know more than that.
Usb, Usb,
} }
#[derive(Debug)]
/// Represents the contents of the /sys/block/<dev>/stat file.
pub struct BlockDevStat {
pub read_ios: u64,
pub read_merges: u64,
pub read_sectors: u64,
pub read_ticks: u64, //milliseconds
pub write_ios: u64,
pub write_merges: u64,
pub write_sectors: u64,
pub write_ticks: u64, //milliseconds
}

47
src/tools/disks/zfs.rs Normal file
View File

@ -0,0 +1,47 @@
use anyhow::{bail, Error};
use std::path::PathBuf;
use super::*;
pub fn zfs_pool_stats(pool: &OsStr) -> Result<Option<BlockDevStat>, Error> {
let mut path = PathBuf::from("/proc/spl/kstat/zfs");
path.push(pool);
path.push("io");
let text = match proxmox::tools::fs::file_read_optional_string(&path)? {
Some(text) => text,
None => { return Ok(None); }
};
let lines: Vec<&str> = text.lines().collect();
if lines.len() < 3 {
bail!("unable to parse {:?} - got less than 3 lines", path);
}
// https://github.com/openzfs/zfs/blob/master/lib/libspl/include/sys/kstat.h#L578
// nread nwritten reads writes wtime wlentime wupdate rtime rlentime rupdate wcnt rcnt
// Note: w -> wait (wtime -> wait time)
// Note: r -> run (rtime -> run time)
// All times are nanoseconds
let stat: Vec<u64> = lines[2].split_ascii_whitespace().map(|s| {
u64::from_str_radix(s, 10).unwrap_or(0)
}).collect();
let ticks = (stat[4] + stat[7])/1_000_000; // convert to milisec
let stat = BlockDevStat {
read_sectors: stat[0]>>9,
write_sectors: stat[1]>>9,
read_ios: stat[2],
write_ios: stat[3],
read_merges: 0, // there is no such info
write_merges: 0, // there is no such info
write_ticks: ticks,
read_ticks: ticks,
};
Ok(Some(stat))
}

View File

@ -29,109 +29,112 @@ Ext.define('PBS.DataStoreContent', {
throw "no datastore specified"; throw "no datastore specified";
} }
this.data_store = Ext.create('Ext.data.Store', { this.store = Ext.create('Ext.data.Store', {
model: 'pbs-data-store-snapshots', model: 'pbs-data-store-snapshots',
sorters: 'backup-group', sorters: 'backup-group',
groupField: 'backup-group', groupField: 'backup-group',
}); });
this.store.on('load', this.onLoad, this);
Proxmox.Utils.monStoreErrors(view, view.store, true); Proxmox.Utils.monStoreErrors(view, view.store, true);
this.reload(); // initial load this.reload(); // initial load
}, },
reload: function() { reload: function() {
var view = this.getView(); let view = this.getView();
if (!view.store || !this.store) {
console.warn('cannot reload, no store(s)');
return;
}
let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`; let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`;
this.data_store.setProxy({ this.store.setProxy({
type: 'proxmox', type: 'proxmox',
url: url url: url
}); });
this.data_store.load(function(records, operation, success) { this.store.load();
let groups = {}; },
records.forEach(function(item) { getRecordGroups: function(records) {
var btype = item.data["backup-type"]; let groups = {};
let group = btype + "/" + item.data["backup-id"];
if (groups[group] !== undefined) for (const item of records) {
return; var btype = item.data["backup-type"];
let group = btype + "/" + item.data["backup-id"];
var cls = ''; if (groups[group] !== undefined) {
if (btype === 'vm') { continue;
cls = 'fa-desktop'; }
} else if (btype === 'ct') {
cls = 'fa-cube';
} else if (btype === 'host') {
cls = 'fa-building';
} else {
return btype + '/' + value;
}
groups[group] = { var cls = '';
text: group, if (btype === 'vm') {
leaf: false, cls = 'fa-desktop';
iconCls: "fa " + cls, } else if (btype === 'ct') {
expanded: false, cls = 'fa-cube';
backup_type: item.data["backup-type"], } else if (btype === 'host') {
backup_id: item.data["backup-id"], cls = 'fa-building';
children: [] } else {
}; console.warn(`got unkown backup-type '${btype}'`);
}); continue; // FIXME: auto render? what do?
}
let backup_time_to_string = function(backup_time) { groups[group] = {
let pad = function(number) { text: group,
if (number < 10) { leaf: false,
return '0' + number; iconCls: "fa " + cls,
} expanded: false,
return number; backup_type: item.data["backup-type"],
}; backup_id: item.data["backup-id"],
return backup_time.getUTCFullYear() + children: []
'-' + pad(backup_time.getUTCMonth() + 1) +
'-' + pad(backup_time.getUTCDate()) +
'T' + pad(backup_time.getUTCHours()) +
':' + pad(backup_time.getUTCMinutes()) +
':' + pad(backup_time.getUTCSeconds()) +
'Z';
}; };
}
records.forEach(function(item) { return groups;
let group = item.data["backup-type"] + "/" + item.data["backup-id"]; },
let children = groups[group].children;
let data = item.data; onLoad: function(store, records, success) {
let view = this.getView();
data.text = Ext.Date.format(data["backup-time"], 'Y-m-d H:i:s'); if (!success) {
data.text = group + '/' + backup_time_to_string(data["backup-time"]); return;
data.leaf = true; }
data.cls = 'no-leaf-icons';
children.push(data); let groups = this.getRecordGroups(records);
});
let children = []; for (const item of records) {
Ext.Object.each(groups, function(key, group) { let group = item.data["backup-type"] + "/" + item.data["backup-id"];
let last_backup = 0; let children = groups[group].children;
group.children.forEach(function(item) {
if (item["backup-time"] > last_backup) {
last_backup = item["backup-time"];
group["backup-time"] = last_backup;
group.files = item.files;
group.size = item.size;
}
});
group.count = group.children.length;
children.push(group)
})
view.setRootNode({ let data = item.data;
expanded: true,
children: children
});
data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
data.leaf = true;
data.cls = 'no-leaf-icons';
children.push(data);
}
let children = [];
for (const [_key, group] of Object.entries(groups)) {
let last_backup = 0;
for (const item of group.children) {
if (item["backup-time"] > last_backup) {
last_backup = item["backup-time"];
group["backup-time"] = last_backup;
group.files = item.files;
group.size = item.size;
}
}
group.count = group.children.length;
children.push(group);
}
view.setRootNode({
expanded: true,
children: children
}); });
}, },
onPrune: function() { onPrune: function() {

View File

@ -22,6 +22,12 @@ Ext.define('PBS.DataStorePanel', {
datastore: '{datastore}', datastore: '{datastore}',
}, },
}, },
{
xtype: 'pbsDataStoreStatistic',
cbind: {
datastore: '{datastore}',
},
},
{ {
itemId: 'acl', itemId: 'acl',
xtype: 'pbsACLView', xtype: 'pbsACLView',

View File

@ -52,14 +52,13 @@ Ext.define('PBS.DataStorePruneInputPanel', {
method: "POST", method: "POST",
params: params, params: params,
callback: function() { callback: function() {
console.log("DONE"); return; // for easy breakpoint setting
}, },
failure: function (response, opts) { failure: function (response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus); Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}, },
success: function(response, options) { success: function(response, options) {
var data = response.result.data; var data = response.result.data;
console.log(data);
view.prune_store.setData(data); view.prune_store.setData(data);
} }
}); });

108
www/DataStoreStatistic.js Normal file
View File

@ -0,0 +1,108 @@
Ext.define('pve-rrd-datastore', {
extend: 'Ext.data.Model',
fields: [
'used',
'total',
'read_ios',
'read_bytes',
'read_ticks',
'write_ios',
'write_bytes',
'write_ticks',
{
name: 'read_delay', calculate: function(data) {
if (data.read_ios === undefined || data.read_ios === 0 || data.read_ticks == undefined) {
return undefined;
}
return (data.read_ticks*1000)/data.read_ios;
}
},
{
name: 'write_delay', calculate: function(data) {
if (data.write_ios === undefined || data.write_ios === 0 || data.write_ticks == undefined) {
return undefined;
}
return (data.write_ticks*1000)/data.write_ios;
}
},
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
]
});
Ext.define('PBS.DataStoreStatistic', {
extend: 'Ext.panel.Panel',
alias: 'widget.pbsDataStoreStatistic',
title: gettext('Statistics'),
scrollable: true,
initComponent: function() {
var me = this;
if (!me.datastore) {
throw "no datastore specified";
}
me.tbar = [ '->', { xtype: 'proxmoxRRDTypeSelector' } ];
var rrdstore = Ext.create('Proxmox.data.RRDStore', {
rrdurl: "/api2/json/admin/datastore/" + me.datastore + "/rrd",
model: 'pve-rrd-datastore'
});
me.items = {
xtype: 'container',
itemId: 'itemcontainer',
layout: 'column',
minWidth: 700,
defaults: {
minHeight: 320,
padding: 5,
columnWidth: 1
},
items: [
{
xtype: 'proxmoxRRDChart',
title: gettext('Storage usage (bytes)'),
fields: ['total','used'],
fieldTitles: [gettext('Total'), gettext('Storage usage')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Transfer Rate (bytes/second)'),
fields: ['read_bytes','write_bytes'],
fieldTitles: [gettext('Read'), gettext('Write')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Input/Output Operations per Second (IOPS)'),
fields: ['read_ios','write_ios'],
fieldTitles: [gettext('Read'), gettext('Write')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Delay (ms)'),
fields: ['read_delay','write_delay'],
fieldTitles: [gettext('Read'), gettext('Write')],
store: rrdstore
},
]
};
me.listeners = {
activate: function() {
rrdstore.startUpdate();
},
destroy: function() {
rrdstore.stopUpdate();
},
};
me.callParent();
}
});

View File

@ -8,120 +8,13 @@ Ext.define('pbs-datastore-list', {
idProperty: 'store' idProperty: 'store'
}); });
Ext.define('pve-rrd-node', {
extend: 'Ext.data.Model',
fields: [
{
name: 'cpu',
// percentage
convert: function(value) {
return value*100;
}
},
{
name: 'iowait',
// percentage
convert: function(value) {
return value*100;
}
},
'netin',
'netout',
'memtotal',
'memused',
'swaptotal',
'swapused',
'roottotal',
'rootused',
'loadavg',
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
]
});
Ext.define('PBS.DataStoreStatus', { Ext.define('PBS.DataStoreStatus', {
extend: 'Ext.panel.Panel', extend: 'Ext.panel.Panel',
alias: 'widget.pbsDataStoreStatus', alias: 'widget.pbsDataStoreStatus',
title: gettext('Data Store Status'), title: gettext('Data Store Status'),
tbar: ['->', { xtype: 'proxmoxRRDTypeSelector' } ],
scrollable: true, scrollable: true,
initComponent: function() { html: "fixme: Add Datastore status",
var me = this;
// this is just a test for the RRD api
var rrdstore = Ext.create('Proxmox.data.RRDStore', {
rrdurl: "/api2/json/nodes/localhost/rrd",
model: 'pve-rrd-node'
});
me.items = {
xtype: 'container',
itemId: 'itemcontainer',
layout: 'column',
minWidth: 700,
defaults: {
minHeight: 320,
padding: 5,
columnWidth: 1
},
items: [
{
xtype: 'proxmoxRRDChart',
title: gettext('CPU usage'),
fields: ['cpu','iowait'],
fieldTitles: [gettext('CPU usage'), gettext('IO delay')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Server load'),
fields: ['loadavg'],
fieldTitles: [gettext('Load average')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Memory usage'),
fields: ['memtotal','memused'],
fieldTitles: [gettext('Total'), gettext('RAM usage')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Swap usage'),
fields: ['swaptotal','swapused'],
fieldTitles: [gettext('Total'), gettext('Swap usage')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Network traffic'),
fields: ['netin','netout'],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Root Disk usage'),
fields: ['roottotal','rootused'],
fieldTitles: [gettext('Total'), gettext('Disk usage')],
store: rrdstore
},
]
};
me.listeners = {
activate: function() {
rrdstore.startUpdate();
},
destroy: function() {
rrdstore.stopUpdate();
},
};
me.callParent();
}
}); });

View File

@ -7,8 +7,10 @@ IMAGES := \
JSSRC= \ JSSRC= \
form/UserSelector.js \ form/UserSelector.js \
config/UserView.js \ config/UserView.js \
config/RemoteView.js \
config/ACLView.js \ config/ACLView.js \
window/UserEdit.js \ window/UserEdit.js \
window/RemoteEdit.js \
window/ACLEdit.js \ window/ACLEdit.js \
Utils.js \ Utils.js \
LoginView.js \ LoginView.js \
@ -18,6 +20,7 @@ JSSRC= \
DataStorePrune.js \ DataStorePrune.js \
DataStoreConfig.js \ DataStoreConfig.js \
DataStoreStatus.js \ DataStoreStatus.js \
DataStoreStatistic.js \
DataStoreContent.js \ DataStoreContent.js \
DataStorePanel.js \ DataStorePanel.js \
ServerStatus.js \ ServerStatus.js \

View File

@ -30,6 +30,12 @@ Ext.define('PBS.store.NavigationStore', {
path: 'pbsACLView', path: 'pbsACLView',
leaf: true leaf: true
}, },
{
text: gettext('Remotes'),
iconCls: 'fa fa-server',
path: 'pbsRemoteView',
leaf: true,
},
{ {
text: gettext('Data Store'), text: gettext('Data Store'),
iconCls: 'fa fa-archive', iconCls: 'fa fa-archive',

View File

@ -1,10 +1,39 @@
Ext.define('pve-rrd-node', {
extend: 'Ext.data.Model',
fields: [
{
name: 'cpu',
// percentage
convert: function(value) {
return value*100;
}
},
{
name: 'iowait',
// percentage
convert: function(value) {
return value*100;
}
},
'netin',
'netout',
'memtotal',
'memused',
'swaptotal',
'swapused',
'roottotal',
'rootused',
'loadavg',
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
]
});
Ext.define('PBS.ServerStatus', { Ext.define('PBS.ServerStatus', {
extend: 'Ext.panel.Panel', extend: 'Ext.panel.Panel',
alias: 'widget.pbsServerStatus', alias: 'widget.pbsServerStatus',
title: gettext('ServerStatus'), title: gettext('ServerStatus'),
html: "Add Something usefule here ?", scrollable: true,
initComponent: function() { initComponent: function() {
var me = this; var me = this;
@ -41,7 +70,97 @@ Ext.define('PBS.ServerStatus', {
iconCls: 'fa fa-power-off' iconCls: 'fa fa-power-off'
}); });
me.tbar = [ restartBtn, shutdownBtn ]; me.tbar = [ restartBtn, shutdownBtn, '->', { xtype: 'proxmoxRRDTypeSelector' } ];
var rrdstore = Ext.create('Proxmox.data.RRDStore', {
rrdurl: "/api2/json/nodes/localhost/rrd",
model: 'pve-rrd-node'
});
me.items = {
xtype: 'container',
itemId: 'itemcontainer',
layout: 'column',
minWidth: 700,
defaults: {
minHeight: 320,
padding: 5,
columnWidth: 1
},
items: [
{
xtype: 'proxmoxRRDChart',
title: gettext('CPU usage'),
fields: ['cpu','iowait'],
fieldTitles: [gettext('CPU usage'), gettext('IO delay')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Server load'),
fields: ['loadavg'],
fieldTitles: [gettext('Load average')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Memory usage'),
fields: ['memtotal','memused'],
fieldTitles: [gettext('Total'), gettext('RAM usage')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Swap usage'),
fields: ['swaptotal','swapused'],
fieldTitles: [gettext('Total'), gettext('Swap usage')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Network traffic'),
fields: ['netin','netout'],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Root Disk usage'),
fields: ['total','used'],
fieldTitles: [gettext('Total'), gettext('Disk usage')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Root Disk Transfer Rate (bytes/second)'),
fields: ['read_bytes','write_bytes'],
fieldTitles: [gettext('Read'), gettext('Write')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Root Disk Input/Output Operations per Second (IOPS)'),
fields: ['read_ios','write_ios'],
fieldTitles: [gettext('Read'), gettext('Write')],
store: rrdstore
},
{
xtype: 'proxmoxRRDChart',
title: gettext('Root Disk IO Delay (ms)'),
fields: ['read_delay','write_delay'],
fieldTitles: [gettext('Read'), gettext('Write')],
store: rrdstore
},
]
};
me.listeners = {
activate: function() {
rrdstore.startUpdate();
},
destroy: function() {
rrdstore.stopUpdate();
},
};
me.callParent(); me.callParent();
} }

View File

@ -25,14 +25,52 @@ Ext.define('PBS.Utils', {
return path.indexOf(PBS.Utils.dataStorePrefix) === 0; return path.indexOf(PBS.Utils.dataStorePrefix) === 0;
}, },
render_datetime_utc: function(datetime) {
let pad = (number) => number < 10 ? '0' + number : number;
return datetime.getUTCFullYear() +
'-' + pad(datetime.getUTCMonth() + 1) +
'-' + pad(datetime.getUTCDate()) +
'T' + pad(datetime.getUTCHours()) +
':' + pad(datetime.getUTCMinutes()) +
':' + pad(datetime.getUTCSeconds()) +
'Z';
},
render_datastore_worker_id: function(id, what) {
const result = id.match(/^(\S+)_([^_\s]+)_([^_\s]+)$/);
if (result) {
let datastore = result[1], type = result[2], id = result[3];
return `Datastore ${datastore} - ${what} ${type}/${id}`;
}
return what;
},
render_datastore_time_worker_id: function(id, what) {
const res = id.match(/^(\S+)_([^_\s]+)_([^_\s]+)_([^_\s]+)$/);
if (res) {
let datastore = res[1], type = res[2], id = res[3];
let datetime = Ext.Date.parse(parseInt(res[4], 16), 'U');
let utctime = PBS.Utils.render_datetime_utc(datetime);
return `Datastore ${datastore} - ${what} ${type}/${id}/${utctime}`;
}
return what;
},
constructor: function() { constructor: function() {
var me = this; var me = this;
// do whatever you want here // do whatever you want here
Proxmox.Utils.override_task_descriptions({ Proxmox.Utils.override_task_descriptions({
garbage_collection: ['Datastore', gettext('Garbage collect') ], garbage_collection: ['Datastore', gettext('Garbage collect') ],
backup: [ '', gettext('Backup') ], sync: ['Datastore', gettext('Remote Sync') ],
reader: [ '', gettext('Read datastore objects') ], // FIXME: better one prune: (type, id) => {
return PBS.Utils.render_datastore_worker_id(id, gettext('Prune'));
},
backup: (type, id) => {
return PBS.Utils.render_datastore_worker_id(id, gettext('Backup'));
},
reader: (type, id) => {
return PBS.Utils.render_datastore_time_worker_id(id, gettext('Read objects'));
},
}); });
} }
}); });

View File

@ -5,7 +5,7 @@ Ext.define('pmx-acls', {
{ {
name: 'aclid', name: 'aclid',
calculate: function(data) { calculate: function(data) {
return `${data.path} for ${data.ugid}`; return `${data.path} for ${data.ugid} - ${data.roleid}`;
}, },
}, },
], ],
@ -77,6 +77,17 @@ Ext.define('PBS.config.ACLView', {
params.exact = view.aclExact; params.exact = view.aclExact;
} }
proxy.setExtraParams(params); proxy.setExtraParams(params);
Proxmox.Utils.monStoreErrors(view, view.getStore().rstore);
},
control: {
'#': { // view
activate: function() {
this.getView().getStore().rstore.startUpdate();
},
deactivate: function() {
this.getView().getStore().rstore.stopUpdate();
},
},
}, },
}, },
@ -84,12 +95,11 @@ Ext.define('PBS.config.ACLView', {
type: 'diff', type: 'diff',
autoDestroy: true, autoDestroy: true,
autoDestroyRstore: true, autoDestroyRstore: true,
sorters: 'userid', sorters: 'aclid',
rstore: { rstore: {
type: 'update', type: 'update',
storeid: 'pmx-acls', storeid: 'pmx-acls',
model: 'pmx-acls', model: 'pmx-acls',
autoStart: true,
interval: 5000, interval: 5000,
}, },
}, },

129
www/config/RemoteView.js Normal file
View File

@ -0,0 +1,129 @@
Ext.define('pmx-remotes', {
extend: 'Ext.data.Model',
fields: [ 'name', 'host', 'userid', 'fingerprint' ],
idProperty: 'name',
proxy: {
type: 'proxmox',
url: '/api2/json/config/remote',
},
});
Ext.define('PBS.config.RemoteView', {
extend: 'Ext.grid.GridPanel',
alias: 'widget.pbsRemoteView',
stateful: true,
stateId: 'grid-remotes',
title: gettext('Remotes'),
controller: {
xclass: 'Ext.app.ViewController',
addRemote: function() {
let me = this;
Ext.create('PBS.window.RemoteEdit', {
listeners: {
destroy: function() {
me.reload();
},
},
}).show();
},
editRemote: function() {
let me = this;
let view = me.getView();
let selection = view.getSelection();
if (selection.length < 1) return;
Ext.create('PBS.window.RemoteEdit', {
name: selection[0].data.name,
listeners: {
destroy: function() {
me.reload();
},
},
}).show();
},
reload: function() { this.getView().getStore().rstore.load(); },
init: function(view) {
Proxmox.Utils.monStoreErrors(view, view.getStore().rstore);
},
},
listeners: {
activate: 'reload',
itemdblclick: 'editRemote',
},
store: {
type: 'diff',
autoDestroy: true,
autoDestroyRstore: true,
sorters: 'name',
rstore: {
type: 'update',
storeid: 'pmx-remotes',
model: 'pmx-remotes',
autoStart: true,
interval: 5000,
},
},
tbar: [
{
xtype: 'proxmoxButton',
text: gettext('Add'),
handler: 'addRemote',
selModel: false,
},
{
xtype: 'proxmoxButton',
text: gettext('Edit'),
handler: 'editRemote',
disabled: true,
},
{
xtype: 'proxmoxStdRemoveButton',
baseurl: '/config/remote',
callback: 'reload',
},
],
viewConfig: {
trackOver: false,
},
columns: [
{
header: gettext('Remote'),
width: 200,
sortable: true,
renderer: Ext.String.htmlEncode,
dataIndex: 'name',
},
{
header: gettext('Host'),
width: 200,
sortable: true,
dataIndex: 'host',
},
{
header: gettext('User name'),
width: 100,
sortable: true,
renderer: Ext.String.htmlEncode,
dataIndex: 'userid',
},
{
header: gettext('Fingerprint'),
sortable: false,
renderer: Ext.String.htmlEncode,
dataIndex: 'fingerprint',
flex: 1,
},
],
});

View File

@ -60,6 +60,10 @@ Ext.define('PBS.config.UserView', {
}, },
reload: function() { this.getView().getStore().rstore.load(); }, reload: function() { this.getView().getStore().rstore.load(); },
init: function(view) {
Proxmox.Utils.monStoreErrors(view, view.getStore().rstore);
},
}, },
listeners: { listeners: {

89
www/window/RemoteEdit.js Normal file
View File

@ -0,0 +1,89 @@
Ext.define('PBS.window.RemoteEdit', {
extend: 'Proxmox.window.Edit',
alias: 'widget.pbsRemoteEdit',
mixins: ['Proxmox.Mixin.CBind'],
userid: undefined,
isAdd: true,
subject: gettext('Remote'),
fieldDefaults: { labelWidth: 120 },
cbindData: function(initialConfig) {
let me = this;
let baseurl = '/api2/extjs/config/remote';
let name = initialConfig.name;
me.isCreate = !name;
me.url = name ? `${baseurl}/${name}` : baseurl;
me.method = name ? 'PUT' : 'POST';
me.autoLoad = !!name;
return {
passwordEmptyText: me.isCreate ? '' : gettext('Unchanged'),
};
},
items: {
xtype: 'inputpanel',
column1: [
{
xtype: 'pmxDisplayEditField',
name: 'name',
fieldLabel: gettext('Remote'),
renderer: Ext.htmlEncode,
allowBlank: false,
minLength: 4,
cbind: {
editable: '{isCreate}',
},
},
{
xtype: 'proxmoxtextfield',
allowBlank: false,
name: 'host',
fieldLabel: gettext('Host'),
},
],
column2: [
{
xtype: 'proxmoxtextfield',
allowBlank: false,
name: 'userid',
fieldLabel: gettext('Userid'),
},
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('Password'),
name: 'password',
cbind: {
emptyText: '{passwordEmptyText}',
allowBlank: '{!isCreate}',
},
},
],
columnB: [
{
xtype: 'proxmoxtextfield',
name: 'fingerprint',
fieldLabel: gettext('Fingerprint'),
},
],
},
getValues: function() {
let me = this;
let values = me.callParent(arguments);
if (values.password === '') {
delete values.password;
}
return values;
},
});