Compare commits
92 Commits
Author | SHA1 | Date | |
---|---|---|---|
1a48cbf164 | |||
3480777d89 | |||
a71bc08ff4 | |||
df766e668f | |||
0a8f3ae0b3 | |||
da6e67b321 | |||
dec00364b3 | |||
5637087cc9 | |||
5ad4bdc482 | |||
823867f5b7 | |||
c6772c92b8 | |||
79f6a79cfc | |||
4c7f100d22 | |||
9070d11f4c | |||
124b93f31c | |||
0f22f53b36 | |||
3784dbf029 | |||
4c95d58c41 | |||
38d4675921 | |||
7b8aa893fa | |||
fb2678f96e | |||
486ed27299 | |||
df4827f2c0 | |||
ef1b436350 | |||
b19b4bfcb0 | |||
e64b9f9204 | |||
9c33683c25 | |||
ba20987ae7 | |||
729d41fe6a | |||
905147a5ee | |||
0c41e0d06b | |||
b37b59b726 | |||
60b9b48e71 | |||
abf8b5d475 | |||
7eebe1483e | |||
9a76091785 | |||
c386b06fc6 | |||
6bcfc5c1a4 | |||
768e10d0b3 | |||
e7244387c7 | |||
5ade6c25f3 | |||
784fa1c2e3 | |||
66f4e6a809 | |||
8074d2b0c3 | |||
b02d49ab26 | |||
82a0cd2ad4 | |||
ee1a9c3230 | |||
db24c01106 | |||
ae3cfa8f0d | |||
b56c111e93 | |||
bbeb0256f1 | |||
005a5b9677 | |||
55bee04856 | |||
42fd40a124 | |||
f21508b9e1 | |||
ee7a308de4 | |||
636e674ee7 | |||
b02b374b46 | |||
1c13afa8f9 | |||
69b92fab7e | |||
6ab77df3f5 | |||
264c19582b | |||
8acd4d9afc | |||
65b0cea6bd | |||
cfe01b2e6a | |||
b19b032be3 | |||
5441708634 | |||
3c9b370255 | |||
510544770b | |||
e8293841c2 | |||
46114bf28e | |||
0d7e61f06f | |||
fd6a54dfbc | |||
1ea5722b8f | |||
bc8fadf494 | |||
a76934ad33 | |||
d7a122a026 | |||
6c25588e63 | |||
17a1f579d0 | |||
998db63933 | |||
c0fa14d94a | |||
6fd129844d | |||
baae780c99 | |||
09a1da25ed | |||
298c6aaef6 | |||
a329324139 | |||
a83e2ffeab | |||
5d7449a121 | |||
ebbe4958c6 | |||
73b2cc4977 | |||
7ecfde8150 | |||
796480a38b |
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proxmox-backup"
|
||||
version = "0.8.17"
|
||||
version = "0.9.0"
|
||||
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
|
||||
edition = "2018"
|
||||
license = "AGPL-3"
|
||||
@ -38,7 +38,7 @@ pam-sys = "0.5"
|
||||
percent-encoding = "2.1"
|
||||
pin-utils = "0.1.0"
|
||||
pathpatterns = "0.1.2"
|
||||
proxmox = { version = "0.4.1", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
||||
proxmox = { version = "0.4.2", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
||||
#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", "websocket" ] }
|
||||
proxmox-fuse = "0.1.0"
|
||||
@ -61,6 +61,7 @@ walkdir = "2"
|
||||
xdg = "2.2"
|
||||
zstd = { version = "0.4", features = [ "bindgen" ] }
|
||||
nom = "5.1"
|
||||
crossbeam-channel = "0.4"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
69
debian/changelog
vendored
69
debian/changelog
vendored
@ -1,3 +1,72 @@
|
||||
rust-proxmox-backup (0.9.0-1) unstable; urgency=medium
|
||||
|
||||
* use ParallelHandler to verify chunks
|
||||
|
||||
* client: add new paper-key command to CLI tool
|
||||
|
||||
* server: split task list in active and archived
|
||||
|
||||
* tools: add logrotate module and use it for archived tasks, allowing to save
|
||||
more than 100 thousands of tasks efficiently in the archive
|
||||
|
||||
* require square [brackets] for ipv6 addresses and fix ipv6 handling for
|
||||
remotes/sync jobs
|
||||
|
||||
* ui: RemoteEdit: make comment and fingerprint deletable
|
||||
|
||||
* api/disks: create zfs: enable import systemd service unit for newly created
|
||||
ZFS pools
|
||||
|
||||
* client and remotes: add support to specify a custom port number. The server
|
||||
is still always listening on 8007, but you can now use things like reverse
|
||||
proxies or port mapping.
|
||||
|
||||
* ui: RemoteEdit: allow to specify a port in the host field
|
||||
|
||||
* client pull: log progress
|
||||
|
||||
* various fixes and improvements
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 01 Oct 2020 16:19:40 +0200
|
||||
|
||||
rust-proxmox-backup (0.8.21-1) unstable; urgency=medium
|
||||
|
||||
* depend on crossbeam-channel
|
||||
|
||||
* speedup sync jobs (allow up to 4 worker threads)
|
||||
|
||||
* improve docs
|
||||
|
||||
* use jobstate mechanism for verify/garbage_collection schedules
|
||||
|
||||
* proxy: fix error handling in prune scheduling
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Fri, 25 Sep 2020 13:20:19 +0200
|
||||
|
||||
rust-proxmox-backup (0.8.20-1) unstable; urgency=medium
|
||||
|
||||
* improve sync speed
|
||||
|
||||
* benchmark: use compressable data to get more realistic result
|
||||
|
||||
* docs: add onlineHelp to some panels
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Thu, 24 Sep 2020 13:15:45 +0200
|
||||
|
||||
rust-proxmox-backup (0.8.19-1) unstable; urgency=medium
|
||||
|
||||
* src/api2/reader.rs: use std::fs::read instead of tokio::fs::read
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 22 Sep 2020 13:30:27 +0200
|
||||
|
||||
rust-proxmox-backup (0.8.18-1) unstable; urgency=medium
|
||||
|
||||
* src/client/pull.rs: allow up to 20 concurrent download streams
|
||||
|
||||
* docs: add version and date to HTML index
|
||||
|
||||
-- Proxmox Support Team <support@proxmox.com> Tue, 22 Sep 2020 12:39:26 +0200
|
||||
|
||||
rust-proxmox-backup (0.8.17-1) unstable; urgency=medium
|
||||
|
||||
* src/client/pull.rs: open temporary manifest with truncate(true)
|
||||
|
12
debian/control
vendored
12
debian/control
vendored
@ -12,6 +12,7 @@ Build-Depends: debhelper (>= 11),
|
||||
librust-bitflags-1+default-dev (>= 1.2.1-~~),
|
||||
librust-bytes-0.5+default-dev,
|
||||
librust-crc32fast-1+default-dev,
|
||||
librust-crossbeam-channel-0.4+default-dev,
|
||||
librust-endian-trait-0.6+arrays-dev,
|
||||
librust-endian-trait-0.6+default-dev,
|
||||
librust-futures-0.3+default-dev,
|
||||
@ -33,10 +34,10 @@ Build-Depends: debhelper (>= 11),
|
||||
librust-pathpatterns-0.1+default-dev (>= 0.1.2-~~),
|
||||
librust-percent-encoding-2+default-dev (>= 2.1-~~),
|
||||
librust-pin-utils-0.1+default-dev,
|
||||
librust-proxmox-0.4+api-macro-dev (>= 0.4.1-~~),
|
||||
librust-proxmox-0.4+default-dev (>= 0.4.1-~~),
|
||||
librust-proxmox-0.4+sortable-macro-dev (>= 0.4.1-~~),
|
||||
librust-proxmox-0.4+websocket-dev (>= 0.4.1-~~),
|
||||
librust-proxmox-0.4+api-macro-dev (>= 0.4.2-~~),
|
||||
librust-proxmox-0.4+default-dev (>= 0.4.2-~~),
|
||||
librust-proxmox-0.4+sortable-macro-dev (>= 0.4.2-~~),
|
||||
librust-proxmox-0.4+websocket-dev (>= 0.4.2-~~),
|
||||
librust-proxmox-fuse-0.1+default-dev,
|
||||
librust-pxar-0.6+default-dev (>= 0.6.1-~~),
|
||||
librust-pxar-0.6+futures-io-dev (>= 0.6.1-~~),
|
||||
@ -77,6 +78,7 @@ Build-Depends: debhelper (>= 11),
|
||||
uuid-dev,
|
||||
debhelper (>= 12~),
|
||||
bash-completion,
|
||||
pve-eslint,
|
||||
python3-docutils,
|
||||
python3-pygments,
|
||||
rsync,
|
||||
@ -117,7 +119,7 @@ Description: Proxmox Backup Server daemon with tools and GUI
|
||||
|
||||
Package: proxmox-backup-client
|
||||
Architecture: any
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}
|
||||
Depends: qrencode ${misc:Depends}, ${shlibs:Depends}
|
||||
Description: Proxmox Backup Client tools
|
||||
This package contains the Proxmox Backup client, which provides a
|
||||
simple command line tool to create and restore backups.
|
||||
|
4
debian/control.in
vendored
4
debian/control.in
vendored
@ -7,7 +7,7 @@ Depends: fonts-font-awesome,
|
||||
pbs-i18n,
|
||||
proxmox-backup-docs,
|
||||
proxmox-mini-journalreader,
|
||||
proxmox-widget-toolkit (>= 2.2-4),
|
||||
proxmox-widget-toolkit (>= 2.3-1),
|
||||
pve-xtermjs (>= 4.7.0-1),
|
||||
smartmontools,
|
||||
${misc:Depends},
|
||||
@ -19,7 +19,7 @@ Description: Proxmox Backup Server daemon with tools and GUI
|
||||
|
||||
Package: proxmox-backup-client
|
||||
Architecture: any
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}
|
||||
Depends: qrencode ${misc:Depends}, ${shlibs:Depends}
|
||||
Description: Proxmox Backup Client tools
|
||||
This package contains the Proxmox Backup client, which provides a
|
||||
simple command line tool to create and restore backups.
|
||||
|
1
debian/debcargo.toml
vendored
1
debian/debcargo.toml
vendored
@ -14,6 +14,7 @@ section = "admin"
|
||||
build_depends = [
|
||||
"debhelper (>= 12~)",
|
||||
"bash-completion",
|
||||
"pve-eslint",
|
||||
"python3-docutils",
|
||||
"python3-pygments",
|
||||
"rsync",
|
||||
|
@ -74,7 +74,7 @@ onlinehelpinfo:
|
||||
@echo "Build finished. OnlineHelpInfo.js is in $(BUILDDIR)/scanrefs."
|
||||
|
||||
.PHONY: html
|
||||
html: ${GENERATED_SYNOPSIS}
|
||||
html: ${GENERATED_SYNOPSIS} images/proxmox-logo.svg custom.css conf.py
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
cp images/proxmox-logo.svg $(BUILDDIR)/html/_static/
|
||||
cp custom.css $(BUILDDIR)/html/_static/
|
||||
|
11
docs/_templates/index-sidebar.html
vendored
Normal file
11
docs/_templates/index-sidebar.html
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
<h3>Navigation</h3>
|
||||
{{ toctree(includehidden=theme_sidebar_includehidden, collapse=True, titles_only=True) }}
|
||||
{% if theme_extra_nav_links %}
|
||||
<hr />
|
||||
<h3>Links</h3>
|
||||
<ul>
|
||||
{% for text, uri in theme_extra_nav_links.items() %}
|
||||
<li class="toctree-l1"><a href="{{ uri }}">{{ text }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
7
docs/_templates/sidebar-header.html
vendored
Normal file
7
docs/_templates/sidebar-header.html
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<p class="logo">
|
||||
<a href="index.html">
|
||||
<img class="logo" src="_static/proxmox-logo.svg" alt="Logo">
|
||||
</a>
|
||||
</p>
|
||||
<h1 class="logo logo-name"><a href="index.html">Proxmox Backup</a></h1>
|
||||
<hr style="width:100%;">
|
@ -127,7 +127,7 @@ Backup Server Management
|
||||
The command line tool to configure and manage the backup server is called
|
||||
:command:`proxmox-backup-manager`.
|
||||
|
||||
|
||||
.. _datastore_intro:
|
||||
|
||||
:term:`DataStore`
|
||||
~~~~~~~~~~~~~~~~~
|
||||
@ -364,7 +364,7 @@ directories will store the chunked data after a backup operation has been execut
|
||||
276489 drwxr-xr-x 3 backup backup 4.0K Jul 8 12:35 ..
|
||||
276490 drwxr-x--- 1 backup backup 1.1M Jul 8 12:35 .
|
||||
|
||||
|
||||
.. _user_mgmt:
|
||||
|
||||
User Management
|
||||
~~~~~~~~~~~~~~~
|
||||
@ -448,6 +448,8 @@ Or completely remove the user with:
|
||||
# proxmox-backup-manager user remove john@pbs
|
||||
|
||||
|
||||
.. _user_acl:
|
||||
|
||||
Access Control
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@ -631,6 +633,8 @@ You can also configure DNS settings, from the **DNS** section
|
||||
of **Configuration** or by using the ``dns`` subcommand of
|
||||
``proxmox-backup-manager``.
|
||||
|
||||
.. _backup_remote:
|
||||
|
||||
:term:`Remote`
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@ -728,15 +732,33 @@ Repository Locations
|
||||
The client uses the following notation to specify a datastore repository
|
||||
on the backup server.
|
||||
|
||||
[[username@]server:]datastore
|
||||
[[username@]server[:port]:]datastore
|
||||
|
||||
The default value for ``username`` ist ``root``. If no server is specified,
|
||||
The default value for ``username`` ist ``root@pam``. If no server is specified,
|
||||
the default is the local host (``localhost``).
|
||||
|
||||
You can specify a port if your backup server is only reachable on a different
|
||||
port (e.g. with NAT and port forwarding).
|
||||
|
||||
Note that if the server is an IPv6 address, you have to write it with
|
||||
square brackets (e.g. [fe80::01]).
|
||||
|
||||
You can pass the repository with the ``--repository`` command
|
||||
line option, or by setting the ``PBS_REPOSITORY`` environment
|
||||
variable.
|
||||
|
||||
Here some examples of valid repositories and the real values
|
||||
|
||||
================================ ============ ================== ===========
|
||||
Example User Host:Port Datastore
|
||||
================================ ============ ================== ===========
|
||||
mydatastore ``root@pam`` localhost:8007 mydatastore
|
||||
myhostname:mydatastore ``root@pam`` myhostname:8007 mydatastore
|
||||
user@pbs@myhostname:mydatastore ``user@pbs`` myhostname:8007 mydatastore
|
||||
192.168.55.55:1234:mydatastore ``root@pam`` 192.168.55.55:1234 mydatastore
|
||||
[ff80::51]:mydatastore ``root@pam`` [ff80::51]:8007 mydatastore
|
||||
[ff80::51]:1234:mydatastore ``root@pam`` [ff80::51]:1234 mydatastore
|
||||
================================ ============ ================== ===========
|
||||
|
||||
Environment Variables
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
45
docs/conf.py
45
docs/conf.py
@ -97,12 +97,10 @@ language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#
|
||||
# today = ''
|
||||
#
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#
|
||||
# today_fmt = '%B %d, %Y'
|
||||
today_fmt = '%A, %d %B %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
@ -164,18 +162,19 @@ html_theme = 'alabaster'
|
||||
#
|
||||
html_theme_options = {
|
||||
'fixed_sidebar': True,
|
||||
#'sidebar_includehidden': False,
|
||||
'sidebar_collapse': False, # FIXME: documented, but does not works?!
|
||||
'show_relbar_bottom': True, # FIXME: documented, but does not works?!
|
||||
'sidebar_includehidden': False,
|
||||
'sidebar_collapse': False,
|
||||
'globaltoc_collapse': False,
|
||||
'show_relbar_bottom': True,
|
||||
'show_powered_by': False,
|
||||
|
||||
'logo': 'proxmox-logo.svg',
|
||||
'logo_name': True, # show project name below logo
|
||||
#'logo_text_align': 'center',
|
||||
#'description': 'Fast, Secure & Efficient.',
|
||||
'extra_nav_links': {
|
||||
'Proxmox Homepage': 'https://proxmox.com',
|
||||
'PDF': 'proxmox-backup.pdf',
|
||||
},
|
||||
|
||||
'sidebar_width': '300px',
|
||||
'page_width': '1280px',
|
||||
'sidebar_width': '320px',
|
||||
'page_width': '1320px',
|
||||
# font styles
|
||||
'head_font_family': 'Lato, sans-serif',
|
||||
'caption_font_family': 'Lato, sans-serif',
|
||||
@ -183,6 +182,24 @@ html_theme_options = {
|
||||
'font_family': 'Open Sans, sans-serif',
|
||||
}
|
||||
|
||||
# Alabaster theme recommends setting this fixed.
|
||||
# If you switch theme this needs to removed, probably.
|
||||
html_sidebars = {
|
||||
'**': [
|
||||
'sidebar-header.html',
|
||||
'searchbox.html',
|
||||
'navigation.html',
|
||||
'relations.html',
|
||||
],
|
||||
|
||||
'index': [
|
||||
'sidebar-header.html',
|
||||
'searchbox.html',
|
||||
'index-sidebar.html',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
@ -228,10 +245,6 @@ html_static_path = ['_static']
|
||||
#
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#
|
||||
|
@ -13,3 +13,40 @@ div.body img {
|
||||
pre {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
li a.current {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #000;
|
||||
}
|
||||
ul li.toctree-l1 {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
ul li.toctree-l1 > a {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
div.sphinxsidebar form.search {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h1.logo-name {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (max-width: 875px) {
|
||||
div.sphinxsidebar p.logo {
|
||||
display: initial;
|
||||
}
|
||||
div.sphinxsidebar h1.logo-name {
|
||||
display: block;
|
||||
}
|
||||
div.sphinxsidebar span {
|
||||
color: #AAA;
|
||||
}
|
||||
ul li.toctree-l1 > a {
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
Welcome to the Proxmox Backup documentation!
|
||||
============================================
|
||||
|
||||
Copyright (C) 2019-2020 Proxmox Server Solutions GmbH
|
||||
| Copyright (C) 2019-2020 Proxmox Server Solutions GmbH
|
||||
| Version |version| -- |today|
|
||||
|
||||
Permission is granted to copy, distribute and/or modify this document under the
|
||||
terms of the GNU Free Documentation License, Version 1.3 or any later version
|
||||
@ -45,9 +45,10 @@ in the section entitled "GNU Free Documentation License".
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:hidden:
|
||||
:caption: Developer Appendix
|
||||
|
||||
todos.rst
|
||||
|
||||
|
||||
* :ref:`genindex`
|
||||
.. # * :ref:`genindex`
|
||||
|
@ -32,7 +32,7 @@ async fn run() -> Result<(), Error> {
|
||||
.interactive(true)
|
||||
.ticket_cache(true);
|
||||
|
||||
let client = HttpClient::new(host, username, options)?;
|
||||
let client = HttpClient::new(host, 8007, username, options)?;
|
||||
|
||||
let backup_time = proxmox::tools::time::parse_rfc3339("2019-06-28T10:49:48Z")?;
|
||||
|
||||
|
@ -14,7 +14,7 @@ async fn upload_speed() -> Result<f64, Error> {
|
||||
.interactive(true)
|
||||
.ticket_cache(true);
|
||||
|
||||
let client = HttpClient::new(host, username, options)?;
|
||||
let client = HttpClient::new(host, 8007, username, options)?;
|
||||
|
||||
let backup_time = proxmox::tools::time::epoch_i64();
|
||||
|
||||
|
@ -175,7 +175,7 @@ pub fn update_acl(
|
||||
_rpcenv: &mut dyn RpcEnvironment,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(acl::ACL_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(acl::ACL_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut tree, expected_digest) = acl::config()?;
|
||||
|
||||
|
@ -100,7 +100,7 @@ pub fn list_users(
|
||||
/// Create new user.
|
||||
pub fn create_user(password: Option<String>, param: Value) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let user: user::User = serde_json::from_value(param)?;
|
||||
|
||||
@ -211,7 +211,7 @@ pub fn update_user(
|
||||
digest: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut config, expected_digest) = user::config()?;
|
||||
|
||||
@ -285,7 +285,7 @@ pub fn update_user(
|
||||
/// Remove a user from the configuration file.
|
||||
pub fn delete_user(userid: Userid, digest: Option<String>) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut config, expected_digest) = user::config()?;
|
||||
|
||||
|
@ -200,7 +200,7 @@ async move {
|
||||
};
|
||||
if benchmark {
|
||||
env.log("benchmark finished successfully");
|
||||
env.remove_backup()?;
|
||||
tools::runtime::block_in_place(|| env.remove_backup())?;
|
||||
return Ok(());
|
||||
}
|
||||
match (res, env.ensure_finished()) {
|
||||
@ -222,7 +222,7 @@ async move {
|
||||
(Err(err), Err(_)) => {
|
||||
env.log(format!("backup failed: {}", err));
|
||||
env.log("removing failed backup");
|
||||
env.remove_backup()?;
|
||||
tools::runtime::block_in_place(|| env.remove_backup())?;
|
||||
Err(err)
|
||||
},
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ use proxmox::tools::digest_to_hex;
|
||||
use proxmox::tools::fs::{replace_file, CreateOptions};
|
||||
use proxmox::api::{RpcEnvironment, RpcEnvironmentType};
|
||||
|
||||
use crate::api2::types::{Userid, SnapshotVerifyState, VerifyState};
|
||||
use crate::api2::types::Userid;
|
||||
use crate::backup::*;
|
||||
use crate::server::WorkerTask;
|
||||
use crate::server::formatter::*;
|
||||
@ -66,8 +66,8 @@ struct FixedWriterState {
|
||||
incremental: bool,
|
||||
}
|
||||
|
||||
// key=digest, value=(length, existance checked)
|
||||
type KnownChunksMap = HashMap<[u8;32], (u32, bool)>;
|
||||
// key=digest, value=length
|
||||
type KnownChunksMap = HashMap<[u8;32], u32>;
|
||||
|
||||
struct SharedBackupState {
|
||||
finished: bool,
|
||||
@ -156,7 +156,7 @@ impl BackupEnvironment {
|
||||
|
||||
state.ensure_unfinished()?;
|
||||
|
||||
state.known_chunks.insert(digest, (length, false));
|
||||
state.known_chunks.insert(digest, length);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -198,7 +198,7 @@ impl BackupEnvironment {
|
||||
if is_duplicate { data.upload_stat.duplicates += 1; }
|
||||
|
||||
// register chunk
|
||||
state.known_chunks.insert(digest, (size, true));
|
||||
state.known_chunks.insert(digest, size);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -231,7 +231,7 @@ impl BackupEnvironment {
|
||||
if is_duplicate { data.upload_stat.duplicates += 1; }
|
||||
|
||||
// register chunk
|
||||
state.known_chunks.insert(digest, (size, true));
|
||||
state.known_chunks.insert(digest, size);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -240,7 +240,7 @@ impl BackupEnvironment {
|
||||
let state = self.state.lock().unwrap();
|
||||
|
||||
match state.known_chunks.get(digest) {
|
||||
Some((len, _)) => Some(*len),
|
||||
Some(len) => Some(*len),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
@ -457,47 +457,6 @@ impl BackupEnvironment {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure all chunks referenced in this backup actually exist.
|
||||
/// Only call *after* all writers have been closed, to avoid race with GC.
|
||||
/// In case of error, mark the previous backup as 'verify failed'.
|
||||
fn verify_chunk_existance(&self, known_chunks: &KnownChunksMap) -> Result<(), Error> {
|
||||
for (digest, (_, checked)) in known_chunks.iter() {
|
||||
if !checked && !self.datastore.chunk_path(digest).0.exists() {
|
||||
let mark_msg = if let Some(ref last_backup) = self.last_backup {
|
||||
let last_dir = &last_backup.backup_dir;
|
||||
let verify_state = SnapshotVerifyState {
|
||||
state: VerifyState::Failed,
|
||||
upid: self.worker.upid().clone(),
|
||||
};
|
||||
|
||||
let res = proxmox::try_block!{
|
||||
let (mut manifest, _) = self.datastore.load_manifest(last_dir)?;
|
||||
manifest.unprotected["verify_state"] = serde_json::to_value(verify_state)?;
|
||||
self.datastore.store_manifest(last_dir, serde_json::to_value(manifest)?)
|
||||
};
|
||||
|
||||
if let Err(err) = res {
|
||||
format!("tried marking previous snapshot as bad, \
|
||||
but got error accessing manifest: {}", err)
|
||||
} else {
|
||||
"marked previous snapshot as bad, please use \
|
||||
'verify' for a detailed check".to_owned()
|
||||
}
|
||||
} else {
|
||||
"internal error: no base backup registered to mark invalid".to_owned()
|
||||
};
|
||||
|
||||
bail!(
|
||||
"chunk '{}' was attempted to be reused but doesn't exist - {}",
|
||||
digest_to_hex(digest),
|
||||
mark_msg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Mark backup as finished
|
||||
pub fn finish_backup(&self) -> Result<(), Error> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
@ -534,8 +493,6 @@ impl BackupEnvironment {
|
||||
}
|
||||
}
|
||||
|
||||
self.verify_chunk_existance(&state.known_chunks)?;
|
||||
|
||||
// marks the backup as successful
|
||||
state.finished = true;
|
||||
|
||||
|
@ -61,12 +61,15 @@ impl Future for UploadChunk {
|
||||
let (is_duplicate, compressed_size) = match proxmox::try_block! {
|
||||
let mut chunk = DataBlob::from_raw(raw_data)?;
|
||||
|
||||
chunk.verify_unencrypted(this.size as usize, &this.digest)?;
|
||||
tools::runtime::block_in_place(|| {
|
||||
chunk.verify_unencrypted(this.size as usize, &this.digest)?;
|
||||
|
||||
// always comput CRC at server side
|
||||
chunk.set_crc(chunk.compute_crc());
|
||||
// always comput CRC at server side
|
||||
chunk.set_crc(chunk.compute_crc());
|
||||
|
||||
this.store.insert_chunk(&chunk, &this.digest)
|
||||
})
|
||||
|
||||
this.store.insert_chunk(&chunk, &this.digest)
|
||||
} {
|
||||
Ok(res) => res,
|
||||
Err(err) => break err,
|
||||
|
@ -112,7 +112,7 @@ pub fn list_datastores(
|
||||
/// Create new datastore config.
|
||||
pub fn create_datastore(param: Value) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let datastore: datastore::DataStoreConfig = serde_json::from_value(param.clone())?;
|
||||
|
||||
@ -132,6 +132,8 @@ pub fn create_datastore(param: Value) -> Result<(), Error> {
|
||||
datastore::save_config(&config)?;
|
||||
|
||||
crate::config::jobstate::create_state_file("prune", &datastore.name)?;
|
||||
crate::config::jobstate::create_state_file("garbage_collection", &datastore.name)?;
|
||||
crate::config::jobstate::create_state_file("verify", &datastore.name)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -275,7 +277,7 @@ pub fn update_datastore(
|
||||
digest: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
// pass/compare digest
|
||||
let (mut config, expected_digest) = datastore::config()?;
|
||||
@ -313,13 +315,23 @@ pub fn update_datastore(
|
||||
}
|
||||
}
|
||||
|
||||
if gc_schedule.is_some() { data.gc_schedule = gc_schedule; }
|
||||
let mut gc_schedule_changed = false;
|
||||
if gc_schedule.is_some() {
|
||||
gc_schedule_changed = data.gc_schedule != gc_schedule;
|
||||
data.gc_schedule = gc_schedule;
|
||||
}
|
||||
|
||||
let mut prune_schedule_changed = false;
|
||||
if prune_schedule.is_some() {
|
||||
prune_schedule_changed = true;
|
||||
prune_schedule_changed = data.prune_schedule != prune_schedule;
|
||||
data.prune_schedule = prune_schedule;
|
||||
}
|
||||
if verify_schedule.is_some() { data.verify_schedule = verify_schedule; }
|
||||
|
||||
let mut verify_schedule_changed = false;
|
||||
if verify_schedule.is_some() {
|
||||
verify_schedule_changed = data.verify_schedule != verify_schedule;
|
||||
data.verify_schedule = verify_schedule;
|
||||
}
|
||||
|
||||
if keep_last.is_some() { data.keep_last = keep_last; }
|
||||
if keep_hourly.is_some() { data.keep_hourly = keep_hourly; }
|
||||
@ -332,12 +344,20 @@ pub fn update_datastore(
|
||||
|
||||
datastore::save_config(&config)?;
|
||||
|
||||
// we want to reset the statefile, to avoid an immediate sync in some cases
|
||||
// we want to reset the statefiles, to avoid an immediate action in some cases
|
||||
// (e.g. going from monthly to weekly in the second week of the month)
|
||||
if gc_schedule_changed {
|
||||
crate::config::jobstate::create_state_file("garbage_collection", &name)?;
|
||||
}
|
||||
|
||||
if prune_schedule_changed {
|
||||
crate::config::jobstate::create_state_file("prune", &name)?;
|
||||
}
|
||||
|
||||
if verify_schedule_changed {
|
||||
crate::config::jobstate::create_state_file("verify", &name)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -361,7 +381,7 @@ pub fn update_datastore(
|
||||
/// Remove a datastore configuration.
|
||||
pub fn delete_datastore(name: String, digest: Option<String>) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut config, expected_digest) = datastore::config()?;
|
||||
|
||||
@ -377,7 +397,10 @@ pub fn delete_datastore(name: String, digest: Option<String>) -> Result<(), Erro
|
||||
|
||||
datastore::save_config(&config)?;
|
||||
|
||||
crate::config::jobstate::remove_state_file("prune", &name)?;
|
||||
// ignore errors
|
||||
let _ = crate::config::jobstate::remove_state_file("prune", &name);
|
||||
let _ = crate::config::jobstate::remove_state_file("garbage_collection", &name);
|
||||
let _ = crate::config::jobstate::remove_state_file("verify", &name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -60,6 +60,12 @@ pub fn list_remotes(
|
||||
host: {
|
||||
schema: DNS_NAME_OR_IP_SCHEMA,
|
||||
},
|
||||
port: {
|
||||
description: "The (optional) port.",
|
||||
type: u16,
|
||||
optional: true,
|
||||
default: 8007,
|
||||
},
|
||||
userid: {
|
||||
type: Userid,
|
||||
},
|
||||
@ -79,7 +85,7 @@ pub fn list_remotes(
|
||||
/// Create new remote.
|
||||
pub fn create_remote(password: String, param: Value) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let mut data = param.clone();
|
||||
data["password"] = Value::from(base64::encode(password.as_bytes()));
|
||||
@ -136,6 +142,8 @@ pub enum DeletableProperty {
|
||||
comment,
|
||||
/// Delete the fingerprint property.
|
||||
fingerprint,
|
||||
/// Delete the port property.
|
||||
port,
|
||||
}
|
||||
|
||||
#[api(
|
||||
@ -153,6 +161,11 @@ pub enum DeletableProperty {
|
||||
optional: true,
|
||||
schema: DNS_NAME_OR_IP_SCHEMA,
|
||||
},
|
||||
port: {
|
||||
description: "The (optional) port.",
|
||||
type: u16,
|
||||
optional: true,
|
||||
},
|
||||
userid: {
|
||||
optional: true,
|
||||
type: Userid,
|
||||
@ -188,6 +201,7 @@ pub fn update_remote(
|
||||
name: String,
|
||||
comment: Option<String>,
|
||||
host: Option<String>,
|
||||
port: Option<u16>,
|
||||
userid: Option<Userid>,
|
||||
password: Option<String>,
|
||||
fingerprint: Option<String>,
|
||||
@ -195,7 +209,7 @@ pub fn update_remote(
|
||||
digest: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut config, expected_digest) = remote::config()?;
|
||||
|
||||
@ -211,6 +225,7 @@ pub fn update_remote(
|
||||
match delete_prop {
|
||||
DeletableProperty::comment => { data.comment = None; },
|
||||
DeletableProperty::fingerprint => { data.fingerprint = None; },
|
||||
DeletableProperty::port => { data.port = None; },
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -224,6 +239,7 @@ pub fn update_remote(
|
||||
}
|
||||
}
|
||||
if let Some(host) = host { data.host = host; }
|
||||
if port.is_some() { data.port = port; }
|
||||
if let Some(userid) = userid { data.userid = userid; }
|
||||
if let Some(password) = password { data.password = password; }
|
||||
|
||||
@ -256,7 +272,7 @@ pub fn update_remote(
|
||||
/// Remove a remote from the configuration file.
|
||||
pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut config, expected_digest) = remote::config()?;
|
||||
|
||||
|
@ -69,7 +69,7 @@ pub fn list_sync_jobs(
|
||||
/// Create a new sync job.
|
||||
pub fn create_sync_job(param: Value) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let sync_job: sync::SyncJobConfig = serde_json::from_value(param.clone())?;
|
||||
|
||||
@ -187,7 +187,7 @@ pub fn update_sync_job(
|
||||
digest: Option<String>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
// pass/compare digest
|
||||
let (mut config, expected_digest) = sync::config()?;
|
||||
@ -250,7 +250,7 @@ pub fn update_sync_job(
|
||||
/// Remove a sync job configuration
|
||||
pub fn delete_sync_job(id: String, digest: Option<String>) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut config, expected_digest) = sync::config()?;
|
||||
|
||||
|
@ -25,6 +25,8 @@ use crate::server::WorkerTask;
|
||||
|
||||
use crate::api2::types::*;
|
||||
|
||||
use crate::tools::systemd;
|
||||
|
||||
pub const DISK_ARRAY_SCHEMA: Schema = ArraySchema::new(
|
||||
"Disk name list.", &BLOCKDEVICE_NAME_SCHEMA)
|
||||
.schema();
|
||||
@ -355,6 +357,11 @@ pub fn create_zpool(
|
||||
let output = crate::tools::run_command(command, None)?;
|
||||
worker.log(output);
|
||||
|
||||
if std::path::Path::new("/lib/systemd/system/zfs-import@.service").exists() {
|
||||
let import_unit = format!("zfs-import@{}.service", systemd::escape_unit(&name, false));
|
||||
systemd::enable_unit(&import_unit)?;
|
||||
}
|
||||
|
||||
if let Some(compression) = compression {
|
||||
let mut command = std::process::Command::new("zfs");
|
||||
command.args(&["set", &format!("compression={}", compression), &name]);
|
||||
|
@ -241,7 +241,7 @@ pub fn create_interface(
|
||||
let interface_type = crate::tools::required_string_param(¶m, "type")?;
|
||||
let interface_type: NetworkInterfaceType = serde_json::from_value(interface_type.into())?;
|
||||
|
||||
let _lock = open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut config, _digest) = network::config()?;
|
||||
|
||||
@ -505,7 +505,7 @@ pub fn update_interface(
|
||||
param: Value,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut config, expected_digest) = network::config()?;
|
||||
|
||||
@ -646,7 +646,7 @@ pub fn update_interface(
|
||||
/// Remove network interface configuration.
|
||||
pub fn delete_interface(iface: String, digest: Option<String>) -> Result<(), Error> {
|
||||
|
||||
let _lock = open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0))?;
|
||||
let _lock = open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0), true)?;
|
||||
|
||||
let (mut config, expected_digest) = network::config()?;
|
||||
|
||||
|
@ -10,7 +10,7 @@ use proxmox::{identity, list_subdirs_api_method, sortable};
|
||||
|
||||
use crate::tools;
|
||||
use crate::api2::types::*;
|
||||
use crate::server::{self, UPID, TaskState};
|
||||
use crate::server::{self, UPID, TaskState, TaskListInfoIterator};
|
||||
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
|
||||
use crate::config::cached_user_info::CachedUserInfo;
|
||||
|
||||
@ -303,6 +303,7 @@ pub fn list_tasks(
|
||||
limit: u64,
|
||||
errors: bool,
|
||||
running: bool,
|
||||
userfilter: Option<String>,
|
||||
param: Value,
|
||||
mut rpcenv: &mut dyn RpcEnvironment,
|
||||
) -> Result<Vec<TaskListItem>, Error> {
|
||||
@ -315,57 +316,55 @@ pub fn list_tasks(
|
||||
|
||||
let store = param["store"].as_str();
|
||||
|
||||
let userfilter = param["userfilter"].as_str();
|
||||
let list = TaskListInfoIterator::new(running)?;
|
||||
|
||||
let list = server::read_task_list()?;
|
||||
let result: Vec<TaskListItem> = list
|
||||
.take_while(|info| !info.is_err())
|
||||
.filter_map(|info| {
|
||||
let info = match info {
|
||||
Ok(info) => info,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
let mut result = vec![];
|
||||
if !list_all && info.upid.userid != userid { return None; }
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
for info in list {
|
||||
if !list_all && info.upid.userid != userid { continue; }
|
||||
|
||||
|
||||
if let Some(userid) = userfilter {
|
||||
if !info.upid.userid.as_str().contains(userid) { continue; }
|
||||
if let Some(userid) = &userfilter {
|
||||
if !info.upid.userid.as_str().contains(userid) { return None; }
|
||||
}
|
||||
|
||||
if let Some(store) = store {
|
||||
// Note: useful to select all tasks spawned by proxmox-backup-client
|
||||
let worker_id = match &info.upid.worker_id {
|
||||
Some(w) => w,
|
||||
None => continue, // skip
|
||||
None => return None, // skip
|
||||
};
|
||||
|
||||
if info.upid.worker_type == "backup" || info.upid.worker_type == "restore" ||
|
||||
info.upid.worker_type == "prune"
|
||||
{
|
||||
let prefix = format!("{}_", store);
|
||||
if !worker_id.starts_with(&prefix) { continue; }
|
||||
if !worker_id.starts_with(&prefix) { return None; }
|
||||
} else if info.upid.worker_type == "garbage_collection" {
|
||||
if worker_id != store { continue; }
|
||||
if worker_id != store { return None; }
|
||||
} else {
|
||||
continue; // skip
|
||||
return None; // skip
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref state) = info.state {
|
||||
if running { continue; }
|
||||
match state {
|
||||
crate::server::TaskState::OK { .. } if errors => continue,
|
||||
_ => {},
|
||||
}
|
||||
match info.state {
|
||||
Some(_) if running => return None,
|
||||
Some(crate::server::TaskState::OK { .. }) if errors => return None,
|
||||
_ => {},
|
||||
}
|
||||
|
||||
if (count as u64) < start {
|
||||
count += 1;
|
||||
continue;
|
||||
} else {
|
||||
count += 1;
|
||||
}
|
||||
Some(info.into())
|
||||
}).skip(start as usize)
|
||||
.take(limit as usize)
|
||||
.collect();
|
||||
|
||||
if (result.len() as u64) < limit { result.push(info.into()); };
|
||||
let mut count = result.len() + start as usize;
|
||||
if result.len() > 0 && result.len() >= limit as usize { // we have a 'virtual' entry as long as we have any new
|
||||
count += 1;
|
||||
}
|
||||
|
||||
rpcenv["total"] = Value::from(count);
|
||||
|
@ -55,12 +55,13 @@ pub async fn get_pull_parameters(
|
||||
.password(Some(remote.password.clone()))
|
||||
.fingerprint(remote.fingerprint.clone());
|
||||
|
||||
let client = HttpClient::new(&remote.host, &remote.userid, options)?;
|
||||
let src_repo = BackupRepository::new(Some(remote.userid.clone()), Some(remote.host.clone()), remote.port, remote_store.to_string());
|
||||
|
||||
let client = HttpClient::new(&src_repo.host(), src_repo.port(), &src_repo.user(), options)?;
|
||||
let _auth_info = client.login() // make sure we can auth
|
||||
.await
|
||||
.map_err(|err| format_err!("remote connection to '{}' failed - {}", remote.host, err))?;
|
||||
|
||||
let src_repo = BackupRepository::new(Some(remote.userid), Some(remote.host), remote_store.to_string());
|
||||
|
||||
Ok((client, src_repo, tgt_store))
|
||||
}
|
||||
|
@ -229,8 +229,7 @@ fn download_chunk(
|
||||
|
||||
env.debug(format!("download chunk {:?}", path));
|
||||
|
||||
let data = tokio::fs::read(path)
|
||||
.await
|
||||
let data = tools::runtime::block_in_place(|| std::fs::read(path))
|
||||
.map_err(move |err| http_err!(BAD_REQUEST, "reading file {:?} failed: {}", path2, err))?;
|
||||
|
||||
let body = Body::from(data);
|
||||
@ -287,7 +286,7 @@ fn download_chunk_old(
|
||||
|
||||
pub const API_METHOD_SPEEDTEST: ApiMethod = ApiMethod::new(
|
||||
&ApiHandler::AsyncHttp(&speedtest),
|
||||
&ObjectSchema::new("Test 4M block download speed.", &[])
|
||||
&ObjectSchema::new("Test 1M block download speed.", &[])
|
||||
);
|
||||
|
||||
fn speedtest(
|
||||
|
@ -182,7 +182,7 @@ fn datastore_status(
|
||||
input: {
|
||||
properties: {
|
||||
since: {
|
||||
type: u64,
|
||||
type: i64,
|
||||
description: "Only list tasks since this UNIX epoch.",
|
||||
optional: true,
|
||||
},
|
||||
@ -200,6 +200,7 @@ fn datastore_status(
|
||||
)]
|
||||
/// List tasks.
|
||||
pub fn list_tasks(
|
||||
since: Option<i64>,
|
||||
_param: Value,
|
||||
rpcenv: &mut dyn RpcEnvironment,
|
||||
) -> Result<Vec<TaskListItem>, Error> {
|
||||
@ -209,13 +210,28 @@ pub fn list_tasks(
|
||||
let user_privs = user_info.lookup_privs(&userid, &["system", "tasks"]);
|
||||
|
||||
let list_all = (user_privs & PRIV_SYS_AUDIT) != 0;
|
||||
let since = since.unwrap_or_else(|| 0);
|
||||
|
||||
// TODO: replace with call that gets all task since 'since' epoch
|
||||
let list: Vec<TaskListItem> = server::read_task_list()?
|
||||
.into_iter()
|
||||
.map(TaskListItem::from)
|
||||
.filter(|entry| list_all || entry.user == userid)
|
||||
.collect();
|
||||
let list: Vec<TaskListItem> = server::TaskListInfoIterator::new(false)?
|
||||
.take_while(|info| {
|
||||
match info {
|
||||
Ok(info) => info.upid.starttime > since,
|
||||
Err(_) => false
|
||||
}
|
||||
})
|
||||
.filter_map(|info| {
|
||||
match info {
|
||||
Ok(info) => {
|
||||
if list_all || info.upid.userid == userid {
|
||||
Some(Ok(TaskListItem::from(info)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(err) => Some(Err(err))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<TaskListItem>, Error>>()?;
|
||||
|
||||
Ok(list.into())
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use proxmox::api::{api, schema::*};
|
||||
use proxmox::const_regex;
|
||||
use proxmox::{IPRE, IPV4RE, IPV6RE, IPV4OCTET, IPV6H16, IPV6LS32};
|
||||
use proxmox::{IPRE, IPRE_BRACKET, IPV4RE, IPV6RE, IPV4OCTET, IPV6H16, IPV6LS32};
|
||||
|
||||
use crate::backup::CryptMode;
|
||||
use crate::server::UPID;
|
||||
@ -30,7 +30,7 @@ pub const FILENAME_FORMAT: ApiStringFormat = ApiStringFormat::VerifyFn(|name| {
|
||||
});
|
||||
|
||||
macro_rules! DNS_LABEL { () => (r"(?:[a-zA-Z0-9](?:[a-zA-Z0-9\-]*[a-zA-Z0-9])?)") }
|
||||
macro_rules! DNS_NAME { () => (concat!(r"(?:", DNS_LABEL!() , r"\.)*", DNS_LABEL!())) }
|
||||
macro_rules! DNS_NAME { () => (concat!(r"(?:(?:", DNS_LABEL!() , r"\.)*", DNS_LABEL!(), ")")) }
|
||||
|
||||
macro_rules! CIDR_V4_REGEX_STR { () => (concat!(r"(?:", IPV4RE!(), r"/\d{1,2})$")) }
|
||||
macro_rules! CIDR_V6_REGEX_STR { () => (concat!(r"(?:", IPV6RE!(), r"/\d{1,3})$")) }
|
||||
@ -63,9 +63,9 @@ const_regex!{
|
||||
|
||||
pub DNS_NAME_REGEX = concat!(r"^", DNS_NAME!(), r"$");
|
||||
|
||||
pub DNS_NAME_OR_IP_REGEX = concat!(r"^", DNS_NAME!(), "|", IPRE!(), r"$");
|
||||
pub DNS_NAME_OR_IP_REGEX = concat!(r"^(?:", DNS_NAME!(), "|", IPRE!(), r")$");
|
||||
|
||||
pub BACKUP_REPO_URL_REGEX = concat!(r"^^(?:(?:(", USER_ID_REGEX_STR!(), ")@)?(", DNS_NAME!(), "|", IPRE!() ,"):)?(", PROXMOX_SAFE_ID_REGEX_STR!(), r")$");
|
||||
pub BACKUP_REPO_URL_REGEX = concat!(r"^^(?:(?:(", USER_ID_REGEX_STR!(), ")@)?(", DNS_NAME!(), "|", IPRE_BRACKET!() ,"):)?(?:([0-9]{1,5}):)?(", PROXMOX_SAFE_ID_REGEX_STR!(), r")$");
|
||||
|
||||
pub CERT_FINGERPRINT_SHA256_REGEX = r"^(?:[0-9a-fA-F][0-9a-fA-F])(?::[0-9a-fA-F][0-9a-fA-F]){31}$";
|
||||
|
||||
|
@ -198,7 +198,10 @@ impl DataBlob {
|
||||
Ok(data)
|
||||
} else if magic == &COMPRESSED_BLOB_MAGIC_1_0 {
|
||||
let data_start = std::mem::size_of::<DataBlobHeader>();
|
||||
let data = zstd::block::decompress(&self.raw_data[data_start..], MAX_BLOB_SIZE)?;
|
||||
let mut reader = &self.raw_data[data_start..];
|
||||
let data = zstd::stream::decode_all(&mut reader)?;
|
||||
// zstd::block::decompress is abou 10% slower
|
||||
// let data = zstd::block::decompress(&self.raw_data[data_start..], MAX_BLOB_SIZE)?;
|
||||
if let Some(digest) = digest {
|
||||
Self::verify_digest(&data, None, digest)?;
|
||||
}
|
||||
@ -268,6 +271,12 @@ impl DataBlob {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if chunk is encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
let magic = self.magic();
|
||||
magic == &ENCR_COMPR_BLOB_MAGIC_1_0 || magic == &ENCRYPTED_BLOB_MAGIC_1_0
|
||||
}
|
||||
|
||||
/// Verify digest and data length for unencrypted chunks.
|
||||
///
|
||||
/// To do that, we need to decompress data first. Please note that
|
||||
|
@ -478,9 +478,12 @@ impl DataStore {
|
||||
|
||||
if let Ok(ref mut _mutex) = self.gc_mutex.try_lock() {
|
||||
|
||||
// avoids that we run GC if an old daemon process has still a
|
||||
// running backup writer, which is not save as we have no "oldest
|
||||
// writer" information and thus no safe atime cutoff
|
||||
let _exclusive_lock = self.chunk_store.try_exclusive_lock()?;
|
||||
|
||||
let phase1_start_time = unsafe { libc::time(std::ptr::null_mut()) };
|
||||
let phase1_start_time = proxmox::tools::time::epoch_i64();
|
||||
let oldest_writer = self.chunk_store.oldest_writer().unwrap_or(phase1_start_time);
|
||||
|
||||
let mut gc_status = GarbageCollectionStatus::default();
|
||||
|
@ -5,13 +5,22 @@ use std::time::Instant;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
|
||||
use crate::server::WorkerTask;
|
||||
use crate::api2::types::*;
|
||||
|
||||
use super::{
|
||||
DataStore, DataBlob, BackupGroup, BackupDir, BackupInfo, IndexFile,
|
||||
CryptMode,
|
||||
FileInfo, ArchiveType, archive_type,
|
||||
use crate::{
|
||||
server::WorkerTask,
|
||||
api2::types::*,
|
||||
tools::ParallelHandler,
|
||||
backup::{
|
||||
DataStore,
|
||||
DataBlob,
|
||||
BackupGroup,
|
||||
BackupDir,
|
||||
BackupInfo,
|
||||
IndexFile,
|
||||
CryptMode,
|
||||
FileInfo,
|
||||
ArchiveType,
|
||||
archive_type,
|
||||
},
|
||||
};
|
||||
|
||||
fn verify_blob(datastore: Arc<DataStore>, backup_dir: &BackupDir, info: &FileInfo) -> Result<(), Error> {
|
||||
@ -66,55 +75,6 @@ fn rename_corrupted_chunk(
|
||||
};
|
||||
}
|
||||
|
||||
// We use a separate thread to read/load chunks, so that we can do
|
||||
// load and verify in parallel to increase performance.
|
||||
fn chunk_reader_thread(
|
||||
datastore: Arc<DataStore>,
|
||||
index: Box<dyn IndexFile + Send>,
|
||||
verified_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
corrupt_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
errors: Arc<AtomicUsize>,
|
||||
worker: Arc<WorkerTask>,
|
||||
) -> std::sync::mpsc::Receiver<(DataBlob, [u8;32], u64)> {
|
||||
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel(3); // buffer up to 3 chunks
|
||||
|
||||
std::thread::spawn(move|| {
|
||||
for pos in 0..index.index_count() {
|
||||
let info = index.chunk_info(pos).unwrap();
|
||||
let size = info.range.end - info.range.start;
|
||||
|
||||
if verified_chunks.lock().unwrap().contains(&info.digest) {
|
||||
continue; // already verified
|
||||
}
|
||||
|
||||
if corrupt_chunks.lock().unwrap().contains(&info.digest) {
|
||||
let digest_str = proxmox::tools::digest_to_hex(&info.digest);
|
||||
worker.log(format!("chunk {} was marked as corrupt", digest_str));
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
continue;
|
||||
}
|
||||
|
||||
match datastore.load_chunk(&info.digest) {
|
||||
Err(err) => {
|
||||
corrupt_chunks.lock().unwrap().insert(info.digest);
|
||||
worker.log(format!("can't verify chunk, load failed - {}", err));
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
rename_corrupted_chunk(datastore.clone(), &info.digest, worker.clone());
|
||||
continue;
|
||||
}
|
||||
Ok(chunk) => {
|
||||
if sender.send((chunk, info.digest, size)).is_err() {
|
||||
break; // receiver gone - simply stop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
receiver
|
||||
}
|
||||
|
||||
fn verify_index_chunks(
|
||||
datastore: Arc<DataStore>,
|
||||
index: Box<dyn IndexFile + Send>,
|
||||
@ -128,60 +88,87 @@ fn verify_index_chunks(
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
let chunk_channel = chunk_reader_thread(
|
||||
datastore.clone(),
|
||||
index,
|
||||
verified_chunks.clone(),
|
||||
corrupt_chunks.clone(),
|
||||
errors.clone(),
|
||||
worker.clone(),
|
||||
);
|
||||
|
||||
let mut read_bytes = 0;
|
||||
let mut decoded_bytes = 0;
|
||||
|
||||
loop {
|
||||
let worker2 = Arc::clone(&worker);
|
||||
let datastore2 = Arc::clone(&datastore);
|
||||
let corrupt_chunks2 = Arc::clone(&corrupt_chunks);
|
||||
let verified_chunks2 = Arc::clone(&verified_chunks);
|
||||
let errors2 = Arc::clone(&errors);
|
||||
|
||||
let decoder_pool = ParallelHandler::new(
|
||||
"verify chunk decoder", 4,
|
||||
move |(chunk, digest, size): (DataBlob, [u8;32], u64)| {
|
||||
let chunk_crypt_mode = match chunk.crypt_mode() {
|
||||
Err(err) => {
|
||||
corrupt_chunks2.lock().unwrap().insert(digest);
|
||||
worker2.log(format!("can't verify chunk, unknown CryptMode - {}", err));
|
||||
errors2.fetch_add(1, Ordering::SeqCst);
|
||||
return Ok(());
|
||||
},
|
||||
Ok(mode) => mode,
|
||||
};
|
||||
|
||||
if chunk_crypt_mode != crypt_mode {
|
||||
worker2.log(format!(
|
||||
"chunk CryptMode {:?} does not match index CryptMode {:?}",
|
||||
chunk_crypt_mode,
|
||||
crypt_mode
|
||||
));
|
||||
errors2.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if let Err(err) = chunk.verify_unencrypted(size as usize, &digest) {
|
||||
corrupt_chunks2.lock().unwrap().insert(digest);
|
||||
worker2.log(format!("{}", err));
|
||||
errors2.fetch_add(1, Ordering::SeqCst);
|
||||
rename_corrupted_chunk(datastore2.clone(), &digest, worker2.clone());
|
||||
} else {
|
||||
verified_chunks2.lock().unwrap().insert(digest);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
);
|
||||
|
||||
for pos in 0..index.index_count() {
|
||||
|
||||
worker.fail_on_abort()?;
|
||||
crate::tools::fail_on_shutdown()?;
|
||||
|
||||
let (chunk, digest, size) = match chunk_channel.recv() {
|
||||
Ok(tuple) => tuple,
|
||||
Err(std::sync::mpsc::RecvError) => break,
|
||||
};
|
||||
let info = index.chunk_info(pos).unwrap();
|
||||
let size = info.size();
|
||||
|
||||
read_bytes += chunk.raw_size();
|
||||
decoded_bytes += size;
|
||||
|
||||
let chunk_crypt_mode = match chunk.crypt_mode() {
|
||||
Err(err) => {
|
||||
corrupt_chunks.lock().unwrap().insert(digest);
|
||||
worker.log(format!("can't verify chunk, unknown CryptMode - {}", err));
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
continue;
|
||||
},
|
||||
Ok(mode) => mode,
|
||||
};
|
||||
|
||||
if chunk_crypt_mode != crypt_mode {
|
||||
worker.log(format!(
|
||||
"chunk CryptMode {:?} does not match index CryptMode {:?}",
|
||||
chunk_crypt_mode,
|
||||
crypt_mode
|
||||
));
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
if verified_chunks.lock().unwrap().contains(&info.digest) {
|
||||
continue; // already verified
|
||||
}
|
||||
|
||||
if let Err(err) = chunk.verify_unencrypted(size as usize, &digest) {
|
||||
corrupt_chunks.lock().unwrap().insert(digest);
|
||||
worker.log(format!("{}", err));
|
||||
if corrupt_chunks.lock().unwrap().contains(&info.digest) {
|
||||
let digest_str = proxmox::tools::digest_to_hex(&info.digest);
|
||||
worker.log(format!("chunk {} was marked as corrupt", digest_str));
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
rename_corrupted_chunk(datastore.clone(), &digest, worker.clone());
|
||||
} else {
|
||||
verified_chunks.lock().unwrap().insert(digest);
|
||||
continue;
|
||||
}
|
||||
|
||||
match datastore.load_chunk(&info.digest) {
|
||||
Err(err) => {
|
||||
corrupt_chunks.lock().unwrap().insert(info.digest);
|
||||
worker.log(format!("can't verify chunk, load failed - {}", err));
|
||||
errors.fetch_add(1, Ordering::SeqCst);
|
||||
rename_corrupted_chunk(datastore.clone(), &info.digest, worker.clone());
|
||||
continue;
|
||||
}
|
||||
Ok(chunk) => {
|
||||
read_bytes += chunk.raw_size();
|
||||
decoder_pool.send((chunk, info.digest, size))?;
|
||||
decoded_bytes += size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decoder_pool.complete()?;
|
||||
|
||||
let elapsed = start_time.elapsed().as_secs_f64();
|
||||
|
||||
let read_bytes_mib = (read_bytes as f64)/(1024.0*1024.0);
|
||||
|
@ -192,7 +192,7 @@ pub fn complete_repository(_arg: &str, _param: &HashMap<String, String>) -> Vec<
|
||||
result
|
||||
}
|
||||
|
||||
fn connect(server: &str, userid: &Userid) -> Result<HttpClient, Error> {
|
||||
fn connect(server: &str, port: u16, userid: &Userid) -> Result<HttpClient, Error> {
|
||||
|
||||
let fingerprint = std::env::var(ENV_VAR_PBS_FINGERPRINT).ok();
|
||||
|
||||
@ -211,7 +211,7 @@ fn connect(server: &str, userid: &Userid) -> Result<HttpClient, Error> {
|
||||
.fingerprint_cache(true)
|
||||
.ticket_cache(true);
|
||||
|
||||
HttpClient::new(server, userid, options)
|
||||
HttpClient::new(server, port, userid, options)
|
||||
}
|
||||
|
||||
async fn view_task_result(
|
||||
@ -365,7 +365,7 @@ async fn list_backup_groups(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/groups", repo.store());
|
||||
|
||||
@ -438,7 +438,7 @@ async fn list_snapshots(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let group: Option<BackupGroup> = if let Some(path) = param["group"].as_str() {
|
||||
Some(path.parse()?)
|
||||
@ -503,7 +503,7 @@ async fn forget_snapshots(param: Value) -> Result<Value, Error> {
|
||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||
let snapshot: BackupDir = path.parse()?;
|
||||
|
||||
let mut client = connect(repo.host(), repo.user())?;
|
||||
let mut client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/snapshots", repo.store());
|
||||
|
||||
@ -533,7 +533,7 @@ async fn api_login(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
client.login().await?;
|
||||
|
||||
record_repository(&repo);
|
||||
@ -590,7 +590,7 @@ async fn api_version(param: Value) -> Result<(), Error> {
|
||||
|
||||
let repo = extract_repository_from_value(¶m);
|
||||
if let Ok(repo) = repo {
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
match client.get("api2/json/version", None).await {
|
||||
Ok(mut result) => version_info["server"] = result["data"].take(),
|
||||
@ -640,7 +640,7 @@ async fn list_snapshot_files(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/files", repo.store());
|
||||
|
||||
@ -684,7 +684,7 @@ async fn start_garbage_collection(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let mut client = connect(repo.host(), repo.user())?;
|
||||
let mut client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/gc", repo.store());
|
||||
|
||||
@ -996,7 +996,7 @@ async fn create_backup(
|
||||
|
||||
let backup_time = backup_time_opt.unwrap_or_else(|| epoch_i64());
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
record_repository(&repo);
|
||||
|
||||
println!("Starting backup: {}/{}/{}", backup_type, backup_id, BackupDir::backup_time_to_string(backup_time)?);
|
||||
@ -1299,7 +1299,7 @@ async fn restore(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let archive_name = tools::required_string_param(¶m, "archive-name")?;
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
record_repository(&repo);
|
||||
|
||||
@ -1472,7 +1472,7 @@ async fn upload_log(param: Value) -> Result<Value, Error> {
|
||||
let snapshot = tools::required_string_param(¶m, "snapshot")?;
|
||||
let snapshot: BackupDir = snapshot.parse()?;
|
||||
|
||||
let mut client = connect(repo.host(), repo.user())?;
|
||||
let mut client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let (keydata, crypt_mode) = keyfile_parameters(¶m)?;
|
||||
|
||||
@ -1543,7 +1543,7 @@ fn prune<'a>(
|
||||
async fn prune_async(mut param: Value) -> Result<Value, Error> {
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
|
||||
let mut client = connect(repo.host(), repo.user())?;
|
||||
let mut client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/prune", repo.store());
|
||||
|
||||
@ -1626,7 +1626,7 @@ async fn status(param: Value) -> Result<Value, Error> {
|
||||
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/status", repo.store());
|
||||
|
||||
@ -1671,7 +1671,7 @@ async fn try_get(repo: &BackupRepository, url: &str) -> Value {
|
||||
.fingerprint_cache(true)
|
||||
.ticket_cache(true);
|
||||
|
||||
let client = match HttpClient::new(repo.host(), repo.user(), options) {
|
||||
let client = match HttpClient::new(repo.host(), repo.port(), repo.user(), options) {
|
||||
Ok(v) => v,
|
||||
_ => return Value::Null,
|
||||
};
|
||||
|
@ -62,10 +62,10 @@ fn connect() -> Result<HttpClient, Error> {
|
||||
let ticket = Ticket::new("PBS", Userid::root_userid())?
|
||||
.sign(private_auth_key(), None)?;
|
||||
options = options.password(Some(ticket));
|
||||
HttpClient::new("localhost", Userid::root_userid(), options)?
|
||||
HttpClient::new("localhost", 8007, Userid::root_userid(), options)?
|
||||
} else {
|
||||
options = options.ticket_cache(true).interactive(true);
|
||||
HttpClient::new("localhost", Userid::root_userid(), options)?
|
||||
HttpClient::new("localhost", 8007, Userid::root_userid(), options)?
|
||||
};
|
||||
|
||||
Ok(client)
|
||||
@ -410,6 +410,7 @@ pub fn complete_remote_datastore_name(_arg: &str, param: &HashMap<String, String
|
||||
|
||||
let client = HttpClient::new(
|
||||
&remote.host,
|
||||
remote.port.unwrap_or(8007),
|
||||
&remote.userid,
|
||||
options,
|
||||
)?;
|
||||
|
@ -198,44 +198,19 @@ async fn schedule_tasks() -> Result<(), Error> {
|
||||
schedule_datastore_prune().await;
|
||||
schedule_datastore_verification().await;
|
||||
schedule_datastore_sync_jobs().await;
|
||||
schedule_task_log_rotate().await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn lookup_last_worker(worker_type: &str, worker_id: &str) -> Result<Option<server::UPID>, Error> {
|
||||
|
||||
let list = proxmox_backup::server::read_task_list()?;
|
||||
|
||||
let mut last: Option<&server::UPID> = None;
|
||||
|
||||
for entry in list.iter() {
|
||||
if entry.upid.worker_type == worker_type {
|
||||
if let Some(ref id) = entry.upid.worker_id {
|
||||
if id == worker_id {
|
||||
match last {
|
||||
Some(ref upid) => {
|
||||
if upid.starttime < entry.upid.starttime {
|
||||
last = Some(&entry.upid)
|
||||
}
|
||||
}
|
||||
None => {
|
||||
last = Some(&entry.upid)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(last.cloned())
|
||||
}
|
||||
|
||||
|
||||
async fn schedule_datastore_garbage_collection() {
|
||||
|
||||
use proxmox_backup::backup::DataStore;
|
||||
use proxmox_backup::server::{UPID, WorkerTask};
|
||||
use proxmox_backup::config::datastore::{self, DataStoreConfig};
|
||||
use proxmox_backup::config::{
|
||||
jobstate::{self, Job},
|
||||
datastore::{self, DataStoreConfig}
|
||||
};
|
||||
use proxmox_backup::tools::systemd::time::{
|
||||
parse_calendar_event, compute_next_event};
|
||||
|
||||
@ -291,11 +266,10 @@ async fn schedule_datastore_garbage_collection() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match lookup_last_worker(worker_type, &store) {
|
||||
Ok(Some(upid)) => upid.starttime,
|
||||
Ok(None) => 0,
|
||||
match jobstate::last_run_time(worker_type, &store) {
|
||||
Ok(time) => time,
|
||||
Err(err) => {
|
||||
eprintln!("lookup_last_job_start failed: {}", err);
|
||||
eprintln!("could not get last run time of {} {}: {}", worker_type, store, err);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -314,6 +288,11 @@ async fn schedule_datastore_garbage_collection() {
|
||||
|
||||
if next > now { continue; }
|
||||
|
||||
let mut job = match Job::new(worker_type, &store) {
|
||||
Ok(job) => job,
|
||||
Err(_) => continue, // could not get lock
|
||||
};
|
||||
|
||||
let store2 = store.clone();
|
||||
|
||||
if let Err(err) = WorkerTask::new_thread(
|
||||
@ -322,9 +301,20 @@ async fn schedule_datastore_garbage_collection() {
|
||||
Userid::backup_userid().clone(),
|
||||
false,
|
||||
move |worker| {
|
||||
job.start(&worker.upid().to_string())?;
|
||||
|
||||
worker.log(format!("starting garbage collection on store {}", store));
|
||||
worker.log(format!("task triggered by schedule '{}'", event_str));
|
||||
datastore.garbage_collection(&worker)
|
||||
|
||||
let result = datastore.garbage_collection(&worker);
|
||||
|
||||
let status = worker.create_state(&result);
|
||||
|
||||
if let Err(err) = job.finish(status) {
|
||||
eprintln!("could not finish job state for {}: {}", worker_type, err);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
) {
|
||||
eprintln!("unable to start garbage collection on store {} - {}", store2, err);
|
||||
@ -434,7 +424,7 @@ async fn schedule_datastore_prune() {
|
||||
|
||||
job.start(&worker.upid().to_string())?;
|
||||
|
||||
let result = {
|
||||
let result = try_block!({
|
||||
|
||||
worker.log(format!("Starting datastore prune on store \"{}\"", store));
|
||||
worker.log(format!("task triggered by schedule '{}'", event_str));
|
||||
@ -463,7 +453,7 @@ async fn schedule_datastore_prune() {
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
});
|
||||
|
||||
let status = worker.create_state(&result);
|
||||
|
||||
@ -482,7 +472,10 @@ async fn schedule_datastore_prune() {
|
||||
async fn schedule_datastore_verification() {
|
||||
use proxmox_backup::backup::{DataStore, verify_all_backups};
|
||||
use proxmox_backup::server::{WorkerTask};
|
||||
use proxmox_backup::config::datastore::{self, DataStoreConfig};
|
||||
use proxmox_backup::config::{
|
||||
jobstate::{self, Job},
|
||||
datastore::{self, DataStoreConfig}
|
||||
};
|
||||
use proxmox_backup::tools::systemd::time::{
|
||||
parse_calendar_event, compute_next_event};
|
||||
|
||||
@ -526,16 +519,10 @@ async fn schedule_datastore_verification() {
|
||||
|
||||
let worker_type = "verify";
|
||||
|
||||
let last = match lookup_last_worker(worker_type, &store) {
|
||||
Ok(Some(upid)) => {
|
||||
if proxmox_backup::server::worker_is_active_local(&upid) {
|
||||
continue;
|
||||
}
|
||||
upid.starttime
|
||||
}
|
||||
Ok(None) => 0,
|
||||
let last = match jobstate::last_run_time(worker_type, &store) {
|
||||
Ok(time) => time,
|
||||
Err(err) => {
|
||||
eprintln!("lookup_last_job_start failed: {}", err);
|
||||
eprintln!("could not get last run time of {} {}: {}", worker_type, store, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@ -553,6 +540,11 @@ async fn schedule_datastore_verification() {
|
||||
|
||||
if next > now { continue; }
|
||||
|
||||
let mut job = match Job::new(worker_type, &store) {
|
||||
Ok(job) => job,
|
||||
Err(_) => continue, // could not get lock
|
||||
};
|
||||
|
||||
let worker_id = store.clone();
|
||||
let store2 = store.clone();
|
||||
if let Err(err) = WorkerTask::new_thread(
|
||||
@ -561,18 +553,29 @@ async fn schedule_datastore_verification() {
|
||||
Userid::backup_userid().clone(),
|
||||
false,
|
||||
move |worker| {
|
||||
job.start(&worker.upid().to_string())?;
|
||||
worker.log(format!("starting verification on store {}", store2));
|
||||
worker.log(format!("task triggered by schedule '{}'", event_str));
|
||||
if let Ok(failed_dirs) = verify_all_backups(datastore, worker.clone()) {
|
||||
let result = try_block!({
|
||||
let failed_dirs = verify_all_backups(datastore, worker.clone())?;
|
||||
if failed_dirs.len() > 0 {
|
||||
worker.log("Failed to verify following snapshots:");
|
||||
for dir in failed_dirs {
|
||||
worker.log(format!("\t{}", dir));
|
||||
}
|
||||
bail!("verification failed - please check the log for details");
|
||||
Err(format_err!("verification failed - please check the log for details"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
let status = worker.create_state(&result);
|
||||
|
||||
if let Err(err) = job.finish(status) {
|
||||
eprintln!("could not finish job state for {}: {}", worker_type, err);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
result
|
||||
},
|
||||
) {
|
||||
eprintln!("unable to start verification on store {} - {}", store, err);
|
||||
@ -653,6 +656,101 @@ async fn schedule_datastore_sync_jobs() {
|
||||
}
|
||||
}
|
||||
|
||||
async fn schedule_task_log_rotate() {
|
||||
use proxmox_backup::{
|
||||
config::jobstate::{self, Job},
|
||||
server::rotate_task_log_archive,
|
||||
};
|
||||
use proxmox_backup::server::WorkerTask;
|
||||
use proxmox_backup::tools::systemd::time::{
|
||||
parse_calendar_event, compute_next_event};
|
||||
|
||||
let worker_type = "logrotate";
|
||||
let job_id = "task-archive";
|
||||
|
||||
let last = match jobstate::last_run_time(worker_type, job_id) {
|
||||
Ok(time) => time,
|
||||
Err(err) => {
|
||||
eprintln!("could not get last run time of task log archive rotation: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// schedule daily at 00:00 like normal logrotate
|
||||
let schedule = "00:00";
|
||||
|
||||
let event = match parse_calendar_event(schedule) {
|
||||
Ok(event) => event,
|
||||
Err(err) => {
|
||||
// should not happen?
|
||||
eprintln!("unable to parse schedule '{}' - {}", schedule, err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let next = match compute_next_event(&event, last, false) {
|
||||
Ok(Some(next)) => next,
|
||||
Ok(None) => return,
|
||||
Err(err) => {
|
||||
eprintln!("compute_next_event for '{}' failed - {}", schedule, err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let now = proxmox::tools::time::epoch_i64();
|
||||
|
||||
if next > now {
|
||||
// if we never ran the rotation, schedule instantly
|
||||
match jobstate::JobState::load(worker_type, job_id) {
|
||||
Ok(state) => match state {
|
||||
jobstate::JobState::Created { .. } => {},
|
||||
_ => return,
|
||||
},
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
|
||||
let mut job = match Job::new(worker_type, job_id) {
|
||||
Ok(job) => job,
|
||||
Err(_) => return, // could not get lock
|
||||
};
|
||||
|
||||
if let Err(err) = WorkerTask::new_thread(
|
||||
worker_type,
|
||||
Some(job_id.to_string()),
|
||||
Userid::backup_userid().clone(),
|
||||
false,
|
||||
move |worker| {
|
||||
job.start(&worker.upid().to_string())?;
|
||||
worker.log(format!("starting task log rotation"));
|
||||
// one entry has normally about ~100-150 bytes
|
||||
let max_size = 500000; // at least 5000 entries
|
||||
let max_files = 20; // at least 100000 entries
|
||||
let result = try_block!({
|
||||
let has_rotated = rotate_task_log_archive(max_size, true, Some(max_files))?;
|
||||
if has_rotated {
|
||||
worker.log(format!("task log archive was rotated"));
|
||||
} else {
|
||||
worker.log(format!("task log archive was not rotated"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let status = worker.create_state(&result);
|
||||
|
||||
if let Err(err) = job.finish(status) {
|
||||
eprintln!("could not finish job state for {}: {}", worker_type, err);
|
||||
}
|
||||
|
||||
result
|
||||
},
|
||||
) {
|
||||
eprintln!("unable to start task log rotation: {}", err);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async fn run_stat_generator() {
|
||||
|
||||
let mut count = 0;
|
||||
|
@ -21,6 +21,7 @@ use proxmox_backup::backup::{
|
||||
load_and_decrypt_key,
|
||||
CryptConfig,
|
||||
KeyDerivationConfig,
|
||||
DataChunkBuilder,
|
||||
};
|
||||
|
||||
use proxmox_backup::client::*;
|
||||
@ -60,6 +61,9 @@ struct Speed {
|
||||
"aes256_gcm": {
|
||||
type: Speed,
|
||||
},
|
||||
"verify": {
|
||||
type: Speed,
|
||||
},
|
||||
},
|
||||
)]
|
||||
#[derive(Copy, Clone, Serialize)]
|
||||
@ -75,9 +79,10 @@ struct BenchmarkResult {
|
||||
decompress: Speed,
|
||||
/// AES256 GCM encryption speed
|
||||
aes256_gcm: Speed,
|
||||
/// Verify speed
|
||||
verify: Speed,
|
||||
}
|
||||
|
||||
|
||||
static BENCHMARK_RESULT_2020_TOP: BenchmarkResult = BenchmarkResult {
|
||||
tls: Speed {
|
||||
speed: None,
|
||||
@ -85,19 +90,23 @@ static BENCHMARK_RESULT_2020_TOP: BenchmarkResult = BenchmarkResult {
|
||||
},
|
||||
sha256: Speed {
|
||||
speed: None,
|
||||
top: 1_000_000.0 * 2120.0, // AMD Ryzen 7 2700X
|
||||
top: 1_000_000.0 * 2022.0, // AMD Ryzen 7 2700X
|
||||
},
|
||||
compress: Speed {
|
||||
speed: None,
|
||||
top: 1_000_000.0 * 2158.0, // AMD Ryzen 7 2700X
|
||||
top: 1_000_000.0 * 752.0, // AMD Ryzen 7 2700X
|
||||
},
|
||||
decompress: Speed {
|
||||
speed: None,
|
||||
top: 1_000_000.0 * 8062.0, // AMD Ryzen 7 2700X
|
||||
top: 1_000_000.0 * 1198.0, // AMD Ryzen 7 2700X
|
||||
},
|
||||
aes256_gcm: Speed {
|
||||
speed: None,
|
||||
top: 1_000_000.0 * 3803.0, // AMD Ryzen 7 2700X
|
||||
top: 1_000_000.0 * 3645.0, // AMD Ryzen 7 2700X
|
||||
},
|
||||
verify: Speed {
|
||||
speed: None,
|
||||
top: 1_000_000.0 * 758.0, // AMD Ryzen 7 2700X
|
||||
},
|
||||
};
|
||||
|
||||
@ -194,7 +203,10 @@ fn render_result(
|
||||
.column(ColumnConfig::new("decompress")
|
||||
.header("ZStd level 1 decompression speed")
|
||||
.right_align(false).renderer(render_speed))
|
||||
.column(ColumnConfig::new("aes256_gcm")
|
||||
.column(ColumnConfig::new("verify")
|
||||
.header("Chunk verification speed")
|
||||
.right_align(false).renderer(render_speed))
|
||||
.column(ColumnConfig::new("aes256_gcm")
|
||||
.header("AES256 GCM encryption speed")
|
||||
.right_align(false).renderer(render_speed));
|
||||
|
||||
@ -213,7 +225,7 @@ async fn test_upload_speed(
|
||||
|
||||
let backup_time = proxmox::tools::time::epoch_i64();
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
record_repository(&repo);
|
||||
|
||||
if verbose { eprintln!("Connecting to backup server"); }
|
||||
@ -257,7 +269,17 @@ fn test_crypt_speed(
|
||||
|
||||
let crypt_config = CryptConfig::new(testkey)?;
|
||||
|
||||
let random_data = proxmox::sys::linux::random_data(1024*1024)?;
|
||||
//let random_data = proxmox::sys::linux::random_data(1024*1024)?;
|
||||
let mut random_data = vec![];
|
||||
// generate pseudo random byte sequence
|
||||
for i in 0..256*1024 {
|
||||
for j in 0..4 {
|
||||
let byte = ((i >> (j<<3))&0xff) as u8;
|
||||
random_data.push(byte);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(random_data.len(), 1024*1024);
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
@ -322,5 +344,23 @@ fn test_crypt_speed(
|
||||
|
||||
eprintln!("AES256/GCM speed: {:.2} MB/s", speed/1_000_000_.0);
|
||||
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let (chunk, digest) = DataChunkBuilder::new(&random_data)
|
||||
.compress(true)
|
||||
.build()?;
|
||||
|
||||
let mut bytes = 0;
|
||||
loop {
|
||||
chunk.verify_unencrypted(random_data.len(), &digest)?;
|
||||
bytes += random_data.len();
|
||||
if start_time.elapsed().as_micros() > 1_000_000 { break; }
|
||||
}
|
||||
let speed = (bytes as f64)/start_time.elapsed().as_secs_f64();
|
||||
benchmark_result.verify.speed = Some(speed);
|
||||
|
||||
eprintln!("Verify speed: {:.2} MB/s", speed/1_000_000_.0);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ async fn dump_catalog(param: Value) -> Result<Value, Error> {
|
||||
}
|
||||
};
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let client = BackupReader::start(
|
||||
client,
|
||||
@ -153,7 +153,7 @@ async fn dump_catalog(param: Value) -> Result<Value, Error> {
|
||||
/// Shell to interactively inspect and restore snapshots.
|
||||
async fn catalog_shell(param: Value) -> Result<(), Error> {
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
let path = tools::required_string_param(¶m, "snapshot")?;
|
||||
let archive_name = tools::required_string_param(¶m, "archive-name")?;
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
use std::path::PathBuf;
|
||||
use std::io::Write;
|
||||
use std::process::{Stdio, Command};
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -13,6 +15,17 @@ use proxmox_backup::backup::{
|
||||
};
|
||||
use proxmox_backup::tools;
|
||||
|
||||
#[api()]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
/// Paperkey output format
|
||||
pub enum PaperkeyFormat {
|
||||
/// Format as Utf8 text. Includes QR codes as ascii-art.
|
||||
Text,
|
||||
/// Format as Html. Includes QR codes as png images.
|
||||
Html,
|
||||
}
|
||||
|
||||
pub const DEFAULT_ENCRYPTION_KEY_FILE_NAME: &str = "encryption-key.json";
|
||||
pub const MASTER_PUBKEY_FILE_NAME: &str = "master-public.pem";
|
||||
|
||||
@ -261,6 +274,55 @@ fn create_master_key() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[api(
|
||||
input: {
|
||||
properties: {
|
||||
path: {
|
||||
description: "Key file. Without this the default key's will be used.",
|
||||
optional: true,
|
||||
},
|
||||
subject: {
|
||||
description: "Include the specified subject as titel text.",
|
||||
optional: true,
|
||||
},
|
||||
"output-format": {
|
||||
type: PaperkeyFormat,
|
||||
description: "Output format. Text or Html.",
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
)]
|
||||
/// Generate a printable, human readable text file containing the encryption key.
|
||||
///
|
||||
/// This also includes a scanable QR code for fast key restore.
|
||||
fn paper_key(
|
||||
path: Option<String>,
|
||||
subject: Option<String>,
|
||||
output_format: Option<PaperkeyFormat>,
|
||||
) -> Result<(), Error> {
|
||||
let path = match path {
|
||||
Some(path) => PathBuf::from(path),
|
||||
None => {
|
||||
let path = find_default_encryption_key()?
|
||||
.ok_or_else(|| {
|
||||
format_err!("no encryption file provided and no default file found")
|
||||
})?;
|
||||
path
|
||||
}
|
||||
};
|
||||
|
||||
let data = file_get_contents(&path)?;
|
||||
let data = std::str::from_utf8(&data)?;
|
||||
|
||||
let format = output_format.unwrap_or(PaperkeyFormat::Html);
|
||||
|
||||
match format {
|
||||
PaperkeyFormat::Html => paperkey_html(data, subject),
|
||||
PaperkeyFormat::Text => paperkey_text(data, subject),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cli() -> CliCommandMap {
|
||||
let key_create_cmd_def = CliCommand::new(&API_METHOD_CREATE)
|
||||
.arg_param(&["path"])
|
||||
@ -275,9 +337,214 @@ pub fn cli() -> CliCommandMap {
|
||||
.arg_param(&["path"])
|
||||
.completion_cb("path", tools::complete_file_name);
|
||||
|
||||
let paper_key_cmd_def = CliCommand::new(&API_METHOD_PAPER_KEY)
|
||||
.arg_param(&["path"])
|
||||
.completion_cb("path", tools::complete_file_name);
|
||||
|
||||
CliCommandMap::new()
|
||||
.insert("create", key_create_cmd_def)
|
||||
.insert("create-master-key", key_create_master_key_cmd_def)
|
||||
.insert("import-master-pubkey", key_import_master_pubkey_cmd_def)
|
||||
.insert("change-passphrase", key_change_passphrase_cmd_def)
|
||||
.insert("paper-key", paper_key_cmd_def)
|
||||
}
|
||||
|
||||
fn paperkey_html(data: &str, subject: Option<String>) -> Result<(), Error> {
|
||||
|
||||
let img_size_pt = 500;
|
||||
|
||||
println!("<!DOCTYPE html>");
|
||||
println!("<html lang=\"en\">");
|
||||
println!("<head>");
|
||||
println!("<meta charset=\"utf-8\">");
|
||||
println!("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
|
||||
println!("<title>Proxmox Backup Paperkey</title>");
|
||||
println!("<style type=\"text/css\">");
|
||||
|
||||
println!(" p {{");
|
||||
println!(" font-size: 12pt;");
|
||||
println!(" font-family: monospace;");
|
||||
println!(" white-space: pre-wrap;");
|
||||
println!(" line-break: anywhere;");
|
||||
println!(" }}");
|
||||
|
||||
println!("</style>");
|
||||
|
||||
println!("</head>");
|
||||
|
||||
println!("<body>");
|
||||
|
||||
if let Some(subject) = subject {
|
||||
println!("<p>Subject: {}</p>", subject);
|
||||
}
|
||||
|
||||
if data.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") {
|
||||
let lines: Vec<String> = data.lines()
|
||||
.map(|s| s.trim_end())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
if !lines[lines.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
|
||||
bail!("unexpected key format");
|
||||
}
|
||||
|
||||
if lines.len() < 20 {
|
||||
bail!("unexpected key format");
|
||||
}
|
||||
|
||||
const BLOCK_SIZE: usize = 20;
|
||||
let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE;
|
||||
|
||||
for i in 0..blocks {
|
||||
let start = i*BLOCK_SIZE;
|
||||
let mut end = start + BLOCK_SIZE;
|
||||
if end > lines.len() {
|
||||
end = lines.len();
|
||||
}
|
||||
let data = &lines[start..end];
|
||||
|
||||
println!("<div style=\"page-break-inside: avoid;page-break-after: always\">");
|
||||
println!("<p>");
|
||||
|
||||
for l in start..end {
|
||||
println!("{:02}: {}", l, lines[l]);
|
||||
}
|
||||
|
||||
println!("</p>");
|
||||
|
||||
let data = data.join("\n");
|
||||
let qr_code = generate_qr_code("png", data.as_bytes())?;
|
||||
let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
|
||||
|
||||
println!("<center>");
|
||||
println!("<img");
|
||||
println!("width=\"{}pt\" height=\"{}pt\"", img_size_pt, img_size_pt);
|
||||
println!("src=\"data:image/png;base64,{}\"/>", qr_code);
|
||||
println!("</center>");
|
||||
println!("</div>");
|
||||
}
|
||||
|
||||
println!("</body>");
|
||||
println!("</html>");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let key_config: KeyConfig = serde_json::from_str(&data)?;
|
||||
let key_text = serde_json::to_string_pretty(&key_config)?;
|
||||
|
||||
println!("<div style=\"page-break-inside: avoid\">");
|
||||
|
||||
println!("<p>");
|
||||
|
||||
println!("-----BEGIN PROXMOX BACKUP KEY-----");
|
||||
|
||||
for line in key_text.lines() {
|
||||
println!("{}", line);
|
||||
}
|
||||
|
||||
println!("-----END PROXMOX BACKUP KEY-----");
|
||||
|
||||
println!("</p>");
|
||||
|
||||
let qr_code = generate_qr_code("png", key_text.as_bytes())?;
|
||||
let qr_code = base64::encode_config(&qr_code, base64::STANDARD_NO_PAD);
|
||||
|
||||
println!("<center>");
|
||||
println!("<img");
|
||||
println!("width=\"{}pt\" height=\"{}pt\"", img_size_pt, img_size_pt);
|
||||
println!("src=\"data:image/png;base64,{}\"/>", qr_code);
|
||||
println!("</center>");
|
||||
|
||||
println!("</div>");
|
||||
|
||||
println!("</body>");
|
||||
println!("</html>");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn paperkey_text(data: &str, subject: Option<String>) -> Result<(), Error> {
|
||||
|
||||
if let Some(subject) = subject {
|
||||
println!("Subject: {}\n", subject);
|
||||
}
|
||||
|
||||
if data.starts_with("-----BEGIN ENCRYPTED PRIVATE KEY-----\n") {
|
||||
let lines: Vec<String> = data.lines()
|
||||
.map(|s| s.trim_end())
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
if !lines[lines.len()-1].starts_with("-----END ENCRYPTED PRIVATE KEY-----") {
|
||||
bail!("unexpected key format");
|
||||
}
|
||||
|
||||
if lines.len() < 20 {
|
||||
bail!("unexpected key format");
|
||||
}
|
||||
|
||||
const BLOCK_SIZE: usize = 5;
|
||||
let blocks = (lines.len() + BLOCK_SIZE -1)/BLOCK_SIZE;
|
||||
|
||||
for i in 0..blocks {
|
||||
let start = i*BLOCK_SIZE;
|
||||
let mut end = start + BLOCK_SIZE;
|
||||
if end > lines.len() {
|
||||
end = lines.len();
|
||||
}
|
||||
let data = &lines[start..end];
|
||||
|
||||
for l in start..end {
|
||||
println!("{:-2}: {}", l, lines[l]);
|
||||
}
|
||||
let data = data.join("\n");
|
||||
let qr_code = generate_qr_code("utf8i", data.as_bytes())?;
|
||||
let qr_code = String::from_utf8(qr_code)
|
||||
.map_err(|_| format_err!("Failed to read qr code (got non-utf8 data)"))?;
|
||||
println!("{}", qr_code);
|
||||
println!("{}", char::from(12u8)); // page break
|
||||
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let key_config: KeyConfig = serde_json::from_str(&data)?;
|
||||
let key_text = serde_json::to_string_pretty(&key_config)?;
|
||||
|
||||
println!("-----BEGIN PROXMOX BACKUP KEY-----");
|
||||
println!("{}", key_text);
|
||||
println!("-----END PROXMOX BACKUP KEY-----");
|
||||
|
||||
let qr_code = generate_qr_code("utf8i", key_text.as_bytes())?;
|
||||
let qr_code = String::from_utf8(qr_code)
|
||||
.map_err(|_| format_err!("Failed to read qr code (got non-utf8 data)"))?;
|
||||
|
||||
println!("{}", qr_code);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_qr_code(output_type: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
|
||||
|
||||
let mut child = Command::new("qrencode")
|
||||
.args(&["-t", output_type, "-m0", "-s1", "-lm", "--output", "-"])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
{
|
||||
let stdin = child.stdin.as_mut()
|
||||
.ok_or_else(|| format_err!("Failed to open stdin"))?;
|
||||
stdin.write_all(data)
|
||||
.map_err(|_| format_err!("Failed to write to stdin"))?;
|
||||
}
|
||||
|
||||
let output = child.wait_with_output()
|
||||
.map_err(|_| format_err!("Failed to read stdout"))?;
|
||||
|
||||
let output = crate::tools::command_output(output, None)?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
@ -101,7 +101,7 @@ async fn mount_do(param: Value, pipe: Option<RawFd>) -> Result<Value, Error> {
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
let archive_name = tools::required_string_param(¶m, "archive-name")?;
|
||||
let target = tools::required_string_param(¶m, "target")?;
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
record_repository(&repo);
|
||||
|
||||
|
@ -48,7 +48,7 @@ async fn task_list(param: Value) -> Result<Value, Error> {
|
||||
let output_format = get_output_format(¶m);
|
||||
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let limit = param["limit"].as_u64().unwrap_or(50) as usize;
|
||||
let running = !param["all"].as_bool().unwrap_or(false);
|
||||
@ -96,7 +96,7 @@ async fn task_log(param: Value) -> Result<Value, Error> {
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
let upid = tools::required_string_param(¶m, "upid")?;
|
||||
|
||||
let client = connect(repo.host(), repo.user())?;
|
||||
let client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
display_task_log(client, upid, true).await?;
|
||||
|
||||
@ -122,7 +122,7 @@ async fn task_stop(param: Value) -> Result<Value, Error> {
|
||||
let repo = extract_repository_from_value(¶m)?;
|
||||
let upid_str = tools::required_string_param(¶m, "upid")?;
|
||||
|
||||
let mut client = connect(repo.host(), repo.user())?;
|
||||
let mut client = connect(repo.host(), repo.port(), repo.user())?;
|
||||
|
||||
let path = format!("api2/json/nodes/localhost/tasks/{}", upid_str);
|
||||
let _ = client.delete(&path, None).await?;
|
||||
|
@ -54,7 +54,7 @@ impl BackupReader {
|
||||
"store": datastore,
|
||||
"debug": debug,
|
||||
});
|
||||
let req = HttpClient::request_builder(client.server(), "GET", "/api2/json/reader", Some(param)).unwrap();
|
||||
let req = HttpClient::request_builder(client.server(), client.port(), "GET", "/api2/json/reader", Some(param)).unwrap();
|
||||
|
||||
let (h2, abort) = client.start_h2_connection(req, String::from(PROXMOX_BACKUP_READER_PROTOCOL_ID_V1!())).await?;
|
||||
|
||||
|
@ -19,14 +19,22 @@ pub struct BackupRepository {
|
||||
user: Option<Userid>,
|
||||
/// The host name or IP address
|
||||
host: Option<String>,
|
||||
/// The port
|
||||
port: Option<u16>,
|
||||
/// The name of the datastore
|
||||
store: String,
|
||||
}
|
||||
|
||||
impl BackupRepository {
|
||||
|
||||
pub fn new(user: Option<Userid>, host: Option<String>, store: String) -> Self {
|
||||
Self { user, host, store }
|
||||
pub fn new(user: Option<Userid>, host: Option<String>, port: Option<u16>, store: String) -> Self {
|
||||
let host = match host {
|
||||
Some(host) if (IP_V6_REGEX.regex_obj)().is_match(&host) => {
|
||||
Some(format!("[{}]", host))
|
||||
},
|
||||
other => other,
|
||||
};
|
||||
Self { user, host, port, store }
|
||||
}
|
||||
|
||||
pub fn user(&self) -> &Userid {
|
||||
@ -43,6 +51,13 @@ impl BackupRepository {
|
||||
"localhost"
|
||||
}
|
||||
|
||||
pub fn port(&self) -> u16 {
|
||||
if let Some(port) = self.port {
|
||||
return port;
|
||||
}
|
||||
8007
|
||||
}
|
||||
|
||||
pub fn store(&self) -> &str {
|
||||
&self.store
|
||||
}
|
||||
@ -50,13 +65,12 @@ impl BackupRepository {
|
||||
|
||||
impl fmt::Display for BackupRepository {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if let Some(ref user) = self.user {
|
||||
write!(f, "{}@{}:{}", user, self.host(), self.store)
|
||||
} else if let Some(ref host) = self.host {
|
||||
write!(f, "{}:{}", host, self.store)
|
||||
} else {
|
||||
write!(f, "{}", self.store)
|
||||
}
|
||||
match (&self.user, &self.host, self.port) {
|
||||
(Some(user), _, _) => write!(f, "{}@{}:{}:{}", user, self.host(), self.port(), self.store),
|
||||
(None, Some(host), None) => write!(f, "{}:{}", host, self.store),
|
||||
(None, _, Some(port)) => write!(f, "{}:{}:{}", self.host(), port, self.store),
|
||||
(None, None, None) => write!(f, "{}", self.store),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +90,8 @@ impl std::str::FromStr for BackupRepository {
|
||||
Ok(Self {
|
||||
user: cap.get(1).map(|m| Userid::try_from(m.as_str().to_owned())).transpose()?,
|
||||
host: cap.get(2).map(|m| m.as_str().to_owned()),
|
||||
store: cap[3].to_owned(),
|
||||
port: cap.get(3).map(|m| m.as_str().parse::<u16>()).transpose()?,
|
||||
store: cap[4].to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@ impl BackupWriter {
|
||||
});
|
||||
|
||||
let req = HttpClient::request_builder(
|
||||
client.server(), "GET", "/api2/json/backup", Some(param)).unwrap();
|
||||
client.server(), client.port(), "GET", "/api2/json/backup", Some(param)).unwrap();
|
||||
|
||||
let (h2, abort) = client.start_h2_connection(req, String::from(PROXMOX_BACKUP_PROTOCOL_ID_V1!())).await?;
|
||||
|
||||
|
@ -99,6 +99,7 @@ impl HttpClientOptions {
|
||||
pub struct HttpClient {
|
||||
client: Client<HttpsConnector>,
|
||||
server: String,
|
||||
port: u16,
|
||||
fingerprint: Arc<Mutex<Option<String>>>,
|
||||
first_auth: BroadcastFuture<()>,
|
||||
auth: Arc<RwLock<AuthInfo>>,
|
||||
@ -250,6 +251,7 @@ fn load_ticket_info(prefix: &str, server: &str, userid: &Userid) -> Option<(Stri
|
||||
impl HttpClient {
|
||||
pub fn new(
|
||||
server: &str,
|
||||
port: u16,
|
||||
userid: &Userid,
|
||||
mut options: HttpClientOptions,
|
||||
) -> Result<Self, Error> {
|
||||
@ -338,7 +340,7 @@ impl HttpClient {
|
||||
let authinfo = auth2.read().unwrap().clone();
|
||||
(authinfo.userid, authinfo.ticket)
|
||||
};
|
||||
match Self::credentials(client2.clone(), server2.clone(), userid, ticket).await {
|
||||
match Self::credentials(client2.clone(), server2.clone(), port, userid, ticket).await {
|
||||
Ok(auth) => {
|
||||
if use_ticket_cache & &prefix2.is_some() {
|
||||
let _ = store_ticket_info(prefix2.as_ref().unwrap(), &server2, &auth.userid.to_string(), &auth.ticket, &auth.token);
|
||||
@ -358,6 +360,7 @@ impl HttpClient {
|
||||
let login_future = Self::credentials(
|
||||
client.clone(),
|
||||
server.to_owned(),
|
||||
port,
|
||||
userid.to_owned(),
|
||||
password.to_owned(),
|
||||
).map_ok({
|
||||
@ -377,6 +380,7 @@ impl HttpClient {
|
||||
Ok(Self {
|
||||
client,
|
||||
server: String::from(server),
|
||||
port,
|
||||
fingerprint: verified_fingerprint,
|
||||
auth,
|
||||
ticket_abort,
|
||||
@ -486,7 +490,7 @@ impl HttpClient {
|
||||
path: &str,
|
||||
data: Option<Value>,
|
||||
) -> Result<Value, Error> {
|
||||
let req = Self::request_builder(&self.server, "GET", path, data).unwrap();
|
||||
let req = Self::request_builder(&self.server, self.port, "GET", path, data)?;
|
||||
self.request(req).await
|
||||
}
|
||||
|
||||
@ -495,7 +499,7 @@ impl HttpClient {
|
||||
path: &str,
|
||||
data: Option<Value>,
|
||||
) -> Result<Value, Error> {
|
||||
let req = Self::request_builder(&self.server, "DELETE", path, data).unwrap();
|
||||
let req = Self::request_builder(&self.server, self.port, "DELETE", path, data)?;
|
||||
self.request(req).await
|
||||
}
|
||||
|
||||
@ -504,7 +508,7 @@ impl HttpClient {
|
||||
path: &str,
|
||||
data: Option<Value>,
|
||||
) -> Result<Value, Error> {
|
||||
let req = Self::request_builder(&self.server, "POST", path, data).unwrap();
|
||||
let req = Self::request_builder(&self.server, self.port, "POST", path, data)?;
|
||||
self.request(req).await
|
||||
}
|
||||
|
||||
@ -513,7 +517,7 @@ impl HttpClient {
|
||||
path: &str,
|
||||
output: &mut (dyn Write + Send),
|
||||
) -> Result<(), Error> {
|
||||
let mut req = Self::request_builder(&self.server, "GET", path, None).unwrap();
|
||||
let mut req = Self::request_builder(&self.server, self.port, "GET", path, None)?;
|
||||
|
||||
let client = self.client.clone();
|
||||
|
||||
@ -549,7 +553,7 @@ impl HttpClient {
|
||||
) -> Result<Value, Error> {
|
||||
|
||||
let path = path.trim_matches('/');
|
||||
let mut url = format!("https://{}:8007/{}", &self.server, path);
|
||||
let mut url = format!("https://{}:{}/{}", &self.server, self.port, path);
|
||||
|
||||
if let Some(data) = data {
|
||||
let query = tools::json_object_to_query(data).unwrap();
|
||||
@ -624,11 +628,12 @@ impl HttpClient {
|
||||
async fn credentials(
|
||||
client: Client<HttpsConnector>,
|
||||
server: String,
|
||||
port: u16,
|
||||
username: Userid,
|
||||
password: String,
|
||||
) -> Result<AuthInfo, Error> {
|
||||
let data = json!({ "username": username, "password": password });
|
||||
let req = Self::request_builder(&server, "POST", "/api2/json/access/ticket", Some(data)).unwrap();
|
||||
let req = Self::request_builder(&server, port, "POST", "/api2/json/access/ticket", Some(data))?;
|
||||
let cred = Self::api_request(client, req).await?;
|
||||
let auth = AuthInfo {
|
||||
userid: cred["data"]["username"].as_str().unwrap().parse()?,
|
||||
@ -672,9 +677,13 @@ impl HttpClient {
|
||||
&self.server
|
||||
}
|
||||
|
||||
pub fn request_builder(server: &str, method: &str, path: &str, data: Option<Value>) -> Result<Request<Body>, Error> {
|
||||
pub fn port(&self) -> u16 {
|
||||
self.port
|
||||
}
|
||||
|
||||
pub fn request_builder(server: &str, port: u16, method: &str, path: &str, data: Option<Value>) -> Result<Request<Body>, Error> {
|
||||
let path = path.trim_matches('/');
|
||||
let url: Uri = format!("https://{}:8007/{}", server, path).parse()?;
|
||||
let url: Uri = format!("https://{}:{}/{}", server, port, path).parse()?;
|
||||
|
||||
if let Some(data) = data {
|
||||
if method == "POST" {
|
||||
@ -687,7 +696,7 @@ impl HttpClient {
|
||||
return Ok(request);
|
||||
} else {
|
||||
let query = tools::json_object_to_query(data)?;
|
||||
let url: Uri = format!("https://{}:8007/{}?{}", server, path, query).parse()?;
|
||||
let url: Uri = format!("https://{}:{}/{}?{}", server, port, path, query).parse()?;
|
||||
let request = Request::builder()
|
||||
.method(method)
|
||||
.uri(url)
|
||||
|
@ -3,13 +3,15 @@
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use serde_json::json;
|
||||
use std::convert::TryFrom;
|
||||
use std::sync::Arc;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::{HashSet, HashMap};
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::time::SystemTime;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use proxmox::api::error::{StatusCode, HttpError};
|
||||
use crate::{
|
||||
tools::compute_file_csum,
|
||||
tools::{ParallelHandler, compute_file_csum},
|
||||
server::WorkerTask,
|
||||
backup::*,
|
||||
api2::types::*,
|
||||
@ -22,27 +24,86 @@ use crate::{
|
||||
// Todo: correctly lock backup groups
|
||||
|
||||
async fn pull_index_chunks<I: IndexFile>(
|
||||
_worker: &WorkerTask,
|
||||
chunk_reader: &mut RemoteChunkReader,
|
||||
worker: &WorkerTask,
|
||||
chunk_reader: RemoteChunkReader,
|
||||
target: Arc<DataStore>,
|
||||
index: I,
|
||||
downloaded_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
use futures::stream::{self, StreamExt, TryStreamExt};
|
||||
|
||||
for pos in 0..index.index_count() {
|
||||
let info = index.chunk_info(pos).unwrap();
|
||||
let chunk_exists = target.cond_touch_chunk(&info.digest, false)?;
|
||||
if chunk_exists {
|
||||
//worker.log(format!("chunk {} exists {}", pos, proxmox::tools::digest_to_hex(digest)));
|
||||
continue;
|
||||
}
|
||||
//worker.log(format!("sync {} chunk {}", pos, proxmox::tools::digest_to_hex(digest)));
|
||||
let chunk = chunk_reader.read_raw_chunk(&info.digest).await?;
|
||||
let start_time = SystemTime::now();
|
||||
|
||||
chunk.verify_unencrypted(info.size() as usize, &info.digest)?;
|
||||
let stream = stream::iter(
|
||||
(0..index.index_count())
|
||||
.map(|pos| index.chunk_info(pos).unwrap())
|
||||
.filter(|info| {
|
||||
let mut guard = downloaded_chunks.lock().unwrap();
|
||||
let done = guard.contains(&info.digest);
|
||||
if !done {
|
||||
// Note: We mark a chunk as downloaded before its actually downloaded
|
||||
// to avoid duplicate downloads.
|
||||
guard.insert(info.digest);
|
||||
}
|
||||
!done
|
||||
})
|
||||
);
|
||||
|
||||
target.insert_chunk(&chunk, &info.digest)?;
|
||||
}
|
||||
let target2 = target.clone();
|
||||
let verify_pool = ParallelHandler::new(
|
||||
"sync chunk writer", 4,
|
||||
move |(chunk, digest, size): (DataBlob, [u8;32], u64)| {
|
||||
// println!("verify and write {}", proxmox::tools::digest_to_hex(&digest));
|
||||
chunk.verify_unencrypted(size as usize, &digest)?;
|
||||
target2.insert_chunk(&chunk, &digest)?;
|
||||
Ok(())
|
||||
}
|
||||
);
|
||||
|
||||
let verify_and_write_channel = verify_pool.channel();
|
||||
|
||||
let bytes = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
stream
|
||||
.map(|info| {
|
||||
|
||||
let target = Arc::clone(&target);
|
||||
let chunk_reader = chunk_reader.clone();
|
||||
let bytes = Arc::clone(&bytes);
|
||||
let verify_and_write_channel = verify_and_write_channel.clone();
|
||||
|
||||
Ok::<_, Error>(async move {
|
||||
let chunk_exists = crate::tools::runtime::block_in_place(|| target.cond_touch_chunk(&info.digest, false))?;
|
||||
if chunk_exists {
|
||||
//worker.log(format!("chunk {} exists {}", pos, proxmox::tools::digest_to_hex(digest)));
|
||||
return Ok::<_, Error>(());
|
||||
}
|
||||
//worker.log(format!("sync {} chunk {}", pos, proxmox::tools::digest_to_hex(digest)));
|
||||
let chunk = chunk_reader.read_raw_chunk(&info.digest).await?;
|
||||
let raw_size = chunk.raw_size() as usize;
|
||||
|
||||
// decode, verify and write in a separate threads to maximize throughput
|
||||
crate::tools::runtime::block_in_place(|| verify_and_write_channel.send((chunk, info.digest, info.size())))?;
|
||||
|
||||
bytes.fetch_add(raw_size, Ordering::SeqCst);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.try_buffer_unordered(20)
|
||||
.try_for_each(|_res| futures::future::ok(()))
|
||||
.await?;
|
||||
|
||||
drop(verify_and_write_channel);
|
||||
|
||||
verify_pool.complete()?;
|
||||
|
||||
let elapsed = start_time.elapsed()?.as_secs_f64();
|
||||
|
||||
let bytes = bytes.load(Ordering::SeqCst);
|
||||
|
||||
worker.log(format!("downloaded {} bytes ({} MiB/s)", bytes, (bytes as f64)/(1024.0*1024.0*elapsed)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -89,6 +150,7 @@ async fn pull_single_archive(
|
||||
tgt_store: Arc<DataStore>,
|
||||
snapshot: &BackupDir,
|
||||
archive_info: &FileInfo,
|
||||
downloaded_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let archive_name = &archive_info.filename;
|
||||
@ -115,7 +177,7 @@ async fn pull_single_archive(
|
||||
let (csum, size) = index.compute_csum();
|
||||
verify_archive(archive_info, &csum, size)?;
|
||||
|
||||
pull_index_chunks(worker, chunk_reader, tgt_store.clone(), index).await?;
|
||||
pull_index_chunks(worker, chunk_reader.clone(), tgt_store.clone(), index, downloaded_chunks).await?;
|
||||
}
|
||||
ArchiveType::FixedIndex => {
|
||||
let index = FixedIndexReader::new(tmpfile)
|
||||
@ -123,7 +185,7 @@ async fn pull_single_archive(
|
||||
let (csum, size) = index.compute_csum();
|
||||
verify_archive(archive_info, &csum, size)?;
|
||||
|
||||
pull_index_chunks(worker, chunk_reader, tgt_store.clone(), index).await?;
|
||||
pull_index_chunks(worker, chunk_reader.clone(), tgt_store.clone(), index, downloaded_chunks).await?;
|
||||
}
|
||||
ArchiveType::Blob => {
|
||||
let (csum, size) = compute_file_csum(&mut tmpfile)?;
|
||||
@ -169,6 +231,7 @@ async fn pull_snapshot(
|
||||
reader: Arc<BackupReader>,
|
||||
tgt_store: Arc<DataStore>,
|
||||
snapshot: &BackupDir,
|
||||
downloaded_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let mut manifest_name = tgt_store.base_path();
|
||||
@ -278,6 +341,7 @@ async fn pull_snapshot(
|
||||
tgt_store.clone(),
|
||||
snapshot,
|
||||
&item,
|
||||
downloaded_chunks.clone(),
|
||||
).await?;
|
||||
}
|
||||
|
||||
@ -300,6 +364,7 @@ pub async fn pull_snapshot_from(
|
||||
reader: Arc<BackupReader>,
|
||||
tgt_store: Arc<DataStore>,
|
||||
snapshot: &BackupDir,
|
||||
downloaded_chunks: Arc<Mutex<HashSet<[u8;32]>>>,
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let (_path, is_new, _snap_lock) = tgt_store.create_locked_backup_dir(&snapshot)?;
|
||||
@ -307,7 +372,7 @@ pub async fn pull_snapshot_from(
|
||||
if is_new {
|
||||
worker.log(format!("sync snapshot {:?}", snapshot.relative_path()));
|
||||
|
||||
if let Err(err) = pull_snapshot(worker, reader, tgt_store.clone(), &snapshot).await {
|
||||
if let Err(err) = pull_snapshot(worker, reader, tgt_store.clone(), &snapshot, downloaded_chunks).await {
|
||||
if let Err(cleanup_err) = tgt_store.remove_backup_dir(&snapshot, true) {
|
||||
worker.log(format!("cleanup error - {}", cleanup_err));
|
||||
}
|
||||
@ -316,7 +381,7 @@ pub async fn pull_snapshot_from(
|
||||
worker.log(format!("sync snapshot {:?} done", snapshot.relative_path()));
|
||||
} else {
|
||||
worker.log(format!("re-sync snapshot {:?}", snapshot.relative_path()));
|
||||
pull_snapshot(worker, reader, tgt_store.clone(), &snapshot).await?;
|
||||
pull_snapshot(worker, reader, tgt_store.clone(), &snapshot, downloaded_chunks).await?;
|
||||
worker.log(format!("re-sync snapshot {:?} done", snapshot.relative_path()));
|
||||
}
|
||||
|
||||
@ -330,6 +395,7 @@ pub async fn pull_group(
|
||||
tgt_store: Arc<DataStore>,
|
||||
group: &BackupGroup,
|
||||
delete: bool,
|
||||
progress: Option<(usize, usize)>, // (groups_done, group_count)
|
||||
) -> Result<(), Error> {
|
||||
|
||||
let path = format!("api2/json/admin/datastore/{}/snapshots", src_repo.store());
|
||||
@ -351,7 +417,20 @@ pub async fn pull_group(
|
||||
|
||||
let mut remote_snapshots = std::collections::HashSet::new();
|
||||
|
||||
for item in list {
|
||||
let (per_start, per_group) = if let Some((groups_done, group_count)) = progress {
|
||||
let per_start = (groups_done as f64)/(group_count as f64);
|
||||
let per_group = 1.0/(group_count as f64);
|
||||
(per_start, per_group)
|
||||
} else {
|
||||
(0.0, 1.0)
|
||||
};
|
||||
|
||||
// start with 16384 chunks (up to 65GB)
|
||||
let downloaded_chunks = Arc::new(Mutex::new(HashSet::with_capacity(1024*64)));
|
||||
|
||||
let snapshot_count = list.len();
|
||||
|
||||
for (pos, item) in list.into_iter().enumerate() {
|
||||
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time)?;
|
||||
|
||||
// in-progress backups can't be synced
|
||||
@ -372,7 +451,7 @@ pub async fn pull_group(
|
||||
.password(Some(auth_info.ticket.clone()))
|
||||
.fingerprint(fingerprint.clone());
|
||||
|
||||
let new_client = HttpClient::new(src_repo.host(), src_repo.user(), options)?;
|
||||
let new_client = HttpClient::new(src_repo.host(), src_repo.port(), src_repo.user(), options)?;
|
||||
|
||||
let reader = BackupReader::start(
|
||||
new_client,
|
||||
@ -384,7 +463,13 @@ pub async fn pull_group(
|
||||
true,
|
||||
).await?;
|
||||
|
||||
pull_snapshot_from(worker, reader, tgt_store.clone(), &snapshot).await?;
|
||||
let result = pull_snapshot_from(worker, reader, tgt_store.clone(), &snapshot, downloaded_chunks.clone()).await;
|
||||
|
||||
let percentage = (pos as f64)/(snapshot_count as f64);
|
||||
let percentage = per_start + percentage*per_group;
|
||||
worker.log(format!("percentage done: {:.2}%", percentage*100.0));
|
||||
|
||||
result?; // stop on error
|
||||
}
|
||||
|
||||
if delete {
|
||||
@ -434,6 +519,9 @@ pub async fn pull_store(
|
||||
new_groups.insert(BackupGroup::new(&item.backup_type, &item.backup_id));
|
||||
}
|
||||
|
||||
let group_count = list.len();
|
||||
let mut groups_done = 0;
|
||||
|
||||
for item in list {
|
||||
let group = BackupGroup::new(&item.backup_type, &item.backup_id);
|
||||
|
||||
@ -442,15 +530,24 @@ pub async fn pull_store(
|
||||
if userid != owner { // only the owner is allowed to create additional snapshots
|
||||
worker.log(format!("sync group {}/{} failed - owner check failed ({} != {})",
|
||||
item.backup_type, item.backup_id, userid, owner));
|
||||
errors = true;
|
||||
continue; // do not stop here, instead continue
|
||||
}
|
||||
errors = true; // do not stop here, instead continue
|
||||
|
||||
if let Err(err) = pull_group(worker, client, src_repo, tgt_store.clone(), &group, delete).await {
|
||||
worker.log(format!("sync group {}/{} failed - {}", item.backup_type, item.backup_id, err));
|
||||
errors = true;
|
||||
continue; // do not stop here, instead continue
|
||||
} else {
|
||||
|
||||
if let Err(err) = pull_group(
|
||||
worker,
|
||||
client,
|
||||
src_repo,
|
||||
tgt_store.clone(),
|
||||
&group,
|
||||
delete,
|
||||
Some((groups_done, group_count)),
|
||||
).await {
|
||||
worker.log(format!("sync group {}/{} failed - {}", item.backup_type, item.backup_id, err));
|
||||
errors = true; // do not stop here, instead continue
|
||||
}
|
||||
}
|
||||
groups_done += 1;
|
||||
}
|
||||
|
||||
if delete {
|
||||
|
@ -15,7 +15,7 @@ pub struct RemoteChunkReader {
|
||||
client: Arc<BackupReader>,
|
||||
crypt_config: Option<Arc<CryptConfig>>,
|
||||
crypt_mode: CryptMode,
|
||||
cache_hint: HashMap<[u8; 32], usize>,
|
||||
cache_hint: Arc<HashMap<[u8; 32], usize>>,
|
||||
cache: Arc<Mutex<HashMap<[u8; 32], Vec<u8>>>>,
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ impl RemoteChunkReader {
|
||||
client,
|
||||
crypt_config,
|
||||
crypt_mode,
|
||||
cache_hint,
|
||||
cache_hint: Arc::new(cache_hint),
|
||||
cache: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
@ -97,7 +97,7 @@ where
|
||||
{
|
||||
let mut path = path.as_ref().to_path_buf();
|
||||
path.set_extension("lck");
|
||||
let lock = open_file_locked(&path, Duration::new(10, 0))?;
|
||||
let lock = open_file_locked(&path, Duration::new(10, 0), true)?;
|
||||
let backup_user = crate::backup::backup_user()?;
|
||||
nix::unistd::chown(&path, Some(backup_user.uid), Some(backup_user.gid))?;
|
||||
Ok(lock)
|
||||
|
@ -149,7 +149,7 @@ pub fn compute_file_diff(filename: &str, shadow: &str) -> Result<String, Error>
|
||||
.output()
|
||||
.map_err(|err| format_err!("failed to execute diff - {}", err))?;
|
||||
|
||||
let diff = crate::tools::command_output(output, Some(|c| c == 0 || c == 1))
|
||||
let diff = crate::tools::command_output_as_string(output, Some(|c| c == 0 || c == 1))
|
||||
.map_err(|err| format_err!("diff failed: {}", err))?;
|
||||
|
||||
Ok(diff)
|
||||
|
@ -39,6 +39,11 @@ pub const REMOTE_PASSWORD_SCHEMA: Schema = StringSchema::new("Password or auth t
|
||||
host: {
|
||||
schema: DNS_NAME_OR_IP_SCHEMA,
|
||||
},
|
||||
port: {
|
||||
optional: true,
|
||||
description: "The (optional) port",
|
||||
type: u16,
|
||||
},
|
||||
userid: {
|
||||
type: Userid,
|
||||
},
|
||||
@ -58,6 +63,8 @@ pub struct Remote {
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub comment: Option<String>,
|
||||
pub host: String,
|
||||
#[serde(skip_serializing_if="Option::is_none")]
|
||||
pub port: Option<u16>,
|
||||
pub userid: Userid,
|
||||
#[serde(skip_serializing_if="String::is_empty")]
|
||||
#[serde(with = "proxmox::tools::serde::string_as_base64")]
|
||||
|
@ -17,17 +17,3 @@ pub trait BackupCatalogWriter {
|
||||
fn add_fifo(&mut self, name: &CStr) -> Result<(), Error>;
|
||||
fn add_socket(&mut self, name: &CStr) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
pub struct DummyCatalogWriter();
|
||||
|
||||
impl BackupCatalogWriter for DummyCatalogWriter {
|
||||
fn start_directory(&mut self, _name: &CStr) -> Result<(), Error> { Ok(()) }
|
||||
fn end_directory(&mut self) -> Result<(), Error> { Ok(()) }
|
||||
fn add_file(&mut self, _name: &CStr, _size: u64, _mtime: u64) -> Result<(), Error> { Ok(()) }
|
||||
fn add_symlink(&mut self, _name: &CStr) -> Result<(), Error> { Ok(()) }
|
||||
fn add_hardlink(&mut self, _name: &CStr) -> Result<(), Error> { Ok(()) }
|
||||
fn add_block_device(&mut self, _name: &CStr) -> Result<(), Error> { Ok(()) }
|
||||
fn add_char_device(&mut self, _name: &CStr) -> Result<(), Error> { Ok(()) }
|
||||
fn add_fifo(&mut self, _name: &CStr) -> Result<(), Error> { Ok(()) }
|
||||
fn add_socket(&mut self, _name: &CStr) -> Result<(), Error> { Ok(()) }
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ impl RRA {
|
||||
|
||||
let min_time = epoch - (RRD_DATA_ENTRIES as u64)*reso;
|
||||
let min_time = (min_time/reso + 1)*reso;
|
||||
let mut t = last_update - (RRD_DATA_ENTRIES as u64)*reso;
|
||||
let mut t = last_update.saturating_sub((RRD_DATA_ENTRIES as u64)*reso);
|
||||
let mut index = ((t/reso) % (RRD_DATA_ENTRIES as u64)) as usize;
|
||||
for _ in 0..RRD_DATA_ENTRIES {
|
||||
t += reso; index = (index + 1) % RRD_DATA_ENTRIES;
|
||||
|
@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::fs::File;
|
||||
use std::io::{Read, BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use std::io::{Read, Write, BufRead, BufReader};
|
||||
use std::panic::UnwindSafe;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@ -19,6 +20,7 @@ use proxmox::tools::fs::{create_path, open_file_locked, replace_file, CreateOpti
|
||||
|
||||
use super::UPID;
|
||||
|
||||
use crate::tools::logrotate::{LogRotate, LogRotateFiles};
|
||||
use crate::tools::FileLogger;
|
||||
use crate::api2::types::Userid;
|
||||
|
||||
@ -31,6 +33,10 @@ pub const PROXMOX_BACKUP_LOG_DIR: &str = PROXMOX_BACKUP_LOG_DIR_M!();
|
||||
pub const PROXMOX_BACKUP_TASK_DIR: &str = PROXMOX_BACKUP_TASK_DIR_M!();
|
||||
pub const PROXMOX_BACKUP_TASK_LOCK_FN: &str = concat!(PROXMOX_BACKUP_TASK_DIR_M!(), "/.active.lock");
|
||||
pub const PROXMOX_BACKUP_ACTIVE_TASK_FN: &str = concat!(PROXMOX_BACKUP_TASK_DIR_M!(), "/active");
|
||||
pub const PROXMOX_BACKUP_INDEX_TASK_FN: &str = concat!(PROXMOX_BACKUP_TASK_DIR_M!(), "/index");
|
||||
pub const PROXMOX_BACKUP_ARCHIVE_TASK_FN: &str = concat!(PROXMOX_BACKUP_TASK_DIR_M!(), "/archive");
|
||||
|
||||
const MAX_INDEX_TASKS: usize = 1000;
|
||||
|
||||
lazy_static! {
|
||||
static ref WORKER_TASK_LIST: Mutex<HashMap<usize, Arc<WorkerTask>>> = Mutex::new(HashMap::new());
|
||||
@ -325,86 +331,85 @@ pub struct TaskListInfo {
|
||||
pub state: Option<TaskState>, // endtime, status
|
||||
}
|
||||
|
||||
fn lock_task_list_files(exclusive: bool) -> Result<std::fs::File, Error> {
|
||||
let backup_user = crate::backup::backup_user()?;
|
||||
|
||||
let lock = open_file_locked(PROXMOX_BACKUP_TASK_LOCK_FN, std::time::Duration::new(10, 0), exclusive)?;
|
||||
nix::unistd::chown(PROXMOX_BACKUP_TASK_LOCK_FN, Some(backup_user.uid), Some(backup_user.gid))?;
|
||||
|
||||
Ok(lock)
|
||||
}
|
||||
|
||||
/// checks if the Task Archive is bigger that 'size_threshold' bytes, and
|
||||
/// rotates it if it is
|
||||
pub fn rotate_task_log_archive(size_threshold: u64, compress: bool, max_files: Option<usize>) -> Result<bool, Error> {
|
||||
let _lock = lock_task_list_files(true)?;
|
||||
let path = Path::new(PROXMOX_BACKUP_ARCHIVE_TASK_FN);
|
||||
let metadata = path.metadata()?;
|
||||
if metadata.len() > size_threshold {
|
||||
let mut logrotate = LogRotate::new(PROXMOX_BACKUP_ARCHIVE_TASK_FN, compress).ok_or_else(|| format_err!("could not get archive file names"))?;
|
||||
let backup_user = crate::backup::backup_user()?;
|
||||
logrotate.rotate(
|
||||
CreateOptions::new()
|
||||
.owner(backup_user.uid)
|
||||
.group(backup_user.gid),
|
||||
max_files,
|
||||
)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
// atomically read/update the task list, update status of finished tasks
|
||||
// new_upid is added to the list when specified.
|
||||
// Returns a sorted list of known tasks,
|
||||
fn update_active_workers(new_upid: Option<&UPID>) -> Result<Vec<TaskListInfo>, Error> {
|
||||
fn update_active_workers(new_upid: Option<&UPID>) -> Result<(), Error> {
|
||||
|
||||
let backup_user = crate::backup::backup_user()?;
|
||||
|
||||
let lock = open_file_locked(PROXMOX_BACKUP_TASK_LOCK_FN, std::time::Duration::new(10, 0))?;
|
||||
nix::unistd::chown(PROXMOX_BACKUP_TASK_LOCK_FN, Some(backup_user.uid), Some(backup_user.gid))?;
|
||||
let lock = lock_task_list_files(true)?;
|
||||
|
||||
let reader = match File::open(PROXMOX_BACKUP_ACTIVE_TASK_FN) {
|
||||
Ok(f) => Some(BufReader::new(f)),
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
None
|
||||
} else {
|
||||
bail!("unable to open active worker {:?} - {}", PROXMOX_BACKUP_ACTIVE_TASK_FN, err);
|
||||
let mut finish_list: Vec<TaskListInfo> = read_task_file_from_path(PROXMOX_BACKUP_INDEX_TASK_FN)?;
|
||||
let mut active_list: Vec<TaskListInfo> = read_task_file_from_path(PROXMOX_BACKUP_ACTIVE_TASK_FN)?
|
||||
.into_iter()
|
||||
.filter_map(|info| {
|
||||
if info.state.is_some() {
|
||||
// this can happen when the active file still includes finished tasks
|
||||
finish_list.push(info);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut active_list = vec![];
|
||||
let mut finish_list = vec![];
|
||||
|
||||
if let Some(lines) = reader.map(|r| r.lines()) {
|
||||
|
||||
for line in lines {
|
||||
let line = line?;
|
||||
match parse_worker_status_line(&line) {
|
||||
Err(err) => bail!("unable to parse active worker status '{}' - {}", line, err),
|
||||
Ok((upid_str, upid, state)) => match state {
|
||||
None if worker_is_active_local(&upid) => {
|
||||
active_list.push(TaskListInfo { upid, upid_str, state: None });
|
||||
},
|
||||
None => {
|
||||
println!("Detected stopped UPID {}", upid_str);
|
||||
let now = proxmox::tools::time::epoch_i64();
|
||||
let status = upid_read_status(&upid)
|
||||
.unwrap_or_else(|_| TaskState::Unknown { endtime: now });
|
||||
finish_list.push(TaskListInfo {
|
||||
upid, upid_str, state: Some(status)
|
||||
});
|
||||
},
|
||||
Some(status) => {
|
||||
finish_list.push(TaskListInfo {
|
||||
upid, upid_str, state: Some(status)
|
||||
})
|
||||
}
|
||||
}
|
||||
if !worker_is_active_local(&info.upid) {
|
||||
println!("Detected stopped UPID {}", &info.upid_str);
|
||||
let now = proxmox::tools::time::epoch_i64();
|
||||
let status = upid_read_status(&info.upid)
|
||||
.unwrap_or_else(|_| TaskState::Unknown { endtime: now });
|
||||
finish_list.push(TaskListInfo {
|
||||
upid: info.upid,
|
||||
upid_str: info.upid_str,
|
||||
state: Some(status)
|
||||
});
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(info)
|
||||
}).collect();
|
||||
|
||||
if let Some(upid) = new_upid {
|
||||
active_list.push(TaskListInfo { upid: upid.clone(), upid_str: upid.to_string(), state: None });
|
||||
}
|
||||
|
||||
// assemble list without duplicates
|
||||
// we include all active tasks,
|
||||
// and fill up to 1000 entries with finished tasks
|
||||
let active_raw = render_task_list(&active_list);
|
||||
|
||||
let max = 1000;
|
||||
replace_file(
|
||||
PROXMOX_BACKUP_ACTIVE_TASK_FN,
|
||||
active_raw.as_bytes(),
|
||||
CreateOptions::new()
|
||||
.owner(backup_user.uid)
|
||||
.group(backup_user.gid),
|
||||
)?;
|
||||
|
||||
let mut task_hash = HashMap::new();
|
||||
|
||||
for info in active_list {
|
||||
task_hash.insert(info.upid_str.clone(), info);
|
||||
}
|
||||
|
||||
for info in finish_list {
|
||||
if task_hash.len() > max { break; }
|
||||
if !task_hash.contains_key(&info.upid_str) {
|
||||
task_hash.insert(info.upid_str.clone(), info);
|
||||
}
|
||||
}
|
||||
|
||||
let mut task_list: Vec<TaskListInfo> = vec![];
|
||||
for (_, info) in task_hash { task_list.push(info); }
|
||||
|
||||
task_list.sort_unstable_by(|b, a| { // lastest on top
|
||||
finish_list.sort_unstable_by(|a, b| {
|
||||
match (&a.state, &b.state) {
|
||||
(Some(s1), Some(s2)) => s1.cmp(&s2),
|
||||
(Some(_), None) => std::cmp::Ordering::Less,
|
||||
@ -413,34 +418,198 @@ fn update_active_workers(new_upid: Option<&UPID>) -> Result<Vec<TaskListInfo>, E
|
||||
}
|
||||
});
|
||||
|
||||
let mut raw = String::new();
|
||||
for info in &task_list {
|
||||
if let Some(status) = &info.state {
|
||||
raw.push_str(&format!("{} {:08X} {}\n", info.upid_str, status.endtime(), status));
|
||||
} else {
|
||||
raw.push_str(&info.upid_str);
|
||||
raw.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let start = if finish_list.len() > MAX_INDEX_TASKS {
|
||||
finish_list.len() - MAX_INDEX_TASKS
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let end = (start+MAX_INDEX_TASKS).min(finish_list.len());
|
||||
|
||||
let index_raw = if end > start {
|
||||
render_task_list(&finish_list[start..end])
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
replace_file(
|
||||
PROXMOX_BACKUP_ACTIVE_TASK_FN,
|
||||
raw.as_bytes(),
|
||||
PROXMOX_BACKUP_INDEX_TASK_FN,
|
||||
index_raw.as_bytes(),
|
||||
CreateOptions::new()
|
||||
.owner(backup_user.uid)
|
||||
.group(backup_user.gid),
|
||||
)?;
|
||||
|
||||
if !finish_list.is_empty() && start > 0 {
|
||||
match std::fs::OpenOptions::new().append(true).create(true).open(PROXMOX_BACKUP_ARCHIVE_TASK_FN) {
|
||||
Ok(mut writer) => {
|
||||
for info in &finish_list[0..start] {
|
||||
writer.write_all(render_task_line(&info).as_bytes())?;
|
||||
}
|
||||
},
|
||||
Err(err) => bail!("could not write task archive - {}", err),
|
||||
}
|
||||
|
||||
nix::unistd::chown(PROXMOX_BACKUP_ARCHIVE_TASK_FN, Some(backup_user.uid), Some(backup_user.gid))?;
|
||||
}
|
||||
|
||||
drop(lock);
|
||||
|
||||
Ok(task_list)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns a sorted list of known tasks
|
||||
///
|
||||
/// The list is sorted by `(starttime, endtime)` in ascending order
|
||||
pub fn read_task_list() -> Result<Vec<TaskListInfo>, Error> {
|
||||
update_active_workers(None)
|
||||
fn render_task_line(info: &TaskListInfo) -> String {
|
||||
let mut raw = String::new();
|
||||
if let Some(status) = &info.state {
|
||||
raw.push_str(&format!("{} {:08X} {}\n", info.upid_str, status.endtime(), status));
|
||||
} else {
|
||||
raw.push_str(&info.upid_str);
|
||||
raw.push('\n');
|
||||
}
|
||||
|
||||
raw
|
||||
}
|
||||
|
||||
fn render_task_list(list: &[TaskListInfo]) -> String {
|
||||
let mut raw = String::new();
|
||||
for info in list {
|
||||
raw.push_str(&render_task_line(&info));
|
||||
}
|
||||
raw
|
||||
}
|
||||
|
||||
// note this is not locked, caller has to make sure it is
|
||||
// this will skip (and log) lines that are not valid status lines
|
||||
fn read_task_file<R: Read>(reader: R) -> Result<Vec<TaskListInfo>, Error>
|
||||
{
|
||||
let reader = BufReader::new(reader);
|
||||
let mut list = Vec::new();
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
match parse_worker_status_line(&line) {
|
||||
Ok((upid_str, upid, state)) => list.push(TaskListInfo {
|
||||
upid_str,
|
||||
upid,
|
||||
state
|
||||
}),
|
||||
Err(err) => {
|
||||
eprintln!("unable to parse worker status '{}' - {}", line, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
// note this is not locked, caller has to make sure it is
|
||||
fn read_task_file_from_path<P>(path: P) -> Result<Vec<TaskListInfo>, Error>
|
||||
where
|
||||
P: AsRef<std::path::Path> + std::fmt::Debug,
|
||||
{
|
||||
let file = match File::open(&path) {
|
||||
Ok(f) => f,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
|
||||
Err(err) => bail!("unable to open task list {:?} - {}", path, err),
|
||||
};
|
||||
|
||||
read_task_file(file)
|
||||
}
|
||||
|
||||
enum TaskFile {
|
||||
Active,
|
||||
Index,
|
||||
Archive,
|
||||
End,
|
||||
}
|
||||
|
||||
pub struct TaskListInfoIterator {
|
||||
list: VecDeque<TaskListInfo>,
|
||||
file: TaskFile,
|
||||
archive: Option<LogRotateFiles>,
|
||||
lock: Option<File>,
|
||||
}
|
||||
|
||||
impl TaskListInfoIterator {
|
||||
pub fn new(active_only: bool) -> Result<Self, Error> {
|
||||
let (read_lock, active_list) = {
|
||||
let lock = lock_task_list_files(false)?;
|
||||
let active_list = read_task_file_from_path(PROXMOX_BACKUP_ACTIVE_TASK_FN)?;
|
||||
|
||||
let needs_update = active_list
|
||||
.iter()
|
||||
.any(|info| info.state.is_some() || !worker_is_active_local(&info.upid));
|
||||
|
||||
if needs_update {
|
||||
drop(lock);
|
||||
update_active_workers(None)?;
|
||||
let lock = lock_task_list_files(false)?;
|
||||
let active_list = read_task_file_from_path(PROXMOX_BACKUP_ACTIVE_TASK_FN)?;
|
||||
(lock, active_list)
|
||||
} else {
|
||||
(lock, active_list)
|
||||
}
|
||||
};
|
||||
|
||||
let archive = if active_only {
|
||||
None
|
||||
} else {
|
||||
let logrotate = LogRotate::new(PROXMOX_BACKUP_ARCHIVE_TASK_FN, true).ok_or_else(|| format_err!("could not get archive file names"))?;
|
||||
Some(logrotate.files())
|
||||
};
|
||||
|
||||
let file = if active_only { TaskFile::End } else { TaskFile::Active };
|
||||
let lock = if active_only { None } else { Some(read_lock) };
|
||||
|
||||
Ok(Self {
|
||||
list: active_list.into(),
|
||||
file,
|
||||
archive,
|
||||
lock,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for TaskListInfoIterator {
|
||||
type Item = Result<TaskListInfo, Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
if let Some(element) = self.list.pop_back() {
|
||||
return Some(Ok(element));
|
||||
} else {
|
||||
match self.file {
|
||||
TaskFile::Active => {
|
||||
let index = match read_task_file_from_path(PROXMOX_BACKUP_INDEX_TASK_FN) {
|
||||
Ok(index) => index,
|
||||
Err(err) => return Some(Err(err)),
|
||||
};
|
||||
self.list.append(&mut index.into());
|
||||
self.file = TaskFile::Index;
|
||||
},
|
||||
TaskFile::Index | TaskFile::Archive => {
|
||||
if let Some(mut archive) = self.archive.take() {
|
||||
if let Some(file) = archive.next() {
|
||||
let list = match read_task_file(file) {
|
||||
Ok(list) => list,
|
||||
Err(err) => return Some(Err(err)),
|
||||
};
|
||||
self.list.append(&mut list.into());
|
||||
self.archive = Some(archive);
|
||||
self.file = TaskFile::Archive;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
self.file = TaskFile::End;
|
||||
self.lock.take();
|
||||
return None;
|
||||
}
|
||||
TaskFile::End => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch long running worker tasks.
|
||||
|
21
src/tools.rs
21
src/tools.rs
@ -32,6 +32,10 @@ pub mod ticket;
|
||||
pub mod statistics;
|
||||
pub mod systemd;
|
||||
pub mod nom;
|
||||
pub mod logrotate;
|
||||
|
||||
mod parallel_handler;
|
||||
pub use parallel_handler::*;
|
||||
|
||||
mod wrapped_reader_stream;
|
||||
pub use wrapped_reader_stream::*;
|
||||
@ -401,7 +405,7 @@ pub fn normalize_uri_path(path: &str) -> Result<(String, Vec<&str>), Error> {
|
||||
pub fn command_output(
|
||||
output: std::process::Output,
|
||||
exit_code_check: Option<fn(i32) -> bool>,
|
||||
) -> Result<String, Error> {
|
||||
) -> Result<Vec<u8>, Error> {
|
||||
|
||||
if !output.status.success() {
|
||||
match output.status.code() {
|
||||
@ -422,8 +426,19 @@ pub fn command_output(
|
||||
}
|
||||
}
|
||||
|
||||
let output = String::from_utf8(output.stdout)?;
|
||||
Ok(output.stdout)
|
||||
}
|
||||
|
||||
/// Helper to check result from std::process::Command output, returns String.
|
||||
///
|
||||
/// The exit_code_check() function should return true if the exit code
|
||||
/// is considered successful.
|
||||
pub fn command_output_as_string(
|
||||
output: std::process::Output,
|
||||
exit_code_check: Option<fn(i32) -> bool>,
|
||||
) -> Result<String, Error> {
|
||||
let output = command_output(output, exit_code_check)?;
|
||||
let output = String::from_utf8(output)?;
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
@ -435,7 +450,7 @@ pub fn run_command(
|
||||
let output = command.output()
|
||||
.map_err(|err| format_err!("failed to execute {:?} - {}", command, err))?;
|
||||
|
||||
let output = crate::tools::command_output(output, exit_code_check)
|
||||
let output = crate::tools::command_output_as_string(output, exit_code_check)
|
||||
.map_err(|err| format_err!("command {:?} failed - {}", command, err))?;
|
||||
|
||||
Ok(output)
|
||||
|
184
src/tools/logrotate.rs
Normal file
184
src/tools/logrotate.rs
Normal file
@ -0,0 +1,184 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{File, rename};
|
||||
use std::os::unix::io::FromRawFd;
|
||||
use std::io::Read;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
use nix::unistd;
|
||||
|
||||
use proxmox::tools::fs::{CreateOptions, make_tmp_file, replace_file};
|
||||
|
||||
/// Used for rotating log files and iterating over them
|
||||
pub struct LogRotate {
|
||||
base_path: PathBuf,
|
||||
compress: bool,
|
||||
}
|
||||
|
||||
impl LogRotate {
|
||||
/// Creates a new instance if the path given is a valid file name
|
||||
/// (iow. does not end with ..)
|
||||
/// 'compress' decides if compresses files will be created on
|
||||
/// rotation, and if it will search '.zst' files when iterating
|
||||
pub fn new<P: AsRef<Path>>(path: P, compress: bool) -> Option<Self> {
|
||||
if path.as_ref().file_name().is_some() {
|
||||
Some(Self {
|
||||
base_path: path.as_ref().to_path_buf(),
|
||||
compress,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the logrotated file names that exist
|
||||
pub fn file_names(&self) -> LogRotateFileNames {
|
||||
LogRotateFileNames {
|
||||
base_path: self.base_path.clone(),
|
||||
count: 0,
|
||||
compress: self.compress
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over the logrotated file handles
|
||||
pub fn files(&self) -> LogRotateFiles {
|
||||
LogRotateFiles {
|
||||
file_names: self.file_names(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotates the files up to 'max_files'
|
||||
/// if the 'compress' option was given it will compress the newest file
|
||||
///
|
||||
/// e.g. rotates
|
||||
/// foo.2.zst => foo.3.zst
|
||||
/// foo.1.zst => foo.2.zst
|
||||
/// foo => foo.1.zst
|
||||
/// => foo
|
||||
pub fn rotate(&mut self, options: CreateOptions, max_files: Option<usize>) -> Result<(), Error> {
|
||||
let mut filenames: Vec<PathBuf> = self.file_names().collect();
|
||||
if filenames.is_empty() {
|
||||
return Ok(()); // no file means nothing to rotate
|
||||
}
|
||||
|
||||
let mut next_filename = self.base_path.clone().canonicalize()?.into_os_string();
|
||||
|
||||
if self.compress {
|
||||
next_filename.push(format!(".{}.zst", filenames.len()));
|
||||
} else {
|
||||
next_filename.push(format!(".{}", filenames.len()));
|
||||
}
|
||||
|
||||
filenames.push(PathBuf::from(next_filename));
|
||||
let count = filenames.len();
|
||||
|
||||
// rotate all but the first, that we maybe have to compress
|
||||
for i in (1..count-1).rev() {
|
||||
rename(&filenames[i], &filenames[i+1])?;
|
||||
}
|
||||
|
||||
if self.compress {
|
||||
let mut source = File::open(&filenames[0])?;
|
||||
let (fd, tmp_path) = make_tmp_file(&filenames[1], options.clone())?;
|
||||
let target = unsafe { File::from_raw_fd(fd) };
|
||||
let mut encoder = match zstd::stream::write::Encoder::new(target, 0) {
|
||||
Ok(encoder) => encoder,
|
||||
Err(err) => {
|
||||
let _ = unistd::unlink(&tmp_path);
|
||||
bail!("creating zstd encoder failed - {}", err);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = std::io::copy(&mut source, &mut encoder) {
|
||||
let _ = unistd::unlink(&tmp_path);
|
||||
bail!("zstd encoding failed for file {:?} - {}", &filenames[1], err);
|
||||
}
|
||||
|
||||
if let Err(err) = encoder.finish() {
|
||||
let _ = unistd::unlink(&tmp_path);
|
||||
bail!("zstd finish failed for file {:?} - {}", &filenames[1], err);
|
||||
}
|
||||
|
||||
if let Err(err) = rename(&tmp_path, &filenames[1]) {
|
||||
let _ = unistd::unlink(&tmp_path);
|
||||
bail!("rename failed for file {:?} - {}", &filenames[1], err);
|
||||
}
|
||||
|
||||
unistd::unlink(&filenames[0])?;
|
||||
} else {
|
||||
rename(&filenames[0], &filenames[1])?;
|
||||
}
|
||||
|
||||
// create empty original file
|
||||
replace_file(&filenames[0], b"", options)?;
|
||||
|
||||
if let Some(max_files) = max_files {
|
||||
// delete all files > max_files
|
||||
for file in filenames.iter().skip(max_files) {
|
||||
if let Err(err) = unistd::unlink(file) {
|
||||
eprintln!("could not remove {:?}: {}", &file, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over logrotated file names
|
||||
pub struct LogRotateFileNames {
|
||||
base_path: PathBuf,
|
||||
count: usize,
|
||||
compress: bool,
|
||||
}
|
||||
|
||||
impl Iterator for LogRotateFileNames {
|
||||
type Item = PathBuf;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.count > 0 {
|
||||
let mut path: std::ffi::OsString = self.base_path.clone().into();
|
||||
|
||||
path.push(format!(".{}", self.count));
|
||||
self.count += 1;
|
||||
|
||||
if Path::new(&path).is_file() {
|
||||
Some(path.into())
|
||||
} else if self.compress {
|
||||
path.push(".zst");
|
||||
if Path::new(&path).is_file() {
|
||||
Some(path.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if self.base_path.is_file() {
|
||||
self.count += 1;
|
||||
Some(self.base_path.to_path_buf())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over logrotated files by returning a boxed reader
|
||||
pub struct LogRotateFiles {
|
||||
file_names: LogRotateFileNames,
|
||||
}
|
||||
|
||||
impl Iterator for LogRotateFiles {
|
||||
type Item = Box<dyn Read + Send>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let filename = self.file_names.next()?;
|
||||
let file = File::open(&filename).ok()?;
|
||||
|
||||
if filename.extension().unwrap_or(std::ffi::OsStr::new("")) == "zst" {
|
||||
let encoder = zstd::stream::read::Decoder::new(file).ok()?;
|
||||
return Some(Box::new(encoder));
|
||||
}
|
||||
|
||||
Some(Box::new(file))
|
||||
}
|
||||
}
|
166
src/tools/parallel_handler.rs
Normal file
166
src/tools/parallel_handler.rs
Normal file
@ -0,0 +1,166 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use anyhow::{bail, format_err, Error};
|
||||
use crossbeam_channel::{bounded, Sender};
|
||||
|
||||
/// A handle to send data to the worker thread (implements clone)
|
||||
pub struct SendHandle<I> {
|
||||
input: Sender<I>,
|
||||
abort: Arc<Mutex<Option<String>>>,
|
||||
}
|
||||
|
||||
/// Returns the first error happened, if any
|
||||
pub fn check_abort(abort: &Mutex<Option<String>>) -> Result<(), Error> {
|
||||
let guard = abort.lock().unwrap();
|
||||
if let Some(err_msg) = &*guard {
|
||||
return Err(format_err!("{}", err_msg));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl<I: Send> SendHandle<I> {
|
||||
/// Send data to the worker threads
|
||||
pub fn send(&self, input: I) -> Result<(), Error> {
|
||||
check_abort(&self.abort)?;
|
||||
match self.input.send(input) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(_) => bail!("send failed - channel closed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread pool which run the supplied closure
|
||||
///
|
||||
/// The send command sends data to the worker threads. If one handler
|
||||
/// returns an error, we mark the channel as failed and it is no
|
||||
/// longer possible to send data.
|
||||
///
|
||||
/// When done, the 'complete()' method needs to be called to check for
|
||||
/// outstanding errors.
|
||||
pub struct ParallelHandler<I> {
|
||||
handles: Vec<JoinHandle<()>>,
|
||||
name: String,
|
||||
input: Option<SendHandle<I>>,
|
||||
}
|
||||
|
||||
impl<I> Clone for SendHandle<I> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
input: self.input.clone(),
|
||||
abort: Arc::clone(&self.abort),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Send + 'static> ParallelHandler<I> {
|
||||
/// Create a new thread pool, each thread processing incoming data
|
||||
/// with 'handler_fn'.
|
||||
pub fn new<F>(name: &str, threads: usize, handler_fn: F) -> Self
|
||||
where F: Fn(I) -> Result<(), Error> + Send + Clone + 'static,
|
||||
{
|
||||
let mut handles = Vec::new();
|
||||
let (input_tx, input_rx) = bounded::<I>(threads);
|
||||
|
||||
let abort = Arc::new(Mutex::new(None));
|
||||
|
||||
for i in 0..threads {
|
||||
let input_rx = input_rx.clone();
|
||||
let abort = Arc::clone(&abort);
|
||||
let handler_fn = handler_fn.clone();
|
||||
|
||||
handles.push(
|
||||
std::thread::Builder::new()
|
||||
.name(format!("{} ({})", name, i))
|
||||
.spawn(move || loop {
|
||||
let data = match input_rx.recv() {
|
||||
Ok(data) => data,
|
||||
Err(_) => return,
|
||||
};
|
||||
match (handler_fn)(data) {
|
||||
Ok(()) => (),
|
||||
Err(err) => {
|
||||
let mut guard = abort.lock().unwrap();
|
||||
if guard.is_none() {
|
||||
*guard = Some(err.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
Self {
|
||||
handles,
|
||||
name: name.to_string(),
|
||||
input: Some(SendHandle {
|
||||
input: input_tx,
|
||||
abort,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a cloneable channel to send data to the worker threads
|
||||
pub fn channel(&self) -> SendHandle<I> {
|
||||
self.input.as_ref().unwrap().clone()
|
||||
}
|
||||
|
||||
/// Send data to the worker threads
|
||||
pub fn send(&self, input: I) -> Result<(), Error> {
|
||||
self.input.as_ref().unwrap().send(input)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Wait for worker threads to complete and check for errors
|
||||
pub fn complete(mut self) -> Result<(), Error> {
|
||||
let input = self.input.take().unwrap();
|
||||
let abort = Arc::clone(&input.abort);
|
||||
check_abort(&abort)?;
|
||||
drop(input);
|
||||
|
||||
let msg_list = self.join_threads();
|
||||
|
||||
// an error might be encountered while waiting for the join
|
||||
check_abort(&abort)?;
|
||||
|
||||
if msg_list.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(format_err!("{}", msg_list.join("\n")))
|
||||
}
|
||||
|
||||
fn join_threads(&mut self) -> Vec<String> {
|
||||
|
||||
let mut msg_list = Vec::new();
|
||||
|
||||
let mut i = 0;
|
||||
loop {
|
||||
let handle = match self.handles.pop() {
|
||||
Some(handle) => handle,
|
||||
None => break,
|
||||
};
|
||||
if let Err(panic) = handle.join() {
|
||||
match panic.downcast::<&str>() {
|
||||
Ok(panic_msg) => msg_list.push(
|
||||
format!("thread {} ({}) paniced: {}", self.name, i, panic_msg)
|
||||
),
|
||||
Err(_) => msg_list.push(
|
||||
format!("thread {} ({}) paniced", self.name, i)
|
||||
),
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
msg_list
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We make sure that all threads will be joined
|
||||
impl<I> Drop for ParallelHandler<I> {
|
||||
fn drop(&mut self) {
|
||||
drop(self.input.take());
|
||||
while let Some(handle) = self.handles.pop() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
/*global Proxmox*/
|
||||
Ext.define('PBS.Application', {
|
||||
extend: 'Ext.app.Application',
|
||||
|
||||
@ -6,7 +5,7 @@ Ext.define('PBS.Application', {
|
||||
appProperty: 'app',
|
||||
|
||||
stores: [
|
||||
'NavigationStore'
|
||||
'NavigationStore',
|
||||
],
|
||||
|
||||
layout: 'fit',
|
||||
@ -29,7 +28,7 @@ Ext.define('PBS.Application', {
|
||||
PBS.view = view;
|
||||
me.view = view;
|
||||
|
||||
if (me.currentView != undefined) {
|
||||
if (me.currentView !== undefined) {
|
||||
me.currentView.destroy();
|
||||
}
|
||||
|
||||
@ -58,7 +57,7 @@ Ext.define('PBS.Application', {
|
||||
} else {
|
||||
me.changeView('mainview', true);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Ext.application('PBS.Application');
|
||||
|
@ -13,7 +13,7 @@ Ext.define('PBS.Dashboard', {
|
||||
width: 300,
|
||||
title: gettext('Dashboard Options'),
|
||||
layout: {
|
||||
type: 'auto'
|
||||
type: 'auto',
|
||||
},
|
||||
items: [{
|
||||
xtype: 'form',
|
||||
@ -28,7 +28,7 @@ Ext.define('PBS.Dashboard', {
|
||||
minValue: 1,
|
||||
maxValue: 24,
|
||||
value: viewModel.get('hours'),
|
||||
fieldLabel: gettext('Hours to show')
|
||||
fieldLabel: gettext('Hours to show'),
|
||||
}],
|
||||
buttons: [{
|
||||
text: gettext('Save'),
|
||||
@ -39,9 +39,9 @@ Ext.define('PBS.Dashboard', {
|
||||
var hours = win.down('#hours').getValue();
|
||||
me.setHours(hours, true);
|
||||
win.close();
|
||||
}
|
||||
}]
|
||||
}]
|
||||
},
|
||||
}],
|
||||
}],
|
||||
}).show();
|
||||
},
|
||||
|
||||
@ -119,7 +119,7 @@ Ext.define('PBS.Dashboard', {
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
},
|
||||
text: gettext('Copy')
|
||||
text: gettext('Copy'),
|
||||
},
|
||||
{
|
||||
text: gettext('Ok'),
|
||||
@ -140,10 +140,10 @@ Ext.define('PBS.Dashboard', {
|
||||
me.lookup('longesttasks').updateTasks(top10);
|
||||
|
||||
let data = {
|
||||
backup: { error: 0, warning: 0, ok: 0, },
|
||||
prune: { error: 0, warning: 0, ok: 0, },
|
||||
garbage_collection: { error: 0, warning: 0, ok: 0, },
|
||||
sync: { error: 0, warning: 0, ok: 0, },
|
||||
backup: { error: 0, warning: 0, ok: 0 },
|
||||
prune: { error: 0, warning: 0, ok: 0 },
|
||||
garbage_collection: { error: 0, warning: 0, ok: 0 },
|
||||
sync: { error: 0, warning: 0, ok: 0 },
|
||||
};
|
||||
|
||||
records.forEach(record => {
|
||||
@ -166,7 +166,7 @@ Ext.define('PBS.Dashboard', {
|
||||
var sp = Ext.state.Manager.getProvider();
|
||||
var hours = sp.get('dashboard-hours') || 12;
|
||||
me.setHours(hours, false);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
viewModel: {
|
||||
@ -177,7 +177,7 @@ Ext.define('PBS.Dashboard', {
|
||||
fingerprint: "",
|
||||
'bytes_in': 0,
|
||||
'bytes_out': 0,
|
||||
'avg_ptime': 0.0
|
||||
'avg_ptime': 0.0,
|
||||
},
|
||||
|
||||
formulas: {
|
||||
@ -194,11 +194,11 @@ Ext.define('PBS.Dashboard', {
|
||||
autoDestroy: true,
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: '/api2/json/nodes/localhost/status'
|
||||
url: '/api2/json/nodes/localhost/status',
|
||||
},
|
||||
listeners: {
|
||||
load: 'updateUsageStats'
|
||||
}
|
||||
load: 'updateUsageStats',
|
||||
},
|
||||
},
|
||||
subscription: {
|
||||
storeid: 'dash-subscription',
|
||||
@ -209,11 +209,11 @@ Ext.define('PBS.Dashboard', {
|
||||
autoDestroy: true,
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: '/api2/json/nodes/localhost/subscription'
|
||||
url: '/api2/json/nodes/localhost/subscription',
|
||||
},
|
||||
listeners: {
|
||||
load: 'updateSubscription'
|
||||
}
|
||||
load: 'updateSubscription',
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
storeid: 'dash-tasks',
|
||||
@ -225,19 +225,19 @@ Ext.define('PBS.Dashboard', {
|
||||
model: 'proxmox-tasks',
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: '/api2/json/status/tasks'
|
||||
url: '/api2/json/status/tasks',
|
||||
},
|
||||
listeners: {
|
||||
load: 'updateTasks'
|
||||
}
|
||||
load: 'updateTasks',
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
title: gettext('Dashboard') + ' - WIP',
|
||||
|
||||
layout: {
|
||||
type: 'column'
|
||||
type: 'column',
|
||||
},
|
||||
|
||||
bodyPadding: '20 0 0 20',
|
||||
@ -245,7 +245,7 @@ Ext.define('PBS.Dashboard', {
|
||||
defaults: {
|
||||
columnWidth: 0.49,
|
||||
xtype: 'panel',
|
||||
margin: '0 20 20 0'
|
||||
margin: '0 20 20 0',
|
||||
},
|
||||
|
||||
scrollable: true,
|
||||
@ -268,27 +268,27 @@ Ext.define('PBS.Dashboard', {
|
||||
],
|
||||
layout: {
|
||||
type: 'hbox',
|
||||
align: 'center'
|
||||
align: 'center',
|
||||
},
|
||||
defaults: {
|
||||
xtype: 'proxmoxGauge',
|
||||
spriteFontSize: '20px',
|
||||
flex: 1
|
||||
flex: 1,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
title: gettext('CPU'),
|
||||
reference: 'cpu'
|
||||
reference: 'cpu',
|
||||
},
|
||||
{
|
||||
title: gettext('Memory'),
|
||||
reference: 'mem'
|
||||
reference: 'mem',
|
||||
},
|
||||
{
|
||||
title: gettext('Root Disk'),
|
||||
reference: 'root'
|
||||
}
|
||||
]
|
||||
reference: 'root',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'pbsDatastoresStatistics',
|
||||
@ -314,7 +314,7 @@ Ext.define('PBS.Dashboard', {
|
||||
reference: 'subscription',
|
||||
xtype: 'pbsSubscriptionInfo',
|
||||
},
|
||||
]
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('PBS.dashboard.SubscriptionInfo', {
|
||||
@ -322,7 +322,7 @@ Ext.define('PBS.dashboard.SubscriptionInfo', {
|
||||
xtype: 'pbsSubscriptionInfo',
|
||||
|
||||
style: {
|
||||
cursor: 'pointer'
|
||||
cursor: 'pointer',
|
||||
},
|
||||
|
||||
layout: {
|
||||
@ -382,7 +382,7 @@ Ext.define('PBS.dashboard.SubscriptionInfo', {
|
||||
fn: function() {
|
||||
var mainview = this.component.up('mainview');
|
||||
mainview.getController().redirectTo('pbsSubscription');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -6,9 +6,9 @@ Ext.define('pbs-prune-list', {
|
||||
{
|
||||
name: 'backup-time',
|
||||
type: 'date',
|
||||
dateFormat: 'timestamp'
|
||||
dateFormat: 'timestamp',
|
||||
},
|
||||
]
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('PBS.DataStorePruneInputPanel', {
|
||||
@ -52,21 +52,21 @@ Ext.define('PBS.DataStorePruneInputPanel', {
|
||||
method: "POST",
|
||||
params: params,
|
||||
callback: function() {
|
||||
return; // for easy breakpoint setting
|
||||
// for easy breakpoint setting
|
||||
},
|
||||
failure: function (response, opts) {
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
success: function(response, options) {
|
||||
var data = response.result.data;
|
||||
view.prune_store.setData(data);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
control: {
|
||||
field: { change: 'reload' }
|
||||
}
|
||||
field: { change: 'reload' },
|
||||
},
|
||||
},
|
||||
|
||||
column1: [
|
||||
@ -111,16 +111,16 @@ Ext.define('PBS.DataStorePruneInputPanel', {
|
||||
allowBlank: true,
|
||||
fieldLabel: gettext('keep-yearly'),
|
||||
minValue: 1,
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
initComponent : function() {
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
|
||||
me.prune_store = Ext.create('Ext.data.Store', {
|
||||
model: 'pbs-prune-list',
|
||||
sorters: { property: 'backup-time', direction: 'DESC' }
|
||||
sorters: { property: 'backup-time', direction: 'DESC' },
|
||||
});
|
||||
|
||||
me.column2 = [
|
||||
@ -145,14 +145,14 @@ Ext.define('PBS.DataStorePruneInputPanel', {
|
||||
},
|
||||
{
|
||||
text: "keep",
|
||||
dataIndex: 'keep'
|
||||
}
|
||||
]
|
||||
}
|
||||
dataIndex: 'keep',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
me.callParent();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('PBS.DataStorePrune', {
|
||||
@ -163,7 +163,7 @@ Ext.define('PBS.DataStorePrune', {
|
||||
|
||||
isCreate: true,
|
||||
|
||||
initComponent : function() {
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
|
||||
if (!me.datastore) {
|
||||
@ -183,10 +183,10 @@ Ext.define('PBS.DataStorePrune', {
|
||||
xtype: 'pbsDataStorePruneInputPanel',
|
||||
url: '/api2/extjs/admin/datastore/' + me.datastore + "/prune",
|
||||
backup_type: me.backup_type,
|
||||
backup_id: me.backup_id
|
||||
}]
|
||||
backup_id: me.backup_id,
|
||||
}],
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -19,10 +19,10 @@ Ext.define('pve-rrd-datastore', {
|
||||
return 0;
|
||||
}
|
||||
return (data.io_ticks*1000.0)/ios;
|
||||
}
|
||||
},
|
||||
},
|
||||
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
|
||||
]
|
||||
{ type: 'date', dateFormat: 'timestamp', name: 'time' },
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('PBS.DataStoreStatistic', {
|
||||
@ -40,11 +40,11 @@ Ext.define('PBS.DataStoreStatistic', {
|
||||
throw "no datastore specified";
|
||||
}
|
||||
|
||||
me.tbar = [ '->', { xtype: 'proxmoxRRDTypeSelector' } ];
|
||||
me.tbar = ['->', { xtype: 'proxmoxRRDTypeSelector' }];
|
||||
|
||||
var rrdstore = Ext.create('Proxmox.data.RRDStore', {
|
||||
rrdurl: "/api2/json/admin/datastore/" + me.datastore + "/rrd",
|
||||
model: 'pve-rrd-datastore'
|
||||
model: 'pve-rrd-datastore',
|
||||
});
|
||||
|
||||
me.items = {
|
||||
@ -55,38 +55,38 @@ Ext.define('PBS.DataStoreStatistic', {
|
||||
defaults: {
|
||||
minHeight: 320,
|
||||
padding: 5,
|
||||
columnWidth: 1
|
||||
columnWidth: 1,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Storage usage (bytes)'),
|
||||
fields: ['total','used'],
|
||||
fields: ['total', 'used'],
|
||||
fieldTitles: [gettext('Total'), gettext('Storage usage')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Transfer Rate (bytes/second)'),
|
||||
fields: ['read_bytes','write_bytes'],
|
||||
fields: ['read_bytes', 'write_bytes'],
|
||||
fieldTitles: [gettext('Read'), gettext('Write')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Input/Output Operations per Second (IOPS)'),
|
||||
fields: ['read_ios','write_ios'],
|
||||
fields: ['read_ios', 'write_ios'],
|
||||
fieldTitles: [gettext('Read'), gettext('Write')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('IO Delay (ms)'),
|
||||
fields: ['io_delay'],
|
||||
fieldTitles: [gettext('IO Delay')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
me.listeners = {
|
||||
@ -99,6 +99,6 @@ Ext.define('PBS.DataStoreStatistic', {
|
||||
};
|
||||
|
||||
me.callParent();
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
@ -7,7 +7,6 @@ Ext.define('PBS.LoginView', {
|
||||
|
||||
submitForm: function() {
|
||||
var me = this;
|
||||
var view = me.getView();
|
||||
var loginForm = me.lookupReference('loginForm');
|
||||
var unField = me.lookupReference('usernameField');
|
||||
var saveunField = me.lookupReference('saveunField');
|
||||
@ -19,7 +18,7 @@ Ext.define('PBS.LoginView', {
|
||||
let params = loginForm.getValues();
|
||||
|
||||
params.username = params.username + '@' + params.realm;
|
||||
delete(params.realm);
|
||||
delete params.realm;
|
||||
|
||||
if (loginForm.isVisible()) {
|
||||
loginForm.mask(gettext('Please wait...'), 'x-mask-loading');
|
||||
@ -48,9 +47,9 @@ Ext.define('PBS.LoginView', {
|
||||
loginForm.unmask();
|
||||
Ext.MessageBox.alert(
|
||||
gettext('Error'),
|
||||
gettext('Login failed. Please try again')
|
||||
gettext('Login failed. Please try again'),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@ -63,7 +62,7 @@ Ext.define('PBS.LoginView', {
|
||||
pf.focus(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
'field[name=lang]': {
|
||||
change: function(f, value) {
|
||||
@ -71,10 +70,10 @@ Ext.define('PBS.LoginView', {
|
||||
Ext.util.Cookies.set('PBSLangCookie', value, dt);
|
||||
this.getView().mask(gettext('Please wait...'), 'x-mask-loading');
|
||||
window.location.reload();
|
||||
}
|
||||
},
|
||||
},
|
||||
'button[reference=loginButton]': {
|
||||
click: 'submitForm'
|
||||
click: 'submitForm',
|
||||
},
|
||||
'window[reference=loginwindow]': {
|
||||
show: function() {
|
||||
@ -85,21 +84,21 @@ Ext.define('PBS.LoginView', {
|
||||
var checked = sp.get(checkboxField.getStateId());
|
||||
checkboxField.setValue(checked);
|
||||
|
||||
if(checked === true) {
|
||||
if (checked === true) {
|
||||
var username = sp.get(unField.getStateId());
|
||||
unField.setValue(username);
|
||||
var pwField = this.lookupReference('passwordField');
|
||||
pwField.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
plugins: 'viewport',
|
||||
|
||||
layout: {
|
||||
type: 'border'
|
||||
type: 'border',
|
||||
},
|
||||
|
||||
items: [
|
||||
@ -108,7 +107,7 @@ Ext.define('PBS.LoginView', {
|
||||
xtype: 'container',
|
||||
layout: {
|
||||
type: 'hbox',
|
||||
align: 'middle'
|
||||
align: 'middle',
|
||||
},
|
||||
margin: '2 5 2 5',
|
||||
height: 38,
|
||||
@ -119,12 +118,12 @@ Ext.define('PBS.LoginView', {
|
||||
},
|
||||
{
|
||||
xtype: 'versioninfo',
|
||||
makeApiCall: false
|
||||
}
|
||||
]
|
||||
makeApiCall: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
region: 'center'
|
||||
region: 'center',
|
||||
},
|
||||
{
|
||||
xtype: 'window',
|
||||
@ -138,7 +137,7 @@ Ext.define('PBS.LoginView', {
|
||||
defaultFocus: 'usernameField',
|
||||
|
||||
layout: {
|
||||
type: 'auto'
|
||||
type: 'auto',
|
||||
},
|
||||
|
||||
title: gettext('Proxmox Backup Server Login'),
|
||||
@ -147,7 +146,7 @@ Ext.define('PBS.LoginView', {
|
||||
{
|
||||
xtype: 'form',
|
||||
layout: {
|
||||
type: 'form'
|
||||
type: 'form',
|
||||
},
|
||||
defaultButton: 'loginButton',
|
||||
url: '/api2/extjs/access/ticket',
|
||||
@ -155,7 +154,7 @@ Ext.define('PBS.LoginView', {
|
||||
|
||||
fieldDefaults: {
|
||||
labelAlign: 'right',
|
||||
allowBlank: false
|
||||
allowBlank: false,
|
||||
},
|
||||
|
||||
items: [
|
||||
@ -165,7 +164,7 @@ Ext.define('PBS.LoginView', {
|
||||
name: 'username',
|
||||
itemId: 'usernameField',
|
||||
reference: 'usernameField',
|
||||
stateId: 'login-username'
|
||||
stateId: 'login-username',
|
||||
},
|
||||
{
|
||||
xtype: 'textfield',
|
||||
@ -177,7 +176,7 @@ Ext.define('PBS.LoginView', {
|
||||
},
|
||||
{
|
||||
xtype: 'pmxRealmComboBox',
|
||||
name: 'realm'
|
||||
name: 'realm',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxLanguageSelector',
|
||||
@ -185,8 +184,8 @@ Ext.define('PBS.LoginView', {
|
||||
value: Ext.util.Cookies.get('PBSLangCookie') || Proxmox.defaultLang || 'en',
|
||||
name: 'lang',
|
||||
reference: 'langField',
|
||||
submitValue: false
|
||||
}
|
||||
submitValue: false,
|
||||
},
|
||||
],
|
||||
buttons: [
|
||||
{
|
||||
@ -197,16 +196,16 @@ Ext.define('PBS.LoginView', {
|
||||
stateId: 'login-saveusername',
|
||||
labelWidth: 250,
|
||||
labelAlign: 'right',
|
||||
submitValue: false
|
||||
submitValue: false,
|
||||
},
|
||||
{
|
||||
text: gettext('Login'),
|
||||
reference: 'loginButton',
|
||||
formBind: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
formBind: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -10,11 +10,11 @@ Ext.define('PBS.MainView', {
|
||||
':path:subpath': {
|
||||
action: 'changePath',
|
||||
before: 'beforeChangePath',
|
||||
conditions : {
|
||||
':path' : '(?:([%a-zA-Z0-9\\-\\_\\s,\.]+))',
|
||||
':subpath' : '(?:(?::)([%a-zA-Z0-9\\-\\_\\s,]+))?'
|
||||
}
|
||||
}
|
||||
conditions: {
|
||||
':path': '(?:([%a-zA-Z0-9\\-\\_\\s,.]+))',
|
||||
':subpath': '(?:(?::)([%a-zA-Z0-9\\-\\_\\s,]+))?',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
beforeChangePath: function(path, subpath, action) {
|
||||
@ -79,7 +79,7 @@ Ext.define('PBS.MainView', {
|
||||
obj = contentpanel.add({
|
||||
xtype: path,
|
||||
nodename: 'localhost',
|
||||
border: false
|
||||
border: false,
|
||||
});
|
||||
}
|
||||
|
||||
@ -113,7 +113,6 @@ Ext.define('PBS.MainView', {
|
||||
if (lastpanel) {
|
||||
contentpanel.remove(lastpanel, { destroy: true });
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
logout: function() {
|
||||
@ -126,8 +125,8 @@ Ext.define('PBS.MainView', {
|
||||
|
||||
control: {
|
||||
'[reference=logoutButton]': {
|
||||
click: 'logout'
|
||||
}
|
||||
click: 'logout',
|
||||
},
|
||||
},
|
||||
|
||||
init: function(view) {
|
||||
@ -139,7 +138,7 @@ Ext.define('PBS.MainView', {
|
||||
// show login on requestexception
|
||||
// fixme: what about other errors
|
||||
Ext.Ajax.on('requestexception', function(conn, response, options) {
|
||||
if (response.status == 401) { // auth failure
|
||||
if (response.status === 401 || response.status === '401') { // auth failure
|
||||
me.logout();
|
||||
}
|
||||
});
|
||||
@ -155,7 +154,7 @@ Ext.define('PBS.MainView', {
|
||||
Ext.Ajax.request({
|
||||
params: {
|
||||
username: Proxmox.UserName,
|
||||
password: ticket
|
||||
password: ticket,
|
||||
},
|
||||
url: '/api2/json/access/ticket',
|
||||
method: 'POST',
|
||||
@ -165,17 +164,17 @@ Ext.define('PBS.MainView', {
|
||||
success: function(response, opts) {
|
||||
var obj = Ext.decode(response.responseText);
|
||||
PBS.Utils.updateLoginData(obj.data);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
interval: 15*60*1000
|
||||
interval: 15*60*1000,
|
||||
});
|
||||
|
||||
|
||||
// select treeitem and load page from url fragment, if set
|
||||
let token = Ext.util.History.getToken() || 'pbsDashboard';
|
||||
this.redirectTo(token, true);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
plugins: 'viewport',
|
||||
@ -188,7 +187,7 @@ Ext.define('PBS.MainView', {
|
||||
xtype: 'container',
|
||||
layout: {
|
||||
type: 'hbox',
|
||||
align: 'middle'
|
||||
align: 'middle',
|
||||
},
|
||||
margin: '2 0 2 5',
|
||||
height: 38,
|
||||
@ -229,7 +228,7 @@ Ext.define('PBS.MainView', {
|
||||
style: {
|
||||
// proxmox dark grey p light grey as border
|
||||
backgroundColor: '#464d4d',
|
||||
borderColor: '#ABBABA'
|
||||
borderColor: '#ABBABA',
|
||||
},
|
||||
margin: '0 5 0 0',
|
||||
iconCls: 'fa fa-user',
|
||||
@ -241,7 +240,7 @@ Ext.define('PBS.MainView', {
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
@ -250,7 +249,7 @@ Ext.define('PBS.MainView', {
|
||||
region: 'west',
|
||||
layout: {
|
||||
type: 'vbox',
|
||||
align: 'stretch'
|
||||
align: 'stretch',
|
||||
},
|
||||
items: [{
|
||||
xtype: 'navigationtree',
|
||||
@ -260,20 +259,20 @@ Ext.define('PBS.MainView', {
|
||||
// because of a bug where a viewcontroller does not detect
|
||||
// the selectionchange event of a treelist
|
||||
listeners: {
|
||||
selectionchange: 'navigate'
|
||||
}
|
||||
selectionchange: 'navigate',
|
||||
},
|
||||
}, {
|
||||
xtype: 'box',
|
||||
cls: 'x-treelist-nav',
|
||||
flex: 1
|
||||
}]
|
||||
flex: 1,
|
||||
}],
|
||||
},
|
||||
{
|
||||
xtype: 'panel',
|
||||
layout: { type: 'card' },
|
||||
region: 'center',
|
||||
border: false,
|
||||
reference: 'contentpanel'
|
||||
}
|
||||
]
|
||||
reference: 'contentpanel',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
13
www/Makefile
13
www/Makefile
@ -59,18 +59,23 @@ OnlineHelpInfo.js:
|
||||
$(MAKE) -C ../docs onlinehelpinfo
|
||||
mv ../docs/output/scanrefs/OnlineHelpInfo.js .
|
||||
|
||||
js/proxmox-backup-gui.js: js OnlineHelpInfo.js ${JSSRC}
|
||||
js/proxmox-backup-gui.js: .lint-incremental js OnlineHelpInfo.js ${JSSRC}
|
||||
cat OnlineHelpInfo.js ${JSSRC} >$@.tmp
|
||||
mv $@.tmp $@
|
||||
|
||||
.PHONY: lint
|
||||
lint: ${JSSRC}
|
||||
.PHONY: check
|
||||
check:
|
||||
eslint ${JSSRC}
|
||||
touch ".lint-incremental"
|
||||
|
||||
.lint-incremental: ${JSSRC}
|
||||
eslint $?
|
||||
touch "$@"
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
find . -name '*~' -exec rm {} ';'
|
||||
rm -rf js
|
||||
rm -rf js .lint-incremental
|
||||
|
||||
install: js/proxmox-backup-gui.js css/ext6-pbs.css index.hbs
|
||||
install -dm755 $(DESTDIR)$(JSDIR)
|
||||
|
@ -10,7 +10,7 @@ Ext.define('PBS.store.NavigationStore', {
|
||||
text: gettext('Dashboard'),
|
||||
iconCls: 'fa fa-tachometer',
|
||||
path: 'pbsDashboard',
|
||||
leaf: true
|
||||
leaf: true,
|
||||
},
|
||||
{
|
||||
text: gettext('Configuration'),
|
||||
@ -22,13 +22,13 @@ Ext.define('PBS.store.NavigationStore', {
|
||||
text: gettext('User Management'),
|
||||
iconCls: 'fa fa-user',
|
||||
path: 'pbsUserView',
|
||||
leaf: true
|
||||
leaf: true,
|
||||
},
|
||||
{
|
||||
text: gettext('Permissions'),
|
||||
iconCls: 'fa fa-unlock',
|
||||
path: 'pbsACLView',
|
||||
leaf: true
|
||||
leaf: true,
|
||||
},
|
||||
{
|
||||
text: gettext('Remotes'),
|
||||
@ -46,9 +46,9 @@ Ext.define('PBS.store.NavigationStore', {
|
||||
text: gettext('Subscription'),
|
||||
iconCls: 'fa fa-support',
|
||||
path: 'pbsSubscription',
|
||||
leaf: true
|
||||
}
|
||||
]
|
||||
leaf: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: gettext('Administration'),
|
||||
@ -75,19 +75,19 @@ Ext.define('PBS.store.NavigationStore', {
|
||||
path: 'pbsZFSList',
|
||||
leaf: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: gettext('Datastore'),
|
||||
iconCls: 'fa fa-archive',
|
||||
path: 'pbsDataStoreConfig',
|
||||
expanded: true,
|
||||
leaf: false
|
||||
leaf: false,
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('PBS.view.main.NavigationTree', {
|
||||
@ -98,13 +98,12 @@ Ext.define('PBS.view.main.NavigationTree', {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
init: function(view) {
|
||||
|
||||
view.rstore = Ext.create('Proxmox.data.UpdateStore', {
|
||||
autoStart: true,
|
||||
interval: 15 * 1000,
|
||||
storeId: 'pbs-datastore-list',
|
||||
storeid: 'pbs-datastore-list',
|
||||
model: 'pbs-datastore-list'
|
||||
model: 'pbs-datastore-list',
|
||||
});
|
||||
|
||||
view.rstore.on('load', this.onLoad, this);
|
||||
@ -119,7 +118,7 @@ Ext.define('PBS.view.main.NavigationTree', {
|
||||
|
||||
// FIXME: newly added always get appended to the end..
|
||||
records.sort((a, b) => {
|
||||
if (a.id > b.id) return 1;
|
||||
if (a.id > b.id) return 1;
|
||||
if (a.id < b.id) return -1;
|
||||
return 0;
|
||||
});
|
||||
@ -128,29 +127,28 @@ Ext.define('PBS.view.main.NavigationTree', {
|
||||
var length = records.length;
|
||||
var lookup_hash = {};
|
||||
for (var i = 0; i < length; i++) {
|
||||
var name = records[i].id;
|
||||
let name = records[i].id;
|
||||
lookup_hash[name] = true;
|
||||
if (!list.findChild('text', name, false)) {
|
||||
list.appendChild({
|
||||
text: name,
|
||||
path: `DataStore-${name}`,
|
||||
iconCls: 'fa fa-database',
|
||||
leaf: true
|
||||
leaf: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var erase_list = [];
|
||||
list.eachChild(function(node) {
|
||||
var name = node.data.text;
|
||||
let name = node.data.text;
|
||||
if (!lookup_hash[name]) {
|
||||
erase_list.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
Ext.Array.forEach(erase_list, function(node) { node.erase(); });
|
||||
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
select: function(path) {
|
||||
@ -163,5 +161,5 @@ Ext.define('PBS.view.main.NavigationTree', {
|
||||
expanderOnly: true,
|
||||
expanderFirst: false,
|
||||
store: 'NavigationStore',
|
||||
ui: 'nav'
|
||||
ui: 'nav',
|
||||
});
|
||||
|
@ -3,6 +3,22 @@ const proxmoxOnlineHelpInfo = {
|
||||
"link": "/docs/index.html",
|
||||
"title": "Proxmox Backup Server Documentation Index"
|
||||
},
|
||||
"datastore-intro": {
|
||||
"link": "/docs/administration-guide.html#datastore-intro",
|
||||
"title": ":term:`DataStore`"
|
||||
},
|
||||
"user-mgmt": {
|
||||
"link": "/docs/administration-guide.html#user-mgmt",
|
||||
"title": "User Management"
|
||||
},
|
||||
"user-acl": {
|
||||
"link": "/docs/administration-guide.html#user-acl",
|
||||
"title": "Access Control"
|
||||
},
|
||||
"backup-remote": {
|
||||
"link": "/docs/administration-guide.html#backup-remote",
|
||||
"title": ":term:`Remote`"
|
||||
},
|
||||
"syncjobs": {
|
||||
"link": "/docs/administration-guide.html#syncjobs",
|
||||
"title": "Sync Jobs"
|
||||
|
@ -1,4 +1,3 @@
|
||||
/*global Proxmox*/
|
||||
Ext.define('PBS.ServerAdministration', {
|
||||
extend: 'Ext.tab.Panel',
|
||||
alias: 'widget.pbsServerAdministration',
|
||||
@ -14,13 +13,13 @@ Ext.define('PBS.ServerAdministration', {
|
||||
init: function(view) {
|
||||
var upgradeBtn = view.lookupReference('upgradeBtn');
|
||||
upgradeBtn.setDisabled(!(Proxmox.UserName && Proxmox.UserName === 'root@pam'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
items: [
|
||||
{
|
||||
xtype: 'pbsServerStatus',
|
||||
itemId: 'status'
|
||||
itemId: 'status',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxNodeServiceView',
|
||||
@ -32,7 +31,7 @@ Ext.define('PBS.ServerAdministration', {
|
||||
'proxmox-backup': true,
|
||||
'proxmox-backup-proxy': true,
|
||||
},
|
||||
nodename: 'localhost'
|
||||
nodename: 'localhost',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxNodeAPT',
|
||||
@ -44,10 +43,10 @@ Ext.define('PBS.ServerAdministration', {
|
||||
text: gettext('Upgrade'),
|
||||
handler: function() {
|
||||
Proxmox.Utils.openXtermJsViewer('upgrade', 0, 'localhost');
|
||||
}
|
||||
},
|
||||
},
|
||||
itemId: 'updates',
|
||||
nodename: 'localhost'
|
||||
nodename: 'localhost',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxJournalView',
|
||||
@ -60,9 +59,9 @@ Ext.define('PBS.ServerAdministration', {
|
||||
itemId: 'tasks',
|
||||
title: gettext('Tasks'),
|
||||
height: 'auto',
|
||||
nodename: 'localhost'
|
||||
}
|
||||
]
|
||||
nodename: 'localhost',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
|
@ -6,14 +6,14 @@ Ext.define('pve-rrd-node', {
|
||||
// percentage
|
||||
convert: function(value) {
|
||||
return value*100;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'iowait',
|
||||
// percentage
|
||||
convert: function(value) {
|
||||
return value*100;
|
||||
}
|
||||
},
|
||||
},
|
||||
'netin',
|
||||
'netout',
|
||||
@ -33,15 +33,15 @@ Ext.define('pve-rrd-node', {
|
||||
let ios = 0;
|
||||
if (data.read_ios !== undefined) { ios += data.read_ios; }
|
||||
if (data.write_ios !== undefined) { ios += data.write_ios; }
|
||||
if (ios == 0 || data.io_ticks === undefined) {
|
||||
if (ios === 0 || data.io_ticks === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return (data.io_ticks*1000.0)/ios;
|
||||
}
|
||||
},
|
||||
},
|
||||
'loadavg',
|
||||
{ type: 'date', dateFormat: 'timestamp', name: 'time' }
|
||||
]
|
||||
{ type: 'date', dateFormat: 'timestamp', name: 'time' },
|
||||
],
|
||||
});
|
||||
Ext.define('PBS.ServerStatus', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
@ -62,7 +62,7 @@ Ext.define('PBS.ServerStatus', {
|
||||
waitMsgTarget: me,
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@ -73,7 +73,7 @@ Ext.define('PBS.ServerStatus', {
|
||||
handler: function() {
|
||||
node_command('reboot');
|
||||
},
|
||||
iconCls: 'fa fa-undo'
|
||||
iconCls: 'fa fa-undo',
|
||||
});
|
||||
|
||||
var shutdownBtn = Ext.create('Proxmox.button.Button', {
|
||||
@ -83,7 +83,7 @@ Ext.define('PBS.ServerStatus', {
|
||||
handler: function() {
|
||||
node_command('shutdown');
|
||||
},
|
||||
iconCls: 'fa fa-power-off'
|
||||
iconCls: 'fa fa-power-off',
|
||||
});
|
||||
|
||||
var consoleBtn = Ext.create('Proxmox.button.Button', {
|
||||
@ -91,14 +91,14 @@ Ext.define('PBS.ServerStatus', {
|
||||
iconCls: 'fa fa-terminal',
|
||||
handler: function() {
|
||||
Proxmox.Utils.openXtermJsViewer('shell', 0, Proxmox.NodeName);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
me.tbar = [ consoleBtn, restartBtn, shutdownBtn, '->', { xtype: 'proxmoxRRDTypeSelector' } ];
|
||||
me.tbar = [consoleBtn, restartBtn, shutdownBtn, '->', { xtype: 'proxmoxRRDTypeSelector' }];
|
||||
|
||||
var rrdstore = Ext.create('Proxmox.data.RRDStore', {
|
||||
rrdurl: "/api2/json/nodes/localhost/rrd",
|
||||
model: 'pve-rrd-node'
|
||||
model: 'pve-rrd-node',
|
||||
});
|
||||
|
||||
me.items = {
|
||||
@ -109,72 +109,72 @@ Ext.define('PBS.ServerStatus', {
|
||||
defaults: {
|
||||
minHeight: 320,
|
||||
padding: 5,
|
||||
columnWidth: 1
|
||||
columnWidth: 1,
|
||||
},
|
||||
items: [
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('CPU usage'),
|
||||
fields: ['cpu','iowait'],
|
||||
fields: ['cpu', 'iowait'],
|
||||
fieldTitles: [gettext('CPU usage'), gettext('IO wait')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Server load'),
|
||||
fields: ['loadavg'],
|
||||
fieldTitles: [gettext('Load average')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Memory usage'),
|
||||
fields: ['memtotal','memused'],
|
||||
fields: ['memtotal', 'memused'],
|
||||
fieldTitles: [gettext('Total'), gettext('RAM usage')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Swap usage'),
|
||||
fields: ['swaptotal','swapused'],
|
||||
fields: ['swaptotal', 'swapused'],
|
||||
fieldTitles: [gettext('Total'), gettext('Swap usage')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Network traffic'),
|
||||
fields: ['netin','netout'],
|
||||
store: rrdstore
|
||||
fields: ['netin', 'netout'],
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Root Disk usage'),
|
||||
fields: ['total','used'],
|
||||
fields: ['total', 'used'],
|
||||
fieldTitles: [gettext('Total'), gettext('Disk usage')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Root Disk Transfer Rate (bytes/second)'),
|
||||
fields: ['read_bytes','write_bytes'],
|
||||
fields: ['read_bytes', 'write_bytes'],
|
||||
fieldTitles: [gettext('Read'), gettext('Write')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Root Disk Input/Output Operations per Second (IOPS)'),
|
||||
fields: ['read_ios','write_ios'],
|
||||
fields: ['read_ios', 'write_ios'],
|
||||
fieldTitles: [gettext('Read'), gettext('Write')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Root Disk IO Delay (ms)'),
|
||||
fields: ['io_delay'],
|
||||
fieldTitles: [gettext('IO Delay')],
|
||||
store: rrdstore
|
||||
store: rrdstore,
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
|
||||
me.listeners = {
|
||||
@ -187,6 +187,6 @@ Ext.define('PBS.ServerStatus', {
|
||||
};
|
||||
|
||||
me.callParent();
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
@ -1,4 +1,3 @@
|
||||
/*global Blob,Proxmox*/
|
||||
Ext.define('PBS.SubscriptionKeyEdit', {
|
||||
extend: 'Proxmox.window.Edit',
|
||||
|
||||
@ -12,8 +11,8 @@ Ext.define('PBS.SubscriptionKeyEdit', {
|
||||
xtype: 'textfield',
|
||||
name: 'key',
|
||||
value: '',
|
||||
fieldLabel: gettext('Subscription Key')
|
||||
}
|
||||
fieldLabel: gettext('Subscription Key'),
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('PBS.Subscription', {
|
||||
@ -27,10 +26,10 @@ Ext.define('PBS.Subscription', {
|
||||
onlineHelp: 'getting_help',
|
||||
|
||||
viewConfig: {
|
||||
enableTextSelection: true
|
||||
enableTextSelection: true,
|
||||
},
|
||||
|
||||
initComponent : function() {
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
|
||||
var reload = function() {
|
||||
@ -40,7 +39,6 @@ Ext.define('PBS.Subscription', {
|
||||
var baseurl = '/nodes/localhost/subscription';
|
||||
|
||||
var render_status = function(value) {
|
||||
|
||||
var message = me.getObjectValue('message');
|
||||
|
||||
if (message) {
|
||||
@ -51,31 +49,31 @@ Ext.define('PBS.Subscription', {
|
||||
|
||||
var rows = {
|
||||
productname: {
|
||||
header: gettext('Type')
|
||||
header: gettext('Type'),
|
||||
},
|
||||
key: {
|
||||
header: gettext('Subscription Key')
|
||||
header: gettext('Subscription Key'),
|
||||
},
|
||||
status: {
|
||||
header: gettext('Status'),
|
||||
renderer: render_status
|
||||
renderer: render_status,
|
||||
},
|
||||
message: {
|
||||
visible: false
|
||||
visible: false,
|
||||
},
|
||||
serverid: {
|
||||
header: gettext('Server ID')
|
||||
header: gettext('Server ID'),
|
||||
},
|
||||
sockets: {
|
||||
header: gettext('Sockets')
|
||||
header: gettext('Sockets'),
|
||||
},
|
||||
checktime: {
|
||||
header: gettext('Last checked'),
|
||||
renderer: Proxmox.Utils.render_timestamp
|
||||
renderer: Proxmox.Utils.render_timestamp,
|
||||
},
|
||||
nextduedate: {
|
||||
header: gettext('Next due date')
|
||||
}
|
||||
header: gettext('Next due date'),
|
||||
},
|
||||
};
|
||||
|
||||
Ext.apply(me, {
|
||||
@ -86,11 +84,11 @@ Ext.define('PBS.Subscription', {
|
||||
text: gettext('Upload Subscription Key'),
|
||||
handler: function() {
|
||||
var win = Ext.create('PBS.SubscriptionKeyEdit', {
|
||||
url: '/api2/extjs/' + baseurl
|
||||
url: '/api2/extjs/' + baseurl,
|
||||
});
|
||||
win.show();
|
||||
win.on('destroy', reload);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: gettext('Check'),
|
||||
@ -103,16 +101,16 @@ Ext.define('PBS.Subscription', {
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
callback: reload
|
||||
callback: reload,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
rows: rows
|
||||
rows: rows,
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
|
||||
reload();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -1,5 +1,3 @@
|
||||
/*global Proxmox*/
|
||||
|
||||
Ext.define('PBS.SystemConfiguration', {
|
||||
extend: 'Ext.tab.Panel',
|
||||
xtype: 'pbsSystemConfiguration',
|
||||
@ -16,23 +14,23 @@ Ext.define('PBS.SystemConfiguration', {
|
||||
layout: {
|
||||
type: 'vbox',
|
||||
align: 'stretch',
|
||||
multi: true
|
||||
multi: true,
|
||||
},
|
||||
defaults: {
|
||||
collapsible: true,
|
||||
animCollapse: false,
|
||||
margin: '10 10 0 10'
|
||||
margin: '10 10 0 10',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
title: gettext('Time'),
|
||||
xtype: 'proxmoxNodeTimeView',
|
||||
nodename: 'localhost'
|
||||
nodename: 'localhost',
|
||||
},
|
||||
{
|
||||
title: gettext('DNS'),
|
||||
xtype: 'proxmoxNodeDNSView',
|
||||
nodename: 'localhost'
|
||||
nodename: 'localhost',
|
||||
},
|
||||
{
|
||||
flex: 1,
|
||||
@ -41,28 +39,22 @@ Ext.define('PBS.SystemConfiguration', {
|
||||
xtype: 'proxmoxNodeNetworkView',
|
||||
showApplyBtn: true,
|
||||
types: ['bond', 'bridge', 'vlan'],
|
||||
nodename: 'localhost'
|
||||
nodename: 'localhost',
|
||||
},
|
||||
]
|
||||
// },
|
||||
// {
|
||||
// itemId: 'options',
|
||||
// title: gettext('Options'),
|
||||
// html: "TESWT"
|
||||
// xtype: 'pbsSystemOptions'
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
let me = this;
|
||||
|
||||
me.callParent();
|
||||
|
||||
var networktime = me.getComponent('network');
|
||||
let networktime = me.getComponent('network');
|
||||
Ext.Array.forEach(networktime.query(), function(item) {
|
||||
item.relayEvents(networktime, [ 'activate', 'deactivate', 'destroy']);
|
||||
item.relayEvents(networktime, ['activate', 'deactivate', 'destroy']);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
33
www/Utils.js
33
www/Utils.js
@ -1,4 +1,3 @@
|
||||
/*global Proxmox */
|
||||
Ext.ns('PBS');
|
||||
|
||||
console.log("Starting Backup Server GUI");
|
||||
@ -7,7 +6,6 @@ Ext.define('PBS.Utils', {
|
||||
singleton: true,
|
||||
|
||||
updateLoginData: function(data) {
|
||||
|
||||
Proxmox.Utils.setAuthData(data);
|
||||
},
|
||||
|
||||
@ -74,13 +72,13 @@ Ext.define('PBS.Utils', {
|
||||
render_datastore_worker_id: function(id, what) {
|
||||
const res = id.match(/^(\S+?)_(\S+?)_(\S+?)(_(.+))?$/);
|
||||
if (res) {
|
||||
let datastore = res[1], type = res[2], id = res[3];
|
||||
let datastore = res[1], backupGroup = `${res[2]}/${res[3]}`;
|
||||
if (res[4] !== undefined) {
|
||||
let datetime = Ext.Date.parse(parseInt(res[5], 16), 'U');
|
||||
let utctime = PBS.Utils.render_datetime_utc(datetime);
|
||||
return `Datastore ${datastore} ${what} ${type}/${id}/${utctime}`;
|
||||
return `Datastore ${datastore} ${what} ${backupGroup}/${utctime}`;
|
||||
} else {
|
||||
return `Datastore ${datastore} ${what} ${type}/${id}`;
|
||||
return `Datastore ${datastore} ${what} ${backupGroup}`;
|
||||
}
|
||||
}
|
||||
return `Datastore ${what} ${id}`;
|
||||
@ -91,21 +89,14 @@ Ext.define('PBS.Utils', {
|
||||
|
||||
// do whatever you want here
|
||||
Proxmox.Utils.override_task_descriptions({
|
||||
garbage_collection: ['Datastore', gettext('Garbage collect') ],
|
||||
sync: ['Datastore', gettext('Remote Sync') ],
|
||||
syncjob: [gettext('Sync Job'), gettext('Remote Sync') ],
|
||||
prune: (type, id) => {
|
||||
return PBS.Utils.render_datastore_worker_id(id, gettext('Prune'));
|
||||
},
|
||||
verify: (type, id) => {
|
||||
return PBS.Utils.render_datastore_worker_id(id, gettext('Verify'));
|
||||
},
|
||||
backup: (type, id) => {
|
||||
return PBS.Utils.render_datastore_worker_id(id, gettext('Backup'));
|
||||
},
|
||||
reader: (type, id) => {
|
||||
return PBS.Utils.render_datastore_worker_id(id, gettext('Read objects'));
|
||||
},
|
||||
garbage_collection: ['Datastore', gettext('Garbage collect')],
|
||||
sync: ['Datastore', gettext('Remote Sync')],
|
||||
syncjob: [gettext('Sync Job'), gettext('Remote Sync')],
|
||||
prune: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Prune')),
|
||||
verify: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Verify')),
|
||||
backup: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Backup')),
|
||||
reader: (type, id) => PBS.Utils.render_datastore_worker_id(id, gettext('Read objects')),
|
||||
logrotate: [gettext('Log'), gettext('Rotation')],
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -1,19 +1,18 @@
|
||||
/*global Proxmox*/
|
||||
Ext.define('PBS.view.main.VersionInfo',{
|
||||
Ext.define('PBS.view.main.VersionInfo', {
|
||||
extend: 'Ext.Component',
|
||||
xtype: 'versioninfo',
|
||||
|
||||
makeApiCall: true,
|
||||
|
||||
data: {
|
||||
version: false
|
||||
version: false,
|
||||
},
|
||||
|
||||
tpl: [
|
||||
'Backup Server',
|
||||
'<tpl if="version">',
|
||||
' {version}-{release}',
|
||||
'</tpl>'
|
||||
'</tpl>',
|
||||
],
|
||||
|
||||
initComponent: function() {
|
||||
@ -26,8 +25,8 @@ Ext.define('PBS.view.main.VersionInfo',{
|
||||
method: 'GET',
|
||||
success: function(response) {
|
||||
me.update(response.result.data);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -14,7 +14,7 @@ Ext.define('PBS.admin.ZFSList', {
|
||||
nodename: me.nodename,
|
||||
listeners: {
|
||||
destroy: function() { me.reload(); },
|
||||
}
|
||||
},
|
||||
}).show();
|
||||
},
|
||||
|
||||
@ -49,7 +49,7 @@ Ext.define('PBS.admin.ZFSList', {
|
||||
}
|
||||
|
||||
let url = `/api2/json/nodes/${view.nodename}/disks/zfs`;
|
||||
view.getStore().getProxy().setUrl(url)
|
||||
view.getStore().getProxy().setUrl(url);
|
||||
|
||||
Proxmox.Utils.monStoreErrors(view, view.getStore(), true);
|
||||
|
||||
@ -61,34 +61,34 @@ Ext.define('PBS.admin.ZFSList', {
|
||||
{
|
||||
text: gettext('Name'),
|
||||
dataIndex: 'name',
|
||||
flex: 1
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
header: gettext('Size'),
|
||||
renderer: Proxmox.Utils.format_size,
|
||||
dataIndex: 'size'
|
||||
dataIndex: 'size',
|
||||
},
|
||||
{
|
||||
header: gettext('Free'),
|
||||
renderer: Proxmox.Utils.format_size,
|
||||
dataIndex: 'free'
|
||||
dataIndex: 'free',
|
||||
},
|
||||
{
|
||||
header: gettext('Allocated'),
|
||||
renderer: Proxmox.Utils.format_size,
|
||||
dataIndex: 'alloc'
|
||||
dataIndex: 'alloc',
|
||||
},
|
||||
{
|
||||
header: gettext('Fragmentation'),
|
||||
renderer: function(value) {
|
||||
return value.toString() + '%';
|
||||
},
|
||||
dataIndex: 'frag'
|
||||
dataIndex: 'frag',
|
||||
},
|
||||
{
|
||||
header: gettext('Health'),
|
||||
renderer: Proxmox.Utils.render_zfs_health,
|
||||
dataIndex: 'health'
|
||||
dataIndex: 'health',
|
||||
},
|
||||
{
|
||||
header: gettext('Deduplication'),
|
||||
@ -96,8 +96,8 @@ Ext.define('PBS.admin.ZFSList', {
|
||||
renderer: function(value) {
|
||||
return value.toFixed(2).toString() + 'x';
|
||||
},
|
||||
dataIndex: 'dedup'
|
||||
}
|
||||
dataIndex: 'dedup',
|
||||
},
|
||||
],
|
||||
|
||||
rootVisible: false,
|
||||
@ -118,7 +118,7 @@ Ext.define('PBS.admin.ZFSList', {
|
||||
xtype: 'proxmoxButton',
|
||||
disabled: true,
|
||||
handler: 'openDetailWindow',
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
listeners: {
|
||||
@ -130,7 +130,7 @@ Ext.define('PBS.admin.ZFSList', {
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
},
|
||||
sorters: 'name'
|
||||
sorters: 'name',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -47,7 +47,7 @@ Ext.define('PBS.config.ACLView', {
|
||||
removeACL: function(btn, event, rec) {
|
||||
let me = this;
|
||||
Proxmox.Utils.API2Request({
|
||||
url:'/access/acl',
|
||||
url: '/access/acl',
|
||||
method: 'PUT',
|
||||
params: {
|
||||
'delete': 1,
|
||||
@ -58,7 +58,7 @@ Ext.define('PBS.config.ACLView', {
|
||||
callback: function() {
|
||||
me.reload();
|
||||
},
|
||||
failure: function (response, opts) {
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
});
|
||||
|
@ -1,11 +1,11 @@
|
||||
Ext.define('pbs-datastore-list', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [ 'name', 'comment' ],
|
||||
fields: ['name', 'comment'],
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: "/api2/json/admin/datastore"
|
||||
url: "/api2/json/admin/datastore",
|
||||
},
|
||||
idProperty: 'store'
|
||||
idProperty: 'store',
|
||||
});
|
||||
|
||||
Ext.define('pbs-data-store-config', {
|
||||
@ -209,7 +209,7 @@ Ext.define('PBS.DataStoreConfig', {
|
||||
dataIndex: 'keep-yearly',
|
||||
width: 70,
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
header: gettext('Comment'),
|
||||
|
@ -1,6 +1,21 @@
|
||||
Ext.define('pmx-remotes', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [ 'name', 'host', 'userid', 'fingerprint', 'comment' ],
|
||||
fields: ['name', 'host', 'port', 'userid', 'fingerprint', 'comment',
|
||||
{
|
||||
name: 'server',
|
||||
calculate: function(data) {
|
||||
let txt = data.host || "localhost";
|
||||
let port = data.port || "8007";
|
||||
if (port.toString() !== "8007") {
|
||||
if (Proxmox.Utils.IP6_match.test(txt)) {
|
||||
txt = `[${txt}]`;
|
||||
}
|
||||
txt += `:${port}`;
|
||||
}
|
||||
return txt;
|
||||
},
|
||||
},
|
||||
],
|
||||
idProperty: 'name',
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
@ -109,7 +124,7 @@ Ext.define('PBS.config.RemoteView', {
|
||||
header: gettext('Host'),
|
||||
width: 200,
|
||||
sortable: true,
|
||||
dataIndex: 'host',
|
||||
dataIndex: 'server',
|
||||
},
|
||||
{
|
||||
header: gettext('User name'),
|
||||
|
@ -69,7 +69,7 @@ Ext.define('PBS.config.SyncJobView', {
|
||||
if (!upid) return;
|
||||
|
||||
Ext.create('Proxmox.window.TaskViewer', {
|
||||
upid
|
||||
upid,
|
||||
}).show();
|
||||
},
|
||||
|
||||
@ -116,13 +116,13 @@ Ext.define('PBS.config.SyncJobView', {
|
||||
text = Proxmox.Utils.unknownText;
|
||||
break;
|
||||
case 'error':
|
||||
icon = 'times critical';
|
||||
icon = 'times critical';
|
||||
text = Proxmox.Utils.errorText + ': ' + value;
|
||||
break;
|
||||
case 'warning':
|
||||
icon = 'exclamation warning';
|
||||
break;
|
||||
case 'ok':
|
||||
case 'ok':
|
||||
icon = 'check good';
|
||||
text = gettext("OK");
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ Ext.define('pbs-datastore-statistics', {
|
||||
}
|
||||
return last;
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'usage',
|
||||
@ -25,13 +25,13 @@ Ext.define('pbs-datastore-statistics', {
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
proxy: {
|
||||
type: 'proxmox',
|
||||
url: "/api2/json/status/datastore-usage"
|
||||
url: "/api2/json/status/datastore-usage",
|
||||
},
|
||||
idProperty: 'store',
|
||||
});
|
||||
@ -57,7 +57,7 @@ Ext.define('PBS.DatastoreStatistics', {
|
||||
|
||||
let timespan = (estimate - now)/1000;
|
||||
|
||||
if (+estimate <= +now || isNaN(timespan)) {
|
||||
if (Number(estimate) <= Number(now) || isNaN(timespan)) {
|
||||
return gettext('Never');
|
||||
}
|
||||
|
||||
@ -131,8 +131,8 @@ Ext.define('PBS.DatastoreStatistics', {
|
||||
lineWidth: 0,
|
||||
chartRangeMin: 0,
|
||||
chartRangeMax: 1,
|
||||
tipTpl: '{y:number("0.00")*100}%'
|
||||
}
|
||||
tipTpl: '{y:number("0.00")*100}%',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@ -150,4 +150,4 @@ Ext.define('PBS.DatastoreStatistics', {
|
||||
},
|
||||
},
|
||||
|
||||
})
|
||||
});
|
||||
|
@ -14,7 +14,6 @@ Ext.define('PBS.LongestTasks', {
|
||||
|
||||
openTask: function(record) {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
Ext.create('Proxmox.window.TaskViewer', {
|
||||
upid: record.data.upid,
|
||||
endtime: record.data.endtime,
|
||||
@ -72,7 +71,7 @@ Ext.define('PBS.LongestTasks', {
|
||||
model: 'proxmox-tasks',
|
||||
proxy: {
|
||||
type: 'memory',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -8,6 +8,13 @@ Ext.define('PBS.RunningTasks', {
|
||||
hideHeaders: true,
|
||||
rowLines: false,
|
||||
|
||||
scrollable: true,
|
||||
maxHeight: 500,
|
||||
|
||||
viewConfig: {
|
||||
deferEmptyText: false,
|
||||
},
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
@ -80,7 +87,7 @@ Ext.define('PBS.RunningTasks', {
|
||||
dataIndex: 'duration',
|
||||
renderer: function(value, md, record) {
|
||||
return Proxmox.Utils.format_duration_human((Date.now() - record.data.starttime)/1000);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'actioncolumn',
|
||||
|
@ -69,7 +69,7 @@ Ext.define('PBS.TaskSummary', {
|
||||
disableSelection: true,
|
||||
|
||||
store: {
|
||||
data: []
|
||||
data: [],
|
||||
},
|
||||
|
||||
columns: [
|
||||
@ -90,7 +90,7 @@ Ext.define('PBS.TaskSummary', {
|
||||
renderer: 'render_count',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
});
|
||||
|
@ -4,7 +4,7 @@
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<title>Proxmox Backup Server</title>
|
||||
<title>{{ NodeName }} - Proxmox Backup Server</title>
|
||||
<link rel="icon" sizes="128x128" href="/images/logo-128.png" />
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="/pve2/images/logo-128.png" />
|
||||
<link rel="stylesheet" type="text/css" href="/extjs/theme-crisp/resources/theme-crisp-all.css" />
|
||||
|
@ -3,6 +3,8 @@ Ext.define('PBS.window.ACLEdit', {
|
||||
alias: 'widget.pbsACLAdd',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
onlineHelp: 'user_acl',
|
||||
|
||||
url: '/access/acl',
|
||||
method: 'PUT',
|
||||
isAdd: true,
|
||||
|
@ -71,11 +71,11 @@ Ext.define('PBS.window.BackupFileDownloader', {
|
||||
|
||||
control: {
|
||||
'proxmoxComboGrid': {
|
||||
change: 'changeFile'
|
||||
change: 'changeFile',
|
||||
},
|
||||
'button': {
|
||||
click: 'downloadFile',
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -89,7 +89,7 @@ Ext.define('PBS.window.BackupFileDownloader', {
|
||||
emptyText: gettext('No file selected'),
|
||||
fieldLabel: gettext('File'),
|
||||
store: {
|
||||
fields: ['filename', 'size', 'crypt-mode',],
|
||||
fields: ['filename', 'size', 'crypt-mode'],
|
||||
idProperty: ['filename'],
|
||||
},
|
||||
listConfig: {
|
||||
@ -115,7 +115,7 @@ Ext.define('PBS.window.BackupFileDownloader', {
|
||||
mode = PBS.Utils.cryptmap.indexOf(value);
|
||||
}
|
||||
return PBS.Utils.cryptText[mode] || Proxmox.Utils.unknownText;
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -133,7 +133,7 @@ Ext.define('PBS.window.BackupFileDownloader', {
|
||||
reference: 'encryptedHint',
|
||||
hidden: true,
|
||||
value: gettext('Encrypted Files cannot be decoded on the server directly. Please use the client where the decryption key is located.'),
|
||||
}
|
||||
},
|
||||
],
|
||||
|
||||
buttons: [
|
||||
|
@ -3,6 +3,9 @@ Ext.define('PBS.DataStoreEdit', {
|
||||
alias: 'widget.pbsDataStoreEdit',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
|
||||
onlineHelp: 'datastore_intro',
|
||||
|
||||
subject: gettext('Datastore'),
|
||||
isAdd: true,
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
Ext.define('pbs-file-tree', {
|
||||
extend: 'Ext.data.Model',
|
||||
|
||||
fields: [ 'filepath', 'text', 'type', 'size',
|
||||
fields: ['filepath', 'text', 'type', 'size',
|
||||
{
|
||||
name: 'mtime',
|
||||
type: 'date',
|
||||
@ -43,7 +43,7 @@ Ext.define('pbs-file-tree', {
|
||||
|
||||
return `fa fa-${icon}`;
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
idProperty: 'filepath',
|
||||
});
|
||||
@ -85,15 +85,15 @@ Ext.define("PBS.window.FileBrowser", {
|
||||
'backup-type': view['backup-type'],
|
||||
'backup-time': view['backup-time'],
|
||||
};
|
||||
params['filepath'] = data.filepath;
|
||||
params.filepath = data.filepath;
|
||||
atag.download = data.text;
|
||||
atag.href = me.buildUrl(`/api2/json/admin/datastore/${view.datastore}/pxar-file-download`, params);
|
||||
atag.href = me
|
||||
.buildUrl(`/api2/json/admin/datastore/${view.datastore}/pxar-file-download`, params);
|
||||
atag.click();
|
||||
},
|
||||
|
||||
fileChanged: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
let tree = me.lookup('tree');
|
||||
let selection = tree.getSelection();
|
||||
if (!selection || selection.length < 1) return;
|
||||
@ -204,7 +204,7 @@ Ext.define("PBS.window.FileBrowser", {
|
||||
|
||||
return asize - bsize;
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
text: gettext('Modified'),
|
||||
@ -226,10 +226,10 @@ Ext.define("PBS.window.FileBrowser", {
|
||||
case 's': return gettext('Socket');
|
||||
default: return Proxmox.Utils.unknownText;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
buttons: [
|
||||
@ -238,6 +238,6 @@ Ext.define("PBS.window.FileBrowser", {
|
||||
handler: 'downloadFile',
|
||||
reference: 'downloadBtn',
|
||||
disabled: true,
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -3,6 +3,8 @@ Ext.define('PBS.window.RemoteEdit', {
|
||||
alias: 'widget.pbsRemoteEdit',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
onlineHelp: 'backup_remote',
|
||||
|
||||
userid: undefined,
|
||||
|
||||
isAdd: true,
|
||||
@ -43,8 +45,45 @@ Ext.define('PBS.window.RemoteEdit', {
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
allowBlank: false,
|
||||
name: 'host',
|
||||
name: 'hostport',
|
||||
submitValue: false,
|
||||
vtype: 'HostPort',
|
||||
fieldLabel: gettext('Host'),
|
||||
listeners: {
|
||||
change: function(field, newvalue) {
|
||||
let host = newvalue;
|
||||
let port;
|
||||
|
||||
let match = Proxmox.Utils.HostPort_match.exec(newvalue);
|
||||
if (match === null) {
|
||||
match = Proxmox.Utils.HostPortBrackets_match.exec(newvalue);
|
||||
if (match === null) {
|
||||
match = Proxmox.Utils.IP6_dotnotation_match.exec(newvalue);
|
||||
}
|
||||
}
|
||||
|
||||
if (match !== null) {
|
||||
host = match[1];
|
||||
if (match[2] !== undefined) {
|
||||
port = match[2];
|
||||
}
|
||||
}
|
||||
|
||||
field.up('inputpanel').down('field[name=host]').setValue(host);
|
||||
field.up('inputpanel').down('field[name=port]').setValue(port);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
hidden: true,
|
||||
name: 'host',
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
hidden: true,
|
||||
deleteEmpty: true,
|
||||
name: 'port',
|
||||
},
|
||||
],
|
||||
|
||||
@ -71,16 +110,33 @@ Ext.define('PBS.window.RemoteEdit', {
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
name: 'fingerprint',
|
||||
deleteEmpty: true,
|
||||
fieldLabel: gettext('Fingerprint'),
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxtextfield',
|
||||
name: 'comment',
|
||||
deleteEmpty: true,
|
||||
fieldLabel: gettext('Comment'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
setValues: function(values) {
|
||||
let me = this;
|
||||
|
||||
let host = values.host;
|
||||
if (values.port !== undefined) {
|
||||
if (Proxmox.Utils.IP6_match.test(host)) {
|
||||
host = `[${host}]`;
|
||||
}
|
||||
host += `:${values.port}`;
|
||||
}
|
||||
values.hostport = host;
|
||||
|
||||
return me.callParent([values]);
|
||||
},
|
||||
|
||||
getValues: function() {
|
||||
let me = this;
|
||||
let values = me.callParent(arguments);
|
||||
|
@ -3,6 +3,8 @@ Ext.define('PBS.window.UserEdit', {
|
||||
alias: 'widget.pbsUserEdit',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
onlineHelp: 'user_mgmt',
|
||||
|
||||
userid: undefined,
|
||||
|
||||
isAdd: true,
|
||||
|
@ -30,8 +30,8 @@ Ext.define('PBS.window.CreateZFS', {
|
||||
xtype: 'proxmoxcheckbox',
|
||||
name: 'add-datastore',
|
||||
fieldLabel: gettext('Add as Datastore'),
|
||||
value: '1'
|
||||
}
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
column2: [
|
||||
{
|
||||
@ -45,8 +45,8 @@ Ext.define('PBS.window.CreateZFS', {
|
||||
['raid10', 'RAID10'],
|
||||
['raidz', 'RAIDZ'],
|
||||
['raidz2', 'RAIDZ2'],
|
||||
['raidz3', 'RAIDZ3']
|
||||
]
|
||||
['raidz3', 'RAIDZ3'],
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxKVComboBox',
|
||||
@ -59,8 +59,8 @@ Ext.define('PBS.window.CreateZFS', {
|
||||
['gzip', 'gzip'],
|
||||
['lz4', 'lz4'],
|
||||
['lzjb', 'lzjb'],
|
||||
['zle', 'zle']
|
||||
]
|
||||
['zle', 'zle'],
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxintegerfield',
|
||||
@ -68,8 +68,8 @@ Ext.define('PBS.window.CreateZFS', {
|
||||
minValue: 9,
|
||||
maxValue: 16,
|
||||
value: '12',
|
||||
name: 'ashift'
|
||||
}
|
||||
name: 'ashift',
|
||||
},
|
||||
],
|
||||
columnB: [
|
||||
{
|
||||
@ -80,8 +80,8 @@ Ext.define('PBS.window.CreateZFS', {
|
||||
valueField: 'name',
|
||||
height: 200,
|
||||
emptyText: gettext('No Disks unused'),
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'displayfield',
|
||||
|
Reference in New Issue
Block a user