Compare commits
38 Commits
Author | SHA1 | Date | |
---|---|---|---|
d80d1f9a2b | |||
6161ac18a4 | |||
6bba120d14 | |||
91e5bb49f5 | |||
547e3c2f6c | |||
4bf26be3bb | |||
25c550bc28 | |||
0146133b4b | |||
3eeba68785 | |||
f5056656b2 | |||
8c87743642 | |||
05d755b282 | |||
143b654550 | |||
97fab7aa11 | |||
ed216fd773 | |||
0f13623443 | |||
dbd959d43f | |||
f68ae22cc0 | |||
06c3dc8a8e | |||
a6fbbd03c8 | |||
26956d73a2 | |||
3f98b34705 | |||
40dc103103 | |||
12710fd3c3 | |||
9e2a4653b4 | |||
de4db62c57 | |||
1a0d3d11d2 | |||
8c03041a2c | |||
3fcc4b4e5c | |||
3ed07ed2cd | |||
75410d65ef | |||
83fd4b3b1b | |||
bfa0146c00 | |||
5dcdcea293 | |||
99f443c6ae | |||
4f966d0592 | |||
db0c228719 | |||
880fa939d1 |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proxmox-backup"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3"
|
||||
@ -36,7 +36,7 @@ pam = "0.7"
|
||||
pam-sys = "0.5"
|
||||
percent-encoding = "2.1"
|
||||
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 = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro" ] }
|
||||
regex = "1.2"
|
||||
|
18
debian/changelog
vendored
18
debian/changelog
vendored
@ -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
|
||||
|
||||
* see git changelog (too many changes)
|
||||
|
1
debian/control.in
vendored
1
debian/control.in
vendored
@ -3,6 +3,7 @@ Architecture: any
|
||||
Depends: fonts-font-awesome,
|
||||
libjs-extjs (>= 6.0.1),
|
||||
libzstd1 (>= 1.3.8),
|
||||
proxmox-backup-docs,
|
||||
proxmox-mini-journalreader,
|
||||
proxmox-widget-toolkit (>= 2.2-4),
|
||||
${misc:Depends},
|
||||
|
@ -843,6 +843,46 @@ fn upload_backup_log(
|
||||
}.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]
|
||||
const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
|
||||
(
|
||||
@ -871,6 +911,11 @@ const DATASTORE_INFO_SUBDIRS: SubdirMap = &[
|
||||
&Router::new()
|
||||
.post(&API_METHOD_PRUNE)
|
||||
),
|
||||
(
|
||||
"rrd",
|
||||
&Router::new()
|
||||
.get(&API_METHOD_GET_RRD_STATS)
|
||||
),
|
||||
(
|
||||
"snapshots",
|
||||
&Router::new()
|
||||
|
@ -1,6 +1,7 @@
|
||||
use anyhow::{bail, Error};
|
||||
use serde_json::Value;
|
||||
use ::serde::{Deserialize, Serialize};
|
||||
use base64;
|
||||
|
||||
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).",
|
||||
type: Array,
|
||||
items: {
|
||||
type: Object,
|
||||
type: remote::Remote,
|
||||
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: {
|
||||
@ -47,14 +29,20 @@ use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY};
|
||||
pub fn list_remotes(
|
||||
_param: Value,
|
||||
_info: &ApiMethod,
|
||||
_rpcenv: &mut dyn RpcEnvironment,
|
||||
) -> Result<Value, Error> {
|
||||
mut rpcenv: &mut dyn RpcEnvironment,
|
||||
) -> Result<Vec<remote::Remote>, Error> {
|
||||
|
||||
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(
|
||||
@ -88,19 +76,21 @@ pub fn list_remotes(
|
||||
},
|
||||
)]
|
||||
/// 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 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()?;
|
||||
|
||||
if let Some(_) = config.sections.get(&name) {
|
||||
bail!("remote '{}' already exists.", name);
|
||||
if let Some(_) = config.sections.get(&remote.name) {
|
||||
bail!("remote '{}' already exists.", remote.name);
|
||||
}
|
||||
|
||||
config.set_data(&name, "remote", &remote)?;
|
||||
config.set_data(&remote.name, "remote", &remote)?;
|
||||
|
||||
remote::save_config(&config)?;
|
||||
|
||||
@ -124,11 +114,15 @@ pub fn create_remote(name: String, param: Value) -> Result<(), Error> {
|
||||
}
|
||||
)]
|
||||
/// 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 mut data = config.lookup_json("remote", &name)?;
|
||||
data.as_object_mut().unwrap()
|
||||
.insert("digest".into(), proxmox::tools::digest_to_hex(&digest).into());
|
||||
let mut data: remote::Remote = config.lookup("remote", &name)?;
|
||||
data.password = "".to_string(); // do not return password in api
|
||||
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
@ -248,6 +242,10 @@ pub fn update_remote(
|
||||
name: {
|
||||
schema: REMOTE_ID_SCHEMA,
|
||||
},
|
||||
digest: {
|
||||
optional: true,
|
||||
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
|
||||
},
|
||||
},
|
||||
},
|
||||
access: {
|
||||
@ -255,12 +253,16 @@ pub fn update_remote(
|
||||
},
|
||||
)]
|
||||
/// 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 ?
|
||||
// fixme: check digest ?
|
||||
let _lock = crate::tools::open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
|
||||
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) {
|
||||
Some(_) => { config.sections.remove(&name); },
|
||||
|
@ -34,8 +34,10 @@ fn get_node_stats(
|
||||
"memtotal", "memused",
|
||||
"swaptotal", "swapused",
|
||||
"netin", "netout",
|
||||
"roottotal", "rootused",
|
||||
"loadavg",
|
||||
"total", "used",
|
||||
"read_ios", "read_bytes", "read_ticks",
|
||||
"write_ios", "write_bytes", "write_ticks",
|
||||
],
|
||||
timeframe,
|
||||
cf,
|
||||
|
@ -260,11 +260,9 @@ fn task_mgmt_cli() -> CommandLineInterface {
|
||||
"remote-store": {
|
||||
schema: DATASTORE_SCHEMA,
|
||||
},
|
||||
delete: {
|
||||
description: "Delete vanished backups. This remove the local copy if the remote backup was deleted.",
|
||||
type: Boolean,
|
||||
"remove-vanished": {
|
||||
schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
|
||||
optional: true,
|
||||
default: true,
|
||||
},
|
||||
"output-format": {
|
||||
schema: OUTPUT_FORMAT,
|
||||
@ -278,7 +276,7 @@ async fn pull_datastore(
|
||||
remote: String,
|
||||
remote_store: String,
|
||||
local_store: String,
|
||||
delete: Option<bool>,
|
||||
remove_vanished: Option<bool>,
|
||||
param: Value,
|
||||
) -> Result<Value, Error> {
|
||||
|
||||
@ -292,8 +290,8 @@ async fn pull_datastore(
|
||||
"remote-store": remote_store,
|
||||
});
|
||||
|
||||
if let Some(delete) = delete {
|
||||
args["delete"] = delete.into();
|
||||
if let Some(remove_vanished) = remove_vanished {
|
||||
args["remove-vanished"] = Value::from(remove_vanished);
|
||||
}
|
||||
|
||||
let result = client.post("api2/json/pull", Some(args)).await?;
|
||||
|
@ -1,4 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
use std::ffi::OsString;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use futures::*;
|
||||
@ -7,6 +9,7 @@ use openssl::ssl::{SslMethod, SslAcceptor, SslFiletype};
|
||||
|
||||
use proxmox::try_block;
|
||||
use proxmox::api::RpcEnvironmentType;
|
||||
use proxmox::sys::linux::procfs::mountinfo::{Device, MountInfo};
|
||||
|
||||
use proxmox_backup::configdir;
|
||||
use proxmox_backup::buildcfg;
|
||||
@ -14,6 +17,7 @@ use proxmox_backup::server;
|
||||
use proxmox_backup::tools::daemon;
|
||||
use proxmox_backup::server::{ApiConfig, rest::*};
|
||||
use proxmox_backup::auth_helpers::*;
|
||||
use proxmox_backup::tools::disks::{ DiskManage, zfs::zfs_pool_stats };
|
||||
|
||||
fn main() {
|
||||
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(
|
||||
worker_type,
|
||||
Some(job_config.store.clone()),
|
||||
Some(job_id.clone()),
|
||||
&username.clone(),
|
||||
false,
|
||||
move |worker| async move {
|
||||
@ -619,6 +623,7 @@ async fn generate_host_stats() {
|
||||
read_meminfo, read_proc_stat, read_proc_net_dev, read_loadavg};
|
||||
use proxmox_backup::config::datastore;
|
||||
|
||||
|
||||
proxmox_backup::tools::runtime::block_in_place(move || {
|
||||
|
||||
match read_proc_stat() {
|
||||
@ -670,15 +675,9 @@ async fn generate_host_stats() {
|
||||
}
|
||||
}
|
||||
|
||||
match disk_usage(std::path::Path::new("/")) {
|
||||
Ok((total, used, _avail)) => {
|
||||
rrd_update_gauge("host/roottotal", total as f64);
|
||||
rrd_update_gauge("host/rootused", used as f64);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("read root disk_usage failed - {}", err);
|
||||
}
|
||||
}
|
||||
let disk_manager = DiskManage::new();
|
||||
|
||||
gather_disk_stats(disk_manager.clone(), Path::new("/"), "host");
|
||||
|
||||
match datastore::config() {
|
||||
Ok((config, _)) => {
|
||||
@ -686,16 +685,10 @@ async fn generate_host_stats() {
|
||||
config.convert_to_typed_array("datastore").unwrap_or(Vec::new());
|
||||
|
||||
for config in datastore_list {
|
||||
match disk_usage(std::path::Path::new(&config.path)) {
|
||||
Ok((total, used, _avail)) => {
|
||||
let rrd_key = format!("datastore/{}", config.name);
|
||||
rrd_update_gauge(&rrd_key, total as f64);
|
||||
rrd_update_gauge(&rrd_key, used as f64);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("read disk_usage on {:?} failed - {}", config.path, err);
|
||||
}
|
||||
}
|
||||
|
||||
let rrd_prefix = format!("datastore/{}", config.name);
|
||||
let path = std::path::Path::new(&config.path);
|
||||
gather_disk_stats(disk_manager.clone(), path, &rrd_prefix);
|
||||
}
|
||||
}
|
||||
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)
|
||||
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))
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Error};
|
||||
use anyhow::{Error};
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
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 fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
|
||||
let content = match std::fs::read_to_string(DATASTORE_CFG_FILENAME) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
String::from("")
|
||||
} else {
|
||||
bail!("unable to read '{}' - {}", DATASTORE_CFG_FILENAME, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let content = proxmox::tools::fs::file_read_optional_string(DATASTORE_CFG_FILENAME)?;
|
||||
let content = content.unwrap_or(String::from(""));
|
||||
|
||||
let digest = openssl::sha::sha256(content.as_bytes());
|
||||
let data = CONFIG.parse(DATASTORE_CFG_FILENAME, &content)?;
|
||||
|
@ -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_LOCKFILE: &str = "/var/lock/pve-network.lck";
|
||||
|
||||
|
||||
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);
|
||||
|
||||
|
@ -149,23 +149,8 @@ pub fn compute_file_diff(filename: &str, shadow: &str) -> Result<String, Error>
|
||||
.output()
|
||||
.map_err(|err| format_err!("failed to execute diff - {}", err))?;
|
||||
|
||||
if !output.status.success() {
|
||||
match output.status.code() {
|
||||
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)?;
|
||||
let diff = crate::tools::command_output(output, Some(|c| c == 0 || c == 1))
|
||||
.map_err(|err| format_err!("diff failed: {}", err))?;
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
@ -180,17 +165,14 @@ pub fn assert_ifupdown2_installed() -> Result<(), Error> {
|
||||
|
||||
pub fn network_reload() -> Result<(), Error> {
|
||||
|
||||
let status = Command::new("/sbin/ifreload")
|
||||
let output = Command::new("/sbin/ifreload")
|
||||
.arg("-a")
|
||||
.status()
|
||||
.map_err(|err| format_err!("failed to execute ifreload: - {}", err))?;
|
||||
.output()
|
||||
.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(())
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Error};
|
||||
use anyhow::{Error};
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
use serde::{Serialize, Deserialize};
|
||||
@ -29,6 +29,9 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
|
||||
|
||||
#[api(
|
||||
properties: {
|
||||
name: {
|
||||
schema: REMOTE_ID_SCHEMA,
|
||||
},
|
||||
comment: {
|
||||
optional: true,
|
||||
schema: SINGLE_LINE_COMMENT_SCHEMA,
|
||||
@ -51,10 +54,13 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
|
||||
#[derive(Serialize,Deserialize)]
|
||||
/// Remote properties.
|
||||
pub struct Remote {
|
||||
pub name: String,
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
pub host: String,
|
||||
pub userid: String,
|
||||
#[serde(skip_serializing_if="String::is_empty")]
|
||||
#[serde(with = "proxmox::tools::serde::string_as_base64")]
|
||||
pub password: String,
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub fingerprint: Option<String>,
|
||||
@ -66,7 +72,7 @@ fn init() -> SectionConfig {
|
||||
_ => 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);
|
||||
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 fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
|
||||
let content = match std::fs::read_to_string(REMOTE_CFG_FILENAME) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
String::from("")
|
||||
} else {
|
||||
bail!("unable to read '{}' - {}", REMOTE_CFG_FILENAME, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let content = proxmox::tools::fs::file_read_optional_string(REMOTE_CFG_FILENAME)?;
|
||||
let content = content.unwrap_or(String::from(""));
|
||||
|
||||
let digest = openssl::sha::sha256(content.as_bytes());
|
||||
let data = CONFIG.parse(REMOTE_CFG_FILENAME, &content)?;
|
||||
|
@ -1,4 +1,4 @@
|
||||
use anyhow::{bail, Error};
|
||||
use anyhow::{Error};
|
||||
use lazy_static::lazy_static;
|
||||
use std::collections::HashMap;
|
||||
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 fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
|
||||
let content = match std::fs::read_to_string(SYNC_CFG_FILENAME) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
String::from("")
|
||||
} else {
|
||||
bail!("unable to read '{}' - {}", SYNC_CFG_FILENAME, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let content = proxmox::tools::fs::file_read_optional_string(SYNC_CFG_FILENAME)?;
|
||||
let content = content.unwrap_or(String::from(""));
|
||||
|
||||
let digest = openssl::sha::sha256(content.as_bytes());
|
||||
let data = CONFIG.parse(SYNC_CFG_FILENAME, &content)?;
|
||||
|
@ -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 fn config() -> Result<(SectionConfigData, [u8;32]), Error> {
|
||||
let content = match std::fs::read_to_string(USER_CFG_FILENAME) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
String::from("")
|
||||
} else {
|
||||
bail!("unable to read '{}' - {}", USER_CFG_FILENAME, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let content = proxmox::tools::fs::file_read_optional_string(USER_CFG_FILENAME)?;
|
||||
let content = content.unwrap_or(String::from(""));
|
||||
|
||||
let digest = openssl::sha::sha256(content.as_bytes());
|
||||
let mut data = CONFIG.parse(USER_CFG_FILENAME, &content)?;
|
||||
|
@ -418,10 +418,8 @@ impl WorkerTask {
|
||||
let logger = FileLogger::new(&path, to_stdout)?;
|
||||
nix::unistd::chown(&path, Some(backup_user.uid), Some(backup_user.gid))?;
|
||||
|
||||
update_active_workers(Some(&upid))?;
|
||||
|
||||
let worker = Arc::new(Self {
|
||||
upid,
|
||||
upid: upid.clone(),
|
||||
abort_requested: AtomicBool::new(false),
|
||||
data: Mutex::new(WorkerTaskData {
|
||||
logger,
|
||||
@ -430,10 +428,14 @@ impl WorkerTask {
|
||||
}),
|
||||
});
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
update_active_workers(Some(&upid))?;
|
||||
|
||||
Ok(worker)
|
||||
}
|
||||
|
34
src/tools.rs
34
src/tools.rs
@ -474,6 +474,40 @@ pub fn normalize_uri_path(path: &str) -> Result<(String, Vec<&str>), Error> {
|
||||
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> {
|
||||
use nix::fcntl::{fcntl, FdFlag, F_GETFD, F_SETFD};
|
||||
let mut flags = FdFlag::from_bits(fcntl(fd, F_GETFD)?)
|
||||
|
@ -16,6 +16,8 @@ use proxmox::sys::error::io_err_other;
|
||||
use proxmox::sys::linux::procfs::MountInfo;
|
||||
use proxmox::{io_bail, io_format_err};
|
||||
|
||||
pub mod zfs;
|
||||
|
||||
bitflags! {
|
||||
/// Ways a device is being used.
|
||||
pub struct DiskUse: u32 {
|
||||
@ -222,7 +224,7 @@ impl Disk {
|
||||
/// Read from a file in this device's sys 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());
|
||||
|
||||
std::fs::read(self.syspath().join(path))
|
||||
@ -423,6 +425,30 @@ impl Disk {
|
||||
.is_mounted
|
||||
.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.
|
||||
@ -439,3 +465,16 @@ pub enum DiskType {
|
||||
/// Some kind of USB disk, but we don't know more than that.
|
||||
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
47
src/tools/disks/zfs.rs
Normal 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))
|
||||
}
|
@ -29,34 +29,44 @@ Ext.define('PBS.DataStoreContent', {
|
||||
throw "no datastore specified";
|
||||
}
|
||||
|
||||
this.data_store = Ext.create('Ext.data.Store', {
|
||||
this.store = Ext.create('Ext.data.Store', {
|
||||
model: 'pbs-data-store-snapshots',
|
||||
sorters: 'backup-group',
|
||||
groupField: 'backup-group',
|
||||
});
|
||||
this.store.on('load', this.onLoad, this);
|
||||
|
||||
Proxmox.Utils.monStoreErrors(view, view.store, true);
|
||||
this.reload(); // initial load
|
||||
},
|
||||
|
||||
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`;
|
||||
this.data_store.setProxy({
|
||||
this.store.setProxy({
|
||||
type: 'proxmox',
|
||||
url: url
|
||||
});
|
||||
|
||||
this.data_store.load(function(records, operation, success) {
|
||||
this.store.load();
|
||||
},
|
||||
|
||||
getRecordGroups: function(records) {
|
||||
let groups = {};
|
||||
|
||||
records.forEach(function(item) {
|
||||
for (const item of records) {
|
||||
var btype = item.data["backup-type"];
|
||||
let group = btype + "/" + item.data["backup-id"];
|
||||
|
||||
if (groups[group] !== undefined)
|
||||
return;
|
||||
if (groups[group] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cls = '';
|
||||
if (btype === 'vm') {
|
||||
@ -66,7 +76,8 @@ Ext.define('PBS.DataStoreContent', {
|
||||
} else if (btype === 'host') {
|
||||
cls = 'fa-building';
|
||||
} else {
|
||||
return btype + '/' + value;
|
||||
console.warn(`got unkown backup-type '${btype}'`);
|
||||
continue; // FIXME: auto render? what do?
|
||||
}
|
||||
|
||||
groups[group] = {
|
||||
@ -78,60 +89,52 @@ Ext.define('PBS.DataStoreContent', {
|
||||
backup_id: item.data["backup-id"],
|
||||
children: []
|
||||
};
|
||||
});
|
||||
|
||||
let backup_time_to_string = function(backup_time) {
|
||||
let pad = function(number) {
|
||||
if (number < 10) {
|
||||
return '0' + number;
|
||||
}
|
||||
return number;
|
||||
};
|
||||
return backup_time.getUTCFullYear() +
|
||||
'-' + 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;
|
||||
},
|
||||
|
||||
onLoad: function(store, records, success) {
|
||||
let view = this.getView();
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
let groups = this.getRecordGroups(records);
|
||||
|
||||
for (const item of records) {
|
||||
let group = item.data["backup-type"] + "/" + item.data["backup-id"];
|
||||
let children = groups[group].children;
|
||||
|
||||
let data = item.data;
|
||||
|
||||
data.text = Ext.Date.format(data["backup-time"], 'Y-m-d H:i:s');
|
||||
data.text = group + '/' + backup_time_to_string(data["backup-time"]);
|
||||
data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
|
||||
data.leaf = true;
|
||||
data.cls = 'no-leaf-icons';
|
||||
|
||||
children.push(data);
|
||||
});
|
||||
}
|
||||
|
||||
let children = [];
|
||||
Ext.Object.each(groups, function(key, group) {
|
||||
for (const [_key, group] of Object.entries(groups)) {
|
||||
let last_backup = 0;
|
||||
group.children.forEach(function(item) {
|
||||
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)
|
||||
})
|
||||
children.push(group);
|
||||
}
|
||||
|
||||
view.setRootNode({
|
||||
expanded: true,
|
||||
children: children
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
onPrune: function() {
|
||||
|
@ -22,6 +22,12 @@ Ext.define('PBS.DataStorePanel', {
|
||||
datastore: '{datastore}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'pbsDataStoreStatistic',
|
||||
cbind: {
|
||||
datastore: '{datastore}',
|
||||
},
|
||||
},
|
||||
{
|
||||
itemId: 'acl',
|
||||
xtype: 'pbsACLView',
|
||||
|
@ -52,14 +52,13 @@ Ext.define('PBS.DataStorePruneInputPanel', {
|
||||
method: "POST",
|
||||
params: params,
|
||||
callback: function() {
|
||||
console.log("DONE");
|
||||
return; // for easy breakpoint setting
|
||||
},
|
||||
failure: function (response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
success: function(response, options) {
|
||||
var data = response.result.data;
|
||||
console.log(data);
|
||||
view.prune_store.setData(data);
|
||||
}
|
||||
});
|
||||
|
108
www/DataStoreStatistic.js
Normal file
108
www/DataStoreStatistic.js
Normal 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();
|
||||
}
|
||||
|
||||
});
|
@ -8,120 +8,13 @@ Ext.define('pbs-datastore-list', {
|
||||
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', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
alias: 'widget.pbsDataStoreStatus',
|
||||
|
||||
title: gettext('Data Store Status'),
|
||||
tbar: ['->', { xtype: 'proxmoxRRDTypeSelector' } ],
|
||||
|
||||
scrollable: true,
|
||||
|
||||
initComponent: function() {
|
||||
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();
|
||||
}
|
||||
html: "fixme: Add Datastore status",
|
||||
});
|
||||
|
@ -7,8 +7,10 @@ IMAGES := \
|
||||
JSSRC= \
|
||||
form/UserSelector.js \
|
||||
config/UserView.js \
|
||||
config/RemoteView.js \
|
||||
config/ACLView.js \
|
||||
window/UserEdit.js \
|
||||
window/RemoteEdit.js \
|
||||
window/ACLEdit.js \
|
||||
Utils.js \
|
||||
LoginView.js \
|
||||
@ -18,6 +20,7 @@ JSSRC= \
|
||||
DataStorePrune.js \
|
||||
DataStoreConfig.js \
|
||||
DataStoreStatus.js \
|
||||
DataStoreStatistic.js \
|
||||
DataStoreContent.js \
|
||||
DataStorePanel.js \
|
||||
ServerStatus.js \
|
||||
|
@ -30,6 +30,12 @@ Ext.define('PBS.store.NavigationStore', {
|
||||
path: 'pbsACLView',
|
||||
leaf: true
|
||||
},
|
||||
{
|
||||
text: gettext('Remotes'),
|
||||
iconCls: 'fa fa-server',
|
||||
path: 'pbsRemoteView',
|
||||
leaf: true,
|
||||
},
|
||||
{
|
||||
text: gettext('Data Store'),
|
||||
iconCls: 'fa fa-archive',
|
||||
|
@ -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', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
alias: 'widget.pbsServerStatus',
|
||||
|
||||
title: gettext('ServerStatus'),
|
||||
|
||||
html: "Add Something usefule here ?",
|
||||
scrollable: true,
|
||||
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
@ -41,7 +70,97 @@ Ext.define('PBS.ServerStatus', {
|
||||
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();
|
||||
}
|
||||
|
42
www/Utils.js
42
www/Utils.js
@ -25,14 +25,52 @@ Ext.define('PBS.Utils', {
|
||||
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() {
|
||||
var me = this;
|
||||
|
||||
// do whatever you want here
|
||||
Proxmox.Utils.override_task_descriptions({
|
||||
garbage_collection: ['Datastore', gettext('Garbage collect') ],
|
||||
backup: [ '', gettext('Backup') ],
|
||||
reader: [ '', gettext('Read datastore objects') ], // FIXME: better one
|
||||
sync: ['Datastore', gettext('Remote Sync') ],
|
||||
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'));
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -5,7 +5,7 @@ Ext.define('pmx-acls', {
|
||||
{
|
||||
name: 'aclid',
|
||||
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;
|
||||
}
|
||||
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',
|
||||
autoDestroy: true,
|
||||
autoDestroyRstore: true,
|
||||
sorters: 'userid',
|
||||
sorters: 'aclid',
|
||||
rstore: {
|
||||
type: 'update',
|
||||
storeid: 'pmx-acls',
|
||||
model: 'pmx-acls',
|
||||
autoStart: true,
|
||||
interval: 5000,
|
||||
},
|
||||
},
|
||||
|
129
www/config/RemoteView.js
Normal file
129
www/config/RemoteView.js
Normal 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,
|
||||
},
|
||||
],
|
||||
});
|
@ -60,6 +60,10 @@ Ext.define('PBS.config.UserView', {
|
||||
},
|
||||
|
||||
reload: function() { this.getView().getStore().rstore.load(); },
|
||||
|
||||
init: function(view) {
|
||||
Proxmox.Utils.monStoreErrors(view, view.getStore().rstore);
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
|
89
www/window/RemoteEdit.js
Normal file
89
www/window/RemoteEdit.js
Normal 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;
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user