Compare commits

..

626 Commits

Author SHA1 Message Date
1ff840ffad bump version to 0.7.0-1 2020-07-07 07:40:22 +02:00
7443a6e092 src/client/remote_chunk_reader.rs: implement clone for RemoteChunkReader 2020-07-07 07:34:58 +02:00
3a9988638b docs: move todolist to own document, don't link in release build
It is always build for html, but not linked if the devbuild tag isn't
set. This tag is set in the Makefile if the $(BUILD_MODE) variable
isn't "release".

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-06 14:44:53 +02:00
96ee857752 client: add --encryption boolen parameter
This can be used to explicitly disable encryption even if a
default key file exists in ~/.config.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-07-06 14:36:04 +02:00
887018bb79 client: use default encryption key if it is available
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-07-06 14:36:04 +02:00
9696f5193b client: move key management into separate module
and use api macro for methods and Kdf type

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-07-06 14:36:04 +02:00
e13c4f66bb minor style & whitespace fixups
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-07-06 10:55:25 +02:00
8a25809573 docs: sync up copyright years
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-04 17:57:47 +02:00
d87b193b0b docs: todo: avoid leaking build details, link only
One can just search for them... If really wanted, we could set it to
true for dev builds (i.e., no DEB_VERSION defined)

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-04 17:54:00 +02:00
ea5289e869 d/rules: do not compress .pdf files
as else the docs .pdf is a PITA to use for some endusers..

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-04 17:53:04 +02:00
1f6a4f587a docs: do not hardcode version
use the debian package ones, if not defined we're doing a dev build

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-04 17:51:58 +02:00
705b2293ec d/control: add missing dependencies for lvm, smartmontools and ZFS
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-03 19:37:43 +02:00
d2c7ef09ba docs: rework and add a bit to introduction
Contributed-by: Daniela Häsler <daniela@proxmox.com>
[ discussed and edited some parts live with me, Thomas ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-03 18:58:17 +02:00
27f86f997e docs: fix index title
Contributed-by: Daniela Häsler <daniela@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-03 18:57:04 +02:00
fc93d38076 ui: ZFS create: set name-field minLength to 3 to match backend
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-03 18:03:51 +02:00
a5a85d41ff ui: ZFS create: use correct typeParameter name for disk selector
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-03 18:00:12 +02:00
08cb2038bd api: disks: indentation fixup
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-03 17:59:30 +02:00
6f711c1737 ui: ZFS list: fix details top-bar button handler
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-03 17:20:33 +02:00
42ec9f577f ui: buildsys: actually include PBS.window.ZFSCreate component in source
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-03 17:19:59 +02:00
9de69cdb1a src/bin/proxmox_backup_client/catalog.rs: split out catalog code 2020-07-03 16:45:47 +02:00
bd260569d3 ui: fix glitch on some zoom steps
if the baseCls is not 'x-plain' the background of the flex
element is white, and on some zoom steps it gets taller
than one pixel and appears as a white line

making it have the plain baseCls, so it does not get any
background color and is always invisible

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-07-03 13:05:19 +02:00
36cb4b30ef add beta text with link to bugtracker
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-07-03 13:05:08 +02:00
4e717240bf bump version to 0.6.0-1 2020-07-03 09:46:19 +02:00
e9764238df make ReadChunk not require mutable self.
That way we can reduce lock contentions because we lock for much shorter
times.
2020-07-03 07:37:29 +02:00
26f499b17b ui: increase timeout for snapshot listing
the api call can take a very long time (for now), until we can
improve that, increase the timeout from the default of 30s

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-07-03 06:14:21 +02:00
cc7995ac40 src/bin/proxmox_backup_client/task.rs: split out task command 2020-07-02 18:04:29 +02:00
43abba4b4f src/bin/proxmox_backup_client/mount.rs: split out mount code 2020-07-02 17:49:59 +02:00
58f950c546 ui: consistently spell Datastore without space between words
Not even hard feeling on 'Datastore' vs. 'Data Store' but consistency
is desired in such names.
Talked shortly with Dominik, which also slightly favored the one
without space - so just go for that one.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-02 17:20:41 +02:00
c426e65893 ui: disk create: sync and improve 'add-datastore' checkbox label
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-07-02 17:06:37 +02:00
caea8d611f proxmox-backup-client: add benchmark command
This is just a start, We need to add more useful things here...
2020-07-02 14:01:57 +02:00
7d0754a6d2 pxar: fixup 'vanished-file' logic a bit
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-30 14:41:42 +02:00
5afa0755ea pxar: fix missing newlines in warnings
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-30 14:37:20 +02:00
40b63186a6 DataStoreConfig.js: add verify button 2020-06-30 13:28:42 +02:00
8f6088c130 DataStoreContent.js: add verify button 2020-06-30 13:22:02 +02:00
2162e2c15d src/api2/admin/datastore.rs: avoid slash in UPID strings 2020-06-30 13:11:22 +02:00
0d5ab04a90 bump version to 0.5.0-1 2020-06-29 13:01:11 +02:00
4059285649 fix typo 2020-06-29 12:59:25 +02:00
2e079b8bf2 partially revert commit 1f82f9b7b5
do it backwards compatible. Also, code was wrong because FixedIndexWriter
still computed old style csums...
2020-06-29 12:44:45 +02:00
4ff2c9b832 ui: allow to Forget (delete) backup snapshots. 2020-06-26 15:58:06 +02:00
a8e2940ff3 pxar: deal with files changing size during archiving
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-26 11:49:51 +02:00
d5d5f2174e bump version to 0.4.0-1 2020-06-26 10:43:52 +02:00
2311238450 depend on proxmox 0.1.41 2020-06-26 10:40:47 +02:00
2ea501ffdf ui: add ZFS management
adds a ZFSList and ZFSCreate class, modeled after the one in pve

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-26 10:33:23 +02:00
4eb4e94918 fix test output
field separator for pools is always a tab when using -H

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-26 10:31:11 +02:00
817bcda848 src/backup/verify.rs: do not stop on server shutdown
This is a read-only task, so there is no need to stop.
2020-06-26 09:45:59 +02:00
f6de2c7359 WorkerTask: add warnings and count them
so that we have one level more between errors and OK

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-26 09:42:11 +02:00
3f0b9c10ec ui: dashboard: remove 'wobbling' of tasks that have the same duration
by sorting them by upid after sorting by duration

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-26 09:13:33 +02:00
2b66abbfab ui: dashboard: use last value for holes in history graph
it is only designed to be a quick overview, so having holes there
is not really pretty and since we do not even show any date
for the points, we can simply reuse the last value for holes

the 'real' graph with holes is still available on the
DataStoreStatistics panel

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-26 09:13:16 +02:00
402c8861d8 fix typo
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-26 09:12:29 +02:00
3f683799a8 improve 'debug' parameter
instead of checking on '1' or 'true', check that it is there and not
'0' and 'false'. this allows using simply

https://foo:8007/?debug

instead of

https://foo:8007/?debug=1

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-26 09:12:14 +02:00
573bcd9a92 ui: automatically add 'localhost' as nodename for all panels
this will make refactoring easier for panels that are reused from pve
(where we always have a hostname)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-26 09:11:36 +02:00
90779237ae ui: show proper loadMask for DataStoreContent
we have to use the correct store, and we have to manually show the
error (since monStoreErrors only works for Proxmox Proxies)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-26 09:11:10 +02:00
1f82f9b7b5 src/backup/index.rs: add compute_csum
And use it for fixed and dynamic index. Please note that this
changes checksums for fixed indexes, so restore older backups
will fails now (not backward compatible).
2020-06-26 09:00:34 +02:00
19b5c3c43e examples/upload-speed.rs: fix compile error 2020-06-26 08:59:51 +02:00
fe3e65c3ea src/api2/backup.rs: call register_chunk in previous download api 2020-06-26 08:22:46 +02:00
fdaab0df4e src/backup/index.rs: add chunk_info method 2020-06-26 08:14:45 +02:00
b957aa81bd update backup api for incremental backup
Signed-off-by: Dietmar Maurer <dietmar@proxmox.com>
Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2020-06-26 07:17:08 +02:00
8ea00f6e49 allow to abort verify jobs
And improve job description rendering on gui.
2020-06-25 12:56:36 +02:00
4bd789b0fa ui: file browser: expand child node if only one archive present
Get the first visible node through the Ext.data.NodeInterface defined
"firstChild" element and expand that if there's only one archive
present.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-06-25 12:52:42 +02:00
2f050cf2ed ui: file browser: adapt height for 4:3 instead of weird 2:1 ratio
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-06-25 11:59:23 +02:00
e22f4882e7 extract create_download_response API helper
and put it into a new "api2::helpers" module.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2020-06-25 11:57:37 +02:00
c65bc99a41 [chore] bump to using pxar 0.2.0
This breaks all previously created pxar archives!

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-25 09:46:56 +02:00
355c055e81 src/bin/proxmox-backup-manager.rs: implement verify 2020-06-24 13:35:21 +02:00
c2009e5309 src/api2/admin/datastore.rs: add verify api 2020-06-24 13:35:21 +02:00
23f74c190e src/backup/backup_info.rs: impl Display for BackupGroup 2020-06-24 13:35:21 +02:00
a6f8728339 update to pxar 0.1.9, update ReadAt implementations
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-24 11:57:12 +02:00
c1769a749c bump version to 0.3.0-1 2020-06-24 10:13:56 +02:00
facd9801cf add incremental backup support
To support incremental backups (where not all chunks are sent to the
server), a new parameter "reuse-csum" is introduced on the
"create_fixed_index" API call. When set and equal to last backups'
checksum, the backup writer clones the data from the last index of this
archive file, and only updates chunks it actually receives.

In incremental mode some checks usually done on closing an index cannot
be made, since they would be inaccurate.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2020-06-24 10:01:25 +02:00
21302088de remove debug println 2020-06-24 09:15:13 +02:00
8268c9d161 fix overflow panic during upload
if *only* data chunks are registered (high chance during incremental
backup), then chunk_count might be one lower then upload_stat.count
because of the zero chunk being unconditionally uploaded but not used.
Thus when subtracting the two, an overflow would occur.

In general, don't let the client make the server panic, instead just set
duplicates to 0.

Signed-off-by: Stefan Reiter <s.reiter@proxmox.com>
2020-06-24 09:07:22 +02:00
b91b7d9ffd api2/node/disks/zfs: check if default zfs mount path exists
and if it does bail, because otherwise we would get an
error on mounting and have a zpool that is not imported
and disks that are used

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:22:39 +02:00
6e1f0c138f ui: fix missing deleteEmpty on SyncJobEdit
else we cannot delete those fields

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:21:59 +02:00
8567c0d29c ui: add pxar FileBrowser
for unencrypted backups, enables browsing the pxar archives and
downloading single files from it

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:21:33 +02:00
d33d8f4e6a api2/admin/datastore: add pxar-file-download api call
streams a file from a pxar file of an unencrypted backup

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:21:15 +02:00
5b1cfa01f1 api2/admin/datastore: add 'catalog' api call
returns the dir listing of the given filepath of the backup snapshot
the filepath has to be base64 encoded or 'root'

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:16:12 +02:00
05d18b907a add From<&DirEntryAttribute to CatalogEntryType and make it pub(crate)
we want to get a string representation of the DirEntryAttribute
like 'f' for file, etc. and since we have such a mapping already
in the CatalogEntryType, use that

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:08:50 +02:00
e44fe0c9f5 derive Clone for the LocalChunkReader
this will be necessary for accessing local pxar behind didx files

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:07:28 +02:00
4cf0ced950 add LocalDynamicReadAt
mostly copied from BufferedDynamicReadAt from proxmox-backup-client
but the reader is wrapped in an Arc in addition to the Mutex

we will use this for local access to a pxar behind a didx file

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:05:31 +02:00
98425309b0 ui: add BackupFileDownloader
enables to be able to download whole files from the backup (e.g.
the decoded didx/fidx/blobs) for unencrypted backups

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:01:56 +02:00
7b1e26699d ui: fix sorting of backup snapshots
we have to sort the treestore not the original store where we get the data

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:01:09 +02:00
676b0fde49 ui: fix encrypted column
do not use two different gettexts for groups and single backups
and correct logic for backup groups

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-24 07:00:32 +02:00
60f9a6ea8f src/backup/datastore.rs: add new helpers to load blobs and verify chunks 2020-06-24 06:58:14 +02:00
1090fd4424 src/backup/data_blob.rs: cleanup - improve code reuse 2020-06-24 06:56:48 +02:00
92c3fd2e22 src/backup/chunk_store.rs: allow to read name()
This is helpful for logging ...
2020-06-24 06:54:21 +02:00
e3efaa1972 ui: fix undefined data for iodelay
if ios are 0 and io_ticks are defined, the io_delay is zero

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-23 11:40:08 +02:00
0cf2b6441e tests/prune.rs: fix compile error 2020-06-23 11:32:53 +02:00
d6d3b353be cleanup: implement FromStr for BackupGroup 2020-06-23 08:16:56 +02:00
a67f7d0a07 cleanup: implement FromStr for BackupDir 2020-06-23 08:09:52 +02:00
c8137518fe src/bin/proxmox_backup_manager/disk.rs: add renderer for wearout
So that we display the same value as the GUI.
2020-06-23 07:44:09 +02:00
cbef49bf4f remove absolute paths when executing binaries
we set the paths manually, so this is ok

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-23 07:09:06 +02:00
0b99e5aebc remove debug prints
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-23 06:33:58 +02:00
29c55e5fc4 remove tokio:main from download-speed example
we use proxmox_backup::tools::runtime::main already

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-23 06:33:48 +02:00
f386f512d0 add AsyncReaderStream
and replace AsyncIndexReader's stream implementation with that

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-23 06:33:31 +02:00
3ddb14889a src/tools/daemon.rs: reopen STDOUT/STDERR journald streams to get correct PID in logs 2020-06-22 13:06:53 +02:00
00c2327564 bump pxar dep to 0.1.8
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-22 11:10:22 +02:00
d79926795a debian/postinst: use try-reload-or-restart for both services 2020-06-22 10:59:13 +02:00
c08fac4d69 tools::daemon: sync with child after MainPid message
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-22 10:58:04 +02:00
c40440092d tools: add socketpair helper
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-22 10:49:56 +02:00
dc2ef2b54f tools::daemon: fetch exe name in the beginning
We get the path to our executable via a readlink() on
"/proc/self/exe", which appends a " (deleted)" during
package reloads.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-22 10:31:54 +02:00
b28253d650 try to reload proxmox-backup-proxy on package upgrade 2020-06-22 09:39:37 +02:00
f28cfb322a avoid compiler warnings 2020-06-20 07:24:02 +02:00
3bbe291c51 zpool_status.rs - indented_list_to_tree: do not set name property
This is no necessary. We only touch/set 'children' and 'leaf' properties.
2020-06-20 07:19:25 +02:00
42d19fdf69 src/api2/node/disks/zfs.rs: always set pool name 2020-06-20 07:15:32 +02:00
215968e033 src/tools/disks/zpool_status.rs: add 'leaf' attribute to root node, rename 'prev' into 'parent' 2020-06-20 06:49:06 +02:00
eddd1a1b9c src/tools/disks/zpool_status.rs: move use clause top of file 2020-06-20 06:17:22 +02:00
d2ce211899 fixup for previous commit 2020-06-20 06:15:26 +02:00
1cb46c6f65 src/tools/disks/zpool_status.rs - cleanup: use struct StackItem instead of tuple 2020-06-19 18:58:57 +02:00
5d88c3a1c8 src/tools/disks/zpool_status.rs: remove unnecessary checks
Thos things can never happen, so simply use unwrap().
2020-06-19 18:27:39 +02:00
07fb504943 src/tools/disks/zpool_status.rs: simplify code by using serde_json::to_value 2020-06-19 17:51:13 +02:00
f675c5e978 src/tools/disks/zpool_status.rs - add all attributes to the tree 2020-06-19 16:55:28 +02:00
4e37d9ce67 add general indented_list_to_tree implementation
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-19 14:37:40 +02:00
e303077132 lru_cache: restrict and annotate Send impl
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-19 09:37:34 +02:00
6ef9bb59eb api2/admin/datastore: add download-decoded endpoint
similar to 'download', but streams the decoded file
when it is not encrypted

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 08:39:15 +02:00
eeaa2c212b impl Sync for DataBlobReader
this is safe for the reason explained in the comment

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 08:37:47 +02:00
4a3adc3de8 add AsyncIndexReader
implements AsyncRead as well as Stream for an IndexFile and a store
that implements AsyncReadChunk

we can use this to asyncread or stream the content of a FixedIndex or
DynamicIndex

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 08:32:33 +02:00
abdb976340 add Display trait to BackupDir
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 08:28:35 +02:00
3b62116ce6 implement AsyncReadChunk for LocalChunkReader
same as the sync ReadChunk but uses tokio::fs::read instead
of file_get_contents

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 07:54:23 +02:00
e005f953d9 ui: add encryption info to snapshot list
show which backups/files are encrypted in the snapshot list

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 07:43:10 +02:00
1c090810f5 api2/admin/datastore/snapshos: show encrypted and size info per file
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 07:39:56 +02:00
e181d2f6da add encrypted info to Manifest
we want to save if a file of a backup is encrypted, so that we can
* show that info on the gui
* can later decide if we need to decrypt the backup

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 07:35:39 +02:00
16021f6ab7 use the existing async method for read_raw_chunk
does the same, except the manual drop, but thats handled there by
letting the value go out of scope

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 07:23:17 +02:00
ba694720fc api2/admin/datastore: log stream error during file download
the client cannot get an error during an chunked http transfer, so at
least log it server side

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 06:58:55 +02:00
bde8e243cf remove unsafe copy code
copy_nonoverlapping is basically a memcpy which can also be done
via copy_from_slice which is not unsafe
(copy_from_slice uses copy_nonoverlapping internally)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-19 06:56:15 +02:00
3352ee5656 parse_zpool_status_field: handle tabs without copying input 2020-06-18 19:40:01 +02:00
b29cbc414d parse_zpool_status_vdev: consider tabs as 8 spaces 2020-06-18 18:38:56 +02:00
026dc1d11f src/api2/node/disks/zfs.rs: add zpool_details api 2020-06-18 15:04:46 +02:00
9438aca6c9 src/tools/disks/zpool_status.rs: improve parser 2020-06-18 14:55:22 +02:00
547f0c97e4 src/tools/nom.rs: new helper parse_complete_line() for single line parsers
Like parse_complete(), but generates simpler error messages.
2020-06-18 12:57:55 +02:00
177a2de992 src/tools/nom.rs: move nom helpers into separate file 2020-06-18 12:41:13 +02:00
0686b1f4db src/tools/disks/zpool_list.rs: split code into separate file 2020-06-18 10:31:07 +02:00
0727e56a06 src/tools/disks/zpool_status.rs: parse zpool status output 2020-06-18 10:23:15 +02:00
2fd3d57490 src/tools/disks/zfs.rs: rename ZFSPoolStatus into ZFSPoolInfo, fix error message 2020-06-17 09:08:26 +02:00
3f851d1321 src/api2/node/disks/directory.rs: add early check if disk is unused 2020-06-17 08:31:11 +02:00
1aef491e24 src/bin/proxmox_backup_manager/disk.rs: add cli to create mounted disks 2020-06-17 08:07:54 +02:00
d0eccae37d avoid compiler warning 2020-06-17 08:07:42 +02:00
a34154d900 src/tools/disks/zfs.rs: cleanup parse_pool_header 2020-06-17 07:47:11 +02:00
c2cc32b4dd src/tools/disks/zfs.rs: add more parser tests 2020-06-17 07:38:19 +02:00
46405fa35d src/tools/disks/zfs.rs: add comment 2020-06-17 07:14:26 +02:00
66af7f51bc src/tools/disks/zfs.rs: make zfs list parser private 2020-06-17 07:00:54 +02:00
c72ccd4e33 src/tools/disks/zfs.rs: add regression tests for parse_zfs_list 2020-06-16 18:14:35 +02:00
902b2cc278 src/tools/disks/zfs.rs: simplify code 2020-06-16 17:51:17 +02:00
8ecd7c9c21 move api dump binaries back to src/bin for package building
they're required for docs

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-16 14:48:01 +02:00
7f17f7444a ui: add DiskList and DirectoryList
this also contains an adapted CreateDirectory window
for now this is mostly copied, since refactoring was not that
straightforward (changed parameters, etc.)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-16 13:36:32 +02:00
fb5a066500 src/api2/node/disks.rs: expose directory api 2020-06-16 13:36:32 +02:00
d19c96d507 move test binaries to examples/
These aren't installed and are only used for manual testing,
so there's no reason to force them to be built all the time.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-16 13:32:24 +02:00
929a13b357 src/api2/node/disks/zfs.rs: add zpool api 2020-06-16 13:25:53 +02:00
36c65ee0b0 src/tools/disks/zfs.rs: cleanup (rename usage properties)
And allow to parse zpool list output without -v flag.
2020-06-16 13:25:53 +02:00
3378fd9fe5 src/tools/disks/zfs.rs: parse more infos (dedup, fragmentation, health) 2020-06-16 13:25:53 +02:00
58c51cf3d9 avoid compiler warnings 2020-06-16 13:25:53 +02:00
5509b199fb use new run_command helper 2020-06-16 13:25:53 +02:00
bb59df9134 catalog: don't panic on invalid file mtimes
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-16 11:25:54 +02:00
2564b0834f fix file timestamps in catalog
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-16 11:25:31 +02:00
9321bbd1f5 pxar: fix missing subdirectories in catalogs
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-16 11:04:56 +02:00
4264e52220 reuse some extractor code in catalog shell
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-16 10:54:54 +02:00
6988b29bdc use O_EXCL when creating files during extraction
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-16 10:33:27 +02:00
98c54240e6 pxar: make extractor state more reusable
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-16 10:32:13 +02:00
d30c192589 AsyncReadChunk: require Send
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-16 09:50:29 +02:00
67908b47fa require pxar 0.1.7
fixes some hardlink reading issues in random-accessor

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-15 10:39:09 +02:00
ac7513e368 src/tools.rs: add setup_safe_path_env() 2020-06-15 10:38:30 +02:00
fbbcd85839 src/api2/node/disks/directory.rs: implement add-datastore feature 2020-06-15 10:01:50 +02:00
7a6b549270 dynamic index: make it hard to mess up endianess
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-15 09:19:35 +02:00
0196b9bf5b remove unnecessary .into
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 14:57:58 +02:00
739a51459a ui: Dashboard: implement subscription panel
and make it nicer

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 14:52:22 +02:00
195d7c90ce ui: Dashboard: show LongestTask/RunningTask/TaskSummary
by querying the new /status/task api every 15 seconds

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
[w.bumiller@proxmox.com: fixup from d.csapak@proxmox.com]
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 14:51:15 +02:00
6f3146c08c ui: add Task Panels for dashboard
LongestTasks:
grid that shows tasks sorted by duration in descending order

RunningTasks:
grid that shows all running tasks

TaskSummary:
an overview of backup,prune,gc and sync tasks (error/warning/ok)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 14:50:22 +02:00
4b12879289 ui: css: improve look of fa icons
with these changes fa icons in actioncolumns,
they have the same layout as <i> elements on the same line
(they were slightly bigger and offset before)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 14:50:20 +02:00
20b3094bcb api2/status: add task list endpoint
for now mostly copy/paste from nodes/nodename/tasks
(without the parameters)
but we should replace the 'read_task_list' with a method
that gives us the tasks since some timestamp

so that we can get a longer list of tasks than for the node
(we could of course embed this then in the nodes/node/task api call and
remove this again as long as the api is not stable)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 14:50:17 +02:00
df528ee6fa implement From<TaskListInfo> for TaskListItem
and use it where its convenient

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 14:50:12 +02:00
57e50fb906 use new Mmap helper for dynamic index
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 13:57:56 +02:00
3136792c95 bump proxmox dep to 0.1.40
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 13:57:35 +02:00
3d571d5509 some internal combinator-influenced api cleanup
The download methods used to take the destination by value
and return them again, since this was required when using
combinators before we had `async fn`.
But this is just an ugly left-over now.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 11:46:42 +02:00
8e6e18b77c client: make dump_image async, use async chunk reader
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 11:40:18 +02:00
4d16badf6f add an AsyncReadChunk trait
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 11:38:21 +02:00
a609cf210e more cleanup
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 11:01:04 +02:00
1498659b4e cleanup
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 10:59:34 +02:00
4482f3fe11 pxar, acl: cleanup acl helper usage
use NixPath for Acl::set_file to avoid memduping the c
string

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 10:52:18 +02:00
5d85847f91 client: only start catalog upload if we have one
else we start a dynamic writer and never close it, leading to a backup error

this fixes an issue with backing up vm templates
(and possibly vms without disks)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 10:38:25 +02:00
476b4acadc BackupEnvironment: do not set finished flag prematurely
we check if all dynamic_writers are closed and if the backup contains
any valid files, we can only mark the backup finished after those
checks, else the backup task gets marked as OK, even though it
is not finished and no cleanups run

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 10:37:52 +02:00
cf1bd08131 pxar: fcaps in fuse
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-12 10:37:48 +02:00
ec8f042459 api2/status: use new rrd::extract_cached_data
and drop the now unused extract_lists function

this also fixes a bug, where we did not add the datastore to the list at
all when there was no rrd data

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-10 13:31:16 +02:00
431cc7b185 rrd: move creation of serde value into api
there is now a 'extract_cached_data' which just returns
the data of the specified field, and an api function that converts
a list of fields to the correct serde value

this way we do not have to create a serde value in rrd/cache.rs
(makes for a better interface)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-10 13:31:14 +02:00
e693818afc refactor time functions to tools
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-10 13:31:10 +02:00
3d68536fc2 pxar: support .pxareclude files, error report updates
Report vanished files (instead of erroring out on them),
also only warn about files inaccessible due to permissions
instead of bailing out.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-10 12:22:18 +02:00
26e78a2efb downgrade some FIXMEs to TODOs
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-10 11:09:23 +02:00
5444fa940b turn pxar::flags into bitflags, pxar::Flags
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-10 11:05:53 +02:00
d4f2397d4c add api to format disks and create datastores 2020-06-10 11:03:36 +02:00
fab2413741 catalog: remove unused SenderWriter
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-10 10:42:42 +02:00
669c137fec src/tools/systemd.rs: implement daemon_reload, start_unit, stop_unit and enable_unit 2020-06-10 08:56:04 +02:00
fc6047fcb1 pxar: don't skip list+found by default
This used to be default-off and was accidentally set to
on-by-default with the pxar crate update.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-10 08:53:10 +02:00
3014088684 pxar: sort .pxareclude-cli file and fix its mode
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-10 08:49:14 +02:00
144006fade src/tools.rs: add new run_command helper 2020-06-10 07:16:47 +02:00
b9cf6ee797 src/tools/systemd/types.rs: add Mount config 2020-06-09 18:47:10 +02:00
cdde66d277 statistics: covariance(): avoid allocation
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-09 13:57:27 +02:00
239e49f927 pxar: create .pxarexclude-cli file
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-09 13:17:59 +02:00
ae66873ce9 ui: add datastore usages to dashboard
shows an overview over the datastores, including a small chart of the
past month and an estimation of when its full

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-09 12:20:43 +02:00
bda48e04da api2: add status/datastore-usages api call
returns a list of the datastores and their usages, a list of usages of
the past month (for the gui) and an estimation of when its full
(using the linear regression)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-09 12:20:25 +02:00
ba97479848 add statistics module
provides some basic statistics functions (sum, mean, etc.)
and a function to return the parameters of the linear regression of
two variables

implemented using num_traits to be more flexible for the types

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-09 12:19:51 +02:00
6cad8ce4ce rrd: add 'extract_lists'
this is an interface to simply get the Vec<Option<f64>> out of rrd
without going through serde values

we return a list of timestamps and a HashMap with the lists we could find
(otherwise it is not in the map)

if no lists could be extracted, the time list is also empty

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-09 12:19:06 +02:00
34020b929e ui: show root disk usage
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-09 12:07:06 +02:00
33070956af let disk_usage return StorageStatus and use it for datastores/nodes
disk_usage returned the same values as defined in StorageStatus,
so simply use that

with that we can replace the logic of the datastore status with that
function and also use it for root disk usage of the nodes

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-09 12:05:39 +02:00
da84cc52f4 src/tools/systemd.rs: implement escape_unit and unescape_unit 2020-06-09 11:52:06 +02:00
9825748e5e Cargo.toml: readd commented-out proxmox crate via path for convenience
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-09 11:16:28 +02:00
2179359f40 move src/pxar.rs -> src/pxar/mod.rs
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-09 10:49:59 +02:00
9bb161c881 src/tools/disks.rs: add create_single_linux_partition and create_file_system 2020-06-08 17:43:01 +02:00
297e600730 cleanup comment 2020-06-08 17:43:01 +02:00
ed7b3a7de2 src/tools/disks.rs: add get_fs_uuid helper 2020-06-08 17:43:01 +02:00
0f358204bd src/tools/disks.rs: add helper to list partitions 2020-06-08 17:43:01 +02:00
ca6124d5fa src/tools/disks.rs: make helpers pub
So that I can use them with my test code.
2020-06-08 17:43:01 +02:00
7eacdc765b pxar: split assert_relative_path
the check for a single component is only required in the dir
stack atm

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-08 15:02:52 +02:00
c443f58b09 switch to external pxar and fuse crates
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-08 13:56:58 +02:00
ab1092392f Cargo.toml: pathpatterns, pxar, proxmox-fuse
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-08 13:56:58 +02:00
1e3d9b103d xattr: make xattr_name_fcaps public
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-08 13:56:58 +02:00
386990ba09 tools: add file_get_non_comment_lines
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-08 13:56:58 +02:00
bc853b028f src/tools/disks.rs: cleanup, remove unused DiskUse bitflag type 2020-06-08 09:43:07 +02:00
d406de299b src/tools/disks.rs: use dev_t to index zfs/lvm device sets 2020-06-08 09:01:34 +02:00
dfb31de8f0 proxmox_backup_manager disk list: display gpt column 2020-06-08 07:35:44 +02:00
7c3aa258f8 src/tools/disks/zfs.rs: allow empty zpool list output 2020-06-08 07:23:04 +02:00
044055062c src/tools/disks.rs: new helper to reread partition table 2020-06-08 07:22:06 +02:00
2b388026f8 src/api2/node/disks.rs: correctly use disk_by_name insteadf of disk_by_node 2020-06-08 07:20:59 +02:00
707974fdb3 src/api2/node/disks.rs: implement initgpt API 2020-06-07 10:30:34 +02:00
9069debcd8 src/api2/types.rs: define BLOCKDEVICE_NAME_SCHEMA 2020-06-07 07:20:25 +02:00
fa2bdc1309 src/config/acl.rs: add /system/disks to valid acl paths 2020-06-06 15:48:15 +02:00
8e40aa63c1 src/bin/proxmox-backup-manager.rs: add disk subcommand 2020-06-06 15:40:28 +02:00
d2522b2db6 src/tools/disks.rs: fix disk size, add completion helper 2020-06-06 15:39:25 +02:00
ce8e3de401 move disks api to /node/<node>/disks 2020-06-06 14:43:36 +02:00
7fa2779559 src/api2/disks.rs: implement smart api 2020-06-06 12:23:11 +02:00
042afd6e52 src/tools/disks.rs: new helper disk_by_name() 2020-06-06 12:22:38 +02:00
ff30caeaf8 src/api2/disks.rs - list-disks: add usage-type filter 2020-06-06 11:48:58 +02:00
553cd12ba6 src/api2/disks.rs: start disks api 2020-06-06 11:38:47 +02:00
de1e1a9d95 src/tools/disks.rs: use api macro so that we can use those types with the api 2020-06-06 11:37:24 +02:00
91960d6162 src/tools/disks.rs - get_disks: query smart status 2020-06-06 09:18:20 +02:00
4c24a48eb3 src/tools/disks/smart.rs: use model.to_string_lossy() to simplify code 2020-06-06 09:05:22 +02:00
484e761dab src/tools/disks/smart.rs: try to get correct wearout for ATA devices 2020-06-06 09:01:15 +02:00
059b7a252e src/tools/disks/smart.rs - get_smart_data: use &Disk instead of &str
So that we can query other device infos easily (model, vendor, ..)
2020-06-06 08:24:58 +02:00
1278aeec36 ui: add gc/prune schedule and options available in the ui
by adding them as columns for the config view,
and as a seperate tab on the edit window (this is done to not show
too many options at once)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-06 07:58:05 +02:00
e53a4c4577 ui: make DataStore configuration editable
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-06 07:57:39 +02:00
98ad58fbd2 ui: refactor DataStoreConfig and Edit
split them into two files and put them into the respective directory

refactor the DataStoreConfigPanel to controller/view
and the DataStoreEdit window/inputpanel to simply an editwindow
(there is no need to have a seperate inputpanel) which also
prepares the window for edit (by using pmxDisplayEditFields)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-06 07:57:19 +02:00
98bb3b9016 ui: replace DataStoreStatus with DataStoreConfig
We will show an overall status of the DataStores in the Dashboard, so there
is no need for a seperate DataStores Status.

This means we can move the Config to where the Status was, and remove
the duplicated entry in the NavigationTree, reducing confusion for users

We can still seperate the permissions by simply showing a permission
denied error, or simply leaving the list empty

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-06 07:56:35 +02:00
eb80aac288 src/tools/disks/smart.rs: parse output from smartctl 2020-06-05 18:30:06 +02:00
c26aad405f src/tools/disks.rs: implement get_disks (similar to the one in PVE::Diskmanage)
But no ceph support for now. Also no support for old cciss block devices.
2020-06-05 10:33:53 +02:00
f03a0e509e src/tools/disks.rs; use correct subdir to check holders 2020-06-05 10:33:53 +02:00
4c1e8855cc src/tools/disks.rs: fix disk type detection, remove newline from vendor string 2020-06-05 08:09:52 +02:00
85a9a5b68c depend on proxmox 0.1.39 2020-06-05 08:08:40 +02:00
f856e0774e ui: fix prune button
the remove button did not get the selModel since the xytpe was not
'grid' so give it the correct xtype

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-04 12:59:48 +02:00
43ba913977 bump version to 0.2.3-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-06-04 10:39:15 +02:00
a720894ff0 rrd: fix off-by-one in save interval calculation
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-06-04 10:30:47 +02:00
a95a3fb893 fix csum calculation of not 'chunk_size' aligned images
the last chunk does not have to be as big as the chunk_size,
just use the already available 'chunk_end' function which does the
correct thing

this fixes restoration of images whose sizes are not a multiple of
'chunk_size' as well

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-04 10:18:30 +02:00
620911b426 src/tools/disks/lvm.rs: implement get_lvm_devices() 2020-06-04 09:12:19 +02:00
5c264c8d80 src/tools/disks.rs: add/use get_partition_type_info 2020-06-04 07:48:22 +02:00
8d78589969 improve display of 'next run' for sync jobs
if the last sync job is too far in the past (or there was none at all
for now) we run it at the next iteration, so we want to show that

we now calculate the next_run by using either the real last endtime
as time or 0

then in the frontend, we check if the next_run is < now and show 'pending'
(we do it this way also for replication on pve)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-04 07:03:54 +02:00
eed8a5ad79 tools/systemd/time: fix compute_next_event for weekdays
two things were wrong here:
* the range (x..y) does not include y, so the range
  (day_num+1..6) goes from (day_num+1) to 5 (but sunday is 6)

* WeekDays.bits() does not return the 'day_num' of that day, but
  the bit value (e.g. 64 for SUNDAY) but was treated as the index of
  the day of the week
  to fix this, we drop the map to WeekDays and use the 'indices'
  directly

this patch makes the test work again

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-04 07:02:33 +02:00
538b9c1c27 systemd/time: add tests for all weekdays
this fails for now

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-06-04 07:02:23 +02:00
55919bf141 verify_file: add missing closing parenthesis in error message
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-06-03 19:10:01 +02:00
456ad0c478 src/tools/disks/zfs.rs: add parser for zpool list output 2020-06-03 12:16:08 +02:00
c76c7f8303 bump version to 0.2.2-1 2020-06-03 10:37:46 +02:00
c48aa39f3b src/bin/proxmox-backup-client.rs: implement quite flag 2020-06-03 10:11:37 +02:00
2d32fe2c04 client restore: don't add server file ending if already specified
If one executes a client command like
 # proxmox-backup-client files <snapshot> --repository ...
the files shown have already the '.fidx' or '.blob' file ending, so
if a user would just copy paste that one the client would always add
.blob, and the server would not find that file.

So avoid adding file endings if it is already a known OK one.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-06-03 07:03:55 +02:00
dc155e9bd7 client restore: factor out archive/type parsing
will be extended in a next patch.

Also drop a dead else branch, can never get hit as we always add
.blob as fallback

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-06-03 07:03:12 +02:00
4e14781aec fix typo 2020-06-03 06:59:43 +02:00
a595f0fee0 client: improve connection/new fingerprint query
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-06-02 10:40:31 +02:00
add5861e8d typo fixes all over the place
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-30 16:39:08 +02:00
1610c45a86 src/client/pull.rs: also download client.log.blob 2020-05-30 14:51:33 +02:00
b2387eaa45 avoid compiler warnings 2020-05-30 14:05:33 +02:00
96d65fbcd0 cleanup: define/use const for predefined blob file names. 2020-05-30 14:04:15 +02:00
7cc3473a4e src/client/backup_specification.rs: split code into extra file 2020-05-30 10:54:38 +02:00
4856a21836 src/client/pull.rs: more verbose logging 2020-05-30 08:12:43 +02:00
a0153b02c9 ui: use Proxmox.Utils.setAuthData
this uses different parameters which we want to be the same for
all products (e.g. secure cookie)

leave the PBS.Utils.updateLoginData for the case that we want to do
something more here (as in pve for example)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-30 07:24:27 +02:00
04b0ca8b59 add owner to group and snapshot listings
while touching it, make columns and tbar in DataStoreContent.js
declarative members and remove the (now) unnecessary initComponent

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-30 07:24:12 +02:00
86e432b0b8 ui: add SyncView
shows a nice overview of sync jobs (incl status of last run, estimated
next run, etc.) with options to add/edit/remove and also show the
log of the last run and manually run it now

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:32:40 +02:00
f0ed6a218c ui: add SyncJobEdit window
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:32:13 +02:00
709584719d ui: add RemoteSelector and DataStoreSelector
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:31:54 +02:00
d43f86f3f3 api2: add admin/sync endpoint
this returns the list of syncjobs with status, as opposed to
config/sync (which is just the config)

also adds an api call where users can run the job manually under
/admin/sync/$ID/run

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:31:32 +02:00
997d7e19fc config/sync: add SyncJobStatus Struct/Schema
contains the config + status

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:29:39 +02:00
c67b1fa72f syncjob: change worker type for sync jobs
'sync' is used for manually pulling a remote datastore
changing it for a scheduled sync to 'syncjob' so that we can
differentiate between both types of syncs

this also adds a seperate task description for it

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:28:04 +02:00
268687ddf0 api2/pull: refactor priv checking and creating pull parameters
we want to reuse those in the api call for manually running a sync job

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:27:43 +02:00
426c1e353b api2/config/sync: fix id parameter
'name' is not the correct parameter for get/post

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:24:54 +02:00
2888b27f4c create SYNC_SCHEDULE_SCHEMA to adapt description for sync jobs
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:24:25 +02:00
f5d00373f3 ui: add missing comment field to remote model
when using a diffstore, we have to add all used columns to the model,
else they will not refresh on a load

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 11:17:15 +02:00
934f5bb8ac src/bin/proxmox-backup-proxy.rs: cleanup, move code to src/tools/disks.rs
And simplify find_mounted_device by using stat.st_dev
2020-05-29 11:13:36 +02:00
9857472211 fix removing of remotes
we have to save the remote config after removing the section

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-29 10:48:26 +02:00
013fa7bbcb rrd: reduce io by saving data only once a minute 2020-05-29 09:16:13 +02:00
a8d7033cb2 src/bin/proxmox-backup-proxy.rs: add test if last prune job is still running 2020-05-29 08:06:48 +02:00
04ad7bc436 src/bin/proxmox-backup-proxy.rs: test if last sync job is still running 2020-05-29 08:06:48 +02:00
77ebbefc1a src/server/worker_task.rs: make worker_is_active_local pub 2020-05-29 08:06:48 +02:00
750252ba2f src/tools/systemd/time.rs: add test for "daily" schedule 2020-05-29 07:52:09 +02:00
dc58194ebe src/bin/proxmox-backup-proxy.rs: use correct id to lookup sync jobs 2020-05-29 07:50:59 +02:00
c6887a8a4d remote config gui: add comment field 2020-05-29 06:46:56 +02:00
090decbe76 BACKUP_REPO_URL_REGEX: move to api2::types and allow all valid data store names
The repo URL consists of
* optional userid
* optional host
* datastore name

All three have defined regex or format, but none of that is used, so
for example not all valid datastore names are accepted.

Move definition of the regex over to api2::types where we can access
all required regexes easily.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-29 06:29:23 +02:00
c32186595e api2::types: factor out USER_ID regex
allows for better reuse in a next patch

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-29 06:27:38 +02:00
947f45252d www/ServerStatus.js: use term "IO wait" for CPU iowait
Because we already use "IO delay" for the storage statistics.
2020-05-29 06:12:49 +02:00
c94e1f655e rrd stats: improve io delay stats 2020-05-28 19:12:13 +02:00
d80d1f9a2b bump version to 0.2.1-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-28 17:39:41 +02:00
6161ac18a4 ui: remotes: fix remote remove buttons base url
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-28 17:29:54 +02:00
6bba120d14 ui: fix RemoteEdit password change
we have to remove the password from the submitvalues if it did not
change

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-26 08:48:05 +02:00
880fa939d1 gui: move system stat RRDs to ServerStatus panel. 2020-05-26 07:33:00 +02:00
052aaeb5e9 re-bump to 0.2.0-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-25 20:10:38 +02:00
5f249127b2 docs: sync version with the package versions
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-25 20:10:38 +02:00
8277f4ace5 ui: navigation: sort datastores entries
adding a new one after load will append it still at the end, though.
But datastores are not something which get frequently added after
initial setup, so don't care about that for now..

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-25 19:46:47 +02:00
9b1aa424b9 ui: add some task log description mappings
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-25 19:06:52 +02:00
fef2b3e04c css: fix load mask background image path
We're not using the exact same paths as in PVE/PMG here.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-25 18:41:14 +02:00
7cebe5a1f4 ui: system config: reorder big panel to bottom
Gives a better look and feel if the flex'd big panel is at the bottom

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-25 16:41:54 +02:00
309ef20d6d src/bin/proxmox-backup-proxy.rs: simplify code 2020-05-25 16:20:32 +02:00
d0833a70f7 src/bin/proxmox-backup-proxy.rs: gather datastore usage stats 2020-05-25 16:20:32 +02:00
dda246403c ui: index: load widget toolkit CSS
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-25 15:44:39 +02:00
16e0dd65f1 d/control: proxmox-widget-toolkit depend on 2.2-2
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2020-05-25 12:50:55 +02:00
c5a516918f bump version to 0.2.0 2020-05-25 12:48:07 +02:00
cc275e8f93 depend on proxmox 0.1.36 2020-05-25 12:13:45 +02:00
d8dc281992 www/DataStoreStatus.js: display loadavg stats 2020-05-25 11:54:15 +02:00
2c66a590c0 src/bin/proxmox-backup-proxy.rs: gather iowait stats 2020-05-25 11:54:15 +02:00
485841da2c src/bin/proxmox-backup-proxy.rs: gather loadavg stats 2020-05-25 11:40:20 +02:00
e8f5810aa1 depend on proxmox 0.1.35 2020-05-25 11:34:34 +02:00
3e930f2bdc www/DataStoreStatus.js: display root disk stats 2020-05-25 11:34:24 +02:00
dd15c0aa3b src/bin/proxmox-backup-proxy.rs: gather root disk stats 2020-05-25 11:10:07 +02:00
c1b24fbf0b www/DataStoreStatus.js: display swap stats 2020-05-25 10:39:54 +02:00
3f23b17298 src/rrd/rrd.rs: do not wrap error and return ErrorKind::NotFound 2020-05-25 10:30:04 +02:00
c25c9d8dd1 src/bin/proxmox-backup-proxy.rs: gather swap usage stats 2020-05-25 10:25:58 +02:00
84dc6adcc1 src/rrd/cache.rs: display/log error when RRD load fails 2020-05-25 10:18:53 +02:00
0c4344650d src/rrd/rrd.rs: store/verify magic number 2020-05-25 09:21:54 +02:00
4f9513996c src/bin/proxmox-backup-proxy.rs: use block_in_place for rrd update 2020-05-25 08:30:59 +02:00
736edc7a7e src/rrd/rrd.rs: implement DST_COUNTER 2020-05-25 08:14:30 +02:00
2b55de407e src/rrd/rrd.rs: correctly compute derived values
use f64 for time.
2020-05-25 07:02:04 +02:00
a608806f65 www/DataStoreStatus.js: display netin/netout 2020-05-24 19:02:35 +02:00
8f0cec2642 src/bin/proxmox-backup-proxy.rs: gather netin/netout stats 2020-05-24 19:02:35 +02:00
0ed9a2b3ae src/config/network.rs: implement is_physical_nic() helper 2020-05-24 19:02:35 +02:00
58edd33d2b src/rrd/rrd.rs: implement DST_DERIVE 2020-05-24 19:02:35 +02:00
4fb05fde17 src/rrd/rrd.rs: restructure whole code 2020-05-24 16:51:28 +02:00
daca4f7888 src/rrd/rrd.rs: reduce size by using f64:NAN as UNKNOWN 2020-05-24 09:09:09 +02:00
4e6585839b src/rrd/rrd.rs: simplify an fix old value deletion 2020-05-24 06:44:06 +02:00
8c981ae379 rrd: fix display interval, try to avoid numeric errors 2020-05-23 16:03:43 +02:00
803ab12ad4 rrd: simplify code 2020-05-23 15:37:17 +02:00
a4a3f7ca5e rrd: pack multiple rrd values into th estat list 2020-05-23 14:03:44 +02:00
ba1c249eec www/DataStoreStatus.js: add test for RRD api 2020-05-23 11:52:26 +02:00
a2f862eed6 add experimental rrd api to get cpu stats 2020-05-23 11:50:53 +02:00
eaeda365e0 start gathering stats using new rrd module 2020-05-23 10:43:08 +02:00
6359dc891a add simple rrd implementation 2020-05-23 10:42:48 +02:00
07ad6470ca src/client/pull.rs: split out pull related code 2020-05-22 08:04:20 +02:00
a6160cdfeb src/bin/proxmox-backup-proxy.rs: schedule sync jobs 2020-05-22 07:50:59 +02:00
183125d576 src/api2/pull.rs: aquire try_shared_chunk_store_lock inside pull_store 2020-05-22 07:24:17 +02:00
a3016d6583 proxmox-backup-manager: add sync-job cli 2020-05-21 11:44:45 +02:00
b29d046e89 proxmox-backup-manager: split out cert.rs 2020-05-21 11:22:20 +02:00
380bd7df97 proxmox-backup-manager: split out datastore.rs 2020-05-21 11:14:34 +02:00
ea6f404e55 proxmox-backup-manager: split out dns.rs 2020-05-21 11:10:58 +02:00
a35a211d9e proxmox-backup-manager: split out network.rs 2020-05-21 11:08:38 +02:00
53e14507c1 proxmox-backup-manager: split out acl.rs 2020-05-21 10:56:46 +02:00
6fa39e53e0 proxmox-backup-manager: split out users.rs 2020-05-21 10:53:06 +02:00
a220a4564a roxmox-backup-manager: start splitting command into several files 2020-05-21 10:46:07 +02:00
6f652b1b3a rename 'job' to 'sync' 2020-05-21 10:29:25 +02:00
b1d4edc769 src/api2/config/job.rs: add job api 2020-05-21 10:16:35 +02:00
b4900286ce src/config/jobs.rs: use SectionConfig for jobs 2020-05-21 10:16:35 +02:00
c681885227 src/bin/proxmox-backup-manager.rs: format output of show commands 2020-05-20 16:47:37 +02:00
ee8b464466 src/tools/systemd.rs: avoid compiler warnings 2020-05-20 16:47:08 +02:00
51c63475e1 ui: add '.' to path regex
since we use the path for datastore ids, which can contain a '.'

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 16:33:26 +02:00
ce55db66d6 proxmox-backup-manager: add show command for remote and datastore
to show the data for a single item

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 16:33:07 +02:00
2882c881e9 api2/access/acl: add path and exact parameter to list_acl
so that we can get only a subset of the acls, filtered by the backed
also return the digest here

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 13:44:36 +02:00
c0ac207453 ui: add ACL panel to datastores
by introducing a datastorepanel (a TabPanel) which holds the content
and acl panel for now.

to be able to handle this in the router, we have to change the logic
of how to select the datastore from using the subpath to putting it
into the path (and extracting it when necessary)

if we need this again (e.g. possibly for remotes), we can further
refactor this logic to be more generic

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 13:27:13 +02:00
ee1458b61d fixup 2020-05-20 13:27:13 +02:00
0542cfdf4f ui: add ACL panel to Configuration
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 13:23:00 +02:00
12e3895399 api2/access/acl: make update_acl a protected api call
since we want to set the owner of the acl config to 'root'
which is only possible when using a protected api call

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 13:22:41 +02:00
11b6391c83 add 'exact' parameter to extract_acl_node_data
so that we can return acls for a single path

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 13:22:10 +02:00
2072aeaee6 ui: add UserSelector
this has to be different from pve for now, since the default of
'enabled' is reverted (pve: default disabled, pbs: default enabled)

if we decide to change this either here or in pve, we can refactor
it to the widget-toolkit

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 13:22:01 +02:00
b05672579e api2/roles: change return field of role to roleid
to be compatible with the pve api
with this, we can reuse the ui parts (RoleSelector)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 13:21:47 +02:00
5160c0e986 api2/acl: add privs array to roles
so that an admin can see which roles have which privileges

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 13:21:37 +02:00
1ad9dd08f4 acls: use constnamemap macro for privileges
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 13:21:28 +02:00
4d3369cb9a depend on proxmox 0.1.34 2020-05-20 13:21:01 +02:00
25829a879b src/bin/proxmox-backup-proxy.rs: schedule prune jobs 2020-05-20 13:00:53 +02:00
872062ee9f src/config/datastore.rs_ change prune types from i64 to u64 2020-05-20 13:00:13 +02:00
67f7ffd0db src/config/datastore.rs: add prune settings 2020-05-20 11:29:59 +02:00
0fafac2492 src/api2/access/user.rs: remove useless description
The description is not used at all if we refer to a type.
2020-05-20 11:27:58 +02:00
49ff10921c src/api2/types.rs: define PRUNE_SCHEMA_KEEP_* 2020-05-20 10:13:38 +02:00
479e4932b5 src/tools/systemd/parse_time.rs: improve error message 2020-05-20 09:43:16 +02:00
dd7a7eae8f src/bin/proxmox-backup-manager.rs: add completion helper for gc-schedule 2020-05-20 09:42:51 +02:00
8545480a31 src/bin/proxmox-backup-proxy.rs: add simple task scheduler for garbage collection 2020-05-20 08:59:45 +02:00
d6c28ddf84 src/tools/systemd/time.rs: export parse/verify 2020-05-20 08:38:39 +02:00
42fdbe5112 src/config/datastore.rs: add gc-schedule property 2020-05-20 08:38:10 +02:00
a67b70c154 depend on proxmox 0.1.33 2020-05-20 06:29:06 +02:00
9c5c383bff user: create default root user as typed struct
we added a userid attribute to the User struct, but missed that we
created the default user without that attribuet via the json! macro
which lead to a runtime panic on the deserialization

by using the struct directly, such errors will be caught by the compiler
in the future

with this change, we can remove the serde_json import here

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-20 06:09:08 +02:00
7d4e362993 depend on proxmox 0.1.32, src/api2/access/user.rs: simplify code 2020-05-19 12:58:46 +02:00
88acc86129 ui: add UserManagement panel
to add/edit users

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-19 09:34:41 +02:00
1d8ef0dcf7 ui: use Logo/RealmComboBox from widget-toolkit
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-19 09:34:19 +02:00
522c0da0a0 use new 'id_property' for user::User and use it in api calls
this allows us to return a user::User (or Vec<> of it)
instead of a generic serde value

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-19 09:33:56 +02:00
16c75c580b adapt to changes of SectionConfigPlugin
it requires not an Option<String> for the optional id_property

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-05-19 09:28:45 +02:00
07ce44a633 avoid compiler warnings 2020-05-19 07:03:41 +02:00
6c5024b050 Cargo.toml: remove native-tls
it's not used anymore.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-05-18 13:52:56 +02:00
b2c9c793ad debcargo.toml: add missing doc build-dependencies
and mark them accordingly.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-05-18 13:48:16 +02:00
79166b3935 debcargo.toml: reflow dependencies
to make changes easier to track

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-05-18 13:08:10 +02:00
e8d1da6a15 depend on proxmox 0.1.31 - use Value to store result metadata 2020-05-18 09:57:35 +02:00
2e686e0a63 update dependencies
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-05-18 09:08:09 +02:00
7a314d18f7 src/tools/systemd/parse_time.rs: check max values 2020-05-16 13:13:50 +02:00
2d08c97ae2 CalendarEvent - compute_next_value: use change tracking to avoid repeated testing 2020-05-16 10:32:27 +02:00
50ce1f987d CalendarEvent - compute_next_value: support seconds 2020-05-16 10:21:24 +02:00
d1a5ffdf78 src/tools/systemd/tm_editor.rs: new helper class 2020-05-16 10:09:41 +02:00
99baf7afcc CalendarEvent: test and fix repeated values 2020-05-16 07:43:51 +02:00
fed270bf3f CalendarEvent: speedup/simplify repetition tests 2020-05-16 07:09:53 +02:00
e05b637c73 src/tools/systemd/parse_time.rs: move parser into separate file 2020-05-16 06:53:15 +02:00
2ee6b3fdb9 src/tools/systemd/time.rs: implement compute_next_event 2020-05-16 06:33:03 +02:00
f3a96b2cdb renamed: src/tools/systemd/parser.rs -> src/tools/systemd/config.rs 2020-05-16 06:32:28 +02:00
a260c74a12 src/tools/systemd/time.rs: add helpers to compute CalendarEvents 2020-05-15 17:55:54 +02:00
52c70f3f5e depend on proxmox 0.1.30 2020-05-15 17:51:52 +02:00
30f577248b src/api2/node/time.rs: avoid custom unsafe readlink implementations 2020-05-15 06:50:07 +02:00
00491c0230 src/tools/systemd/parser.rs: use different setups for service and timer files, code cleanup 2020-05-14 13:55:13 +02:00
2ebdbac1c4 depend on nom, add parser for systemd calendar enents and time span 2020-05-14 12:18:30 +02:00
b4a85a3fa8 update pin-utils dep to stable version
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2020-05-14 11:46:05 +02:00
f486e9e50e add systemd configuration file parser/writer, start job configuration 2020-05-12 13:07:49 +02:00
7f5a27d302 depend on proxmox 0.1.29 2020-05-12 13:03:09 +02:00
40a36bcc57 depend on proxmox 0.1.28 2020-05-12 09:35:57 +02:00
b61d344f01 TODO.rst: update 2020-05-12 09:35:37 +02:00
65dab0266c proxmox-backup-manager: add completion helper for port list 2020-05-08 17:28:04 +02:00
525008f7ad proxmox-backup-manager - network list: render ports/slaves
And render interface name as first comumn.
2020-05-08 16:07:23 +02:00
5bef0f43da src/config/network.rs - check_bridge_ports: correctly check vlan ports 2020-05-08 15:51:47 +02:00
0f6bdbb01f src/config/network.rs - write_config: add more consistency checks 2020-05-08 14:31:38 +02:00
a4ccb46176 src/config/network.rs: avoid duplicate port usage 2020-05-08 11:15:00 +02:00
80bf084876 src/config/network.rs: do not combine entries
It is unclear when and how to write combined entries ...
2020-05-08 10:20:57 +02:00
db5672e83e src/config/network.rs: always write bridge_ports and bond_slaves
So that we can reliable detect the interface type.
2020-05-08 09:58:03 +02:00
86a5d56c4e proxmox-backup-manager: add network create command 2020-05-08 09:55:56 +02:00
3dd27a3bf8 src/api2/node/network.rs: add protected flag to revert 2020-05-08 09:30:25 +02:00
3aedb73816 src/api2/node/network.rs: pass bridge_ports and slaves a property strings
To make it compatible with pve.
2020-05-08 08:49:17 +02:00
bab5d18c3d src/config/network.rs: implement bond_mode
and rename bond_slaves to slaves to make it compatible with pve.
2020-05-07 14:07:45 +02:00
c2ffc68554 src/api2/node/network.rs: cleanup - factor out check_duplicate_gateway 2020-05-07 11:26:30 +02:00
9651833130 src/api2/node/network.rs: allow to create bridge and bond 2020-05-07 11:09:12 +02:00
7b22acd0c2 src/config/network.rs: make it compatible with pve
and depend on proxmox 0.1.26
2020-05-07 09:28:25 +02:00
5751e49566 src/server/worker_task.rs: implement and use status command 2020-05-07 09:27:33 +02:00
197de83ffa src/server/command_socket.rs: do not abort loop on client errors, allow backup gid 2020-05-07 09:27:33 +02:00
10effc9849 add tools/disks.rs (work in progress...)
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-05-05 10:14:42 +02:00
139f891087 TODO.rst: update 2020-05-05 09:22:01 +02:00
99641a6bbb garbage_collect: call fail_on_abort to abort GV when requested. 2020-05-05 09:06:34 +02:00
74f7240b8d src/bin/proxmox-backup-client.rs: add human readable date to prune list 2020-05-05 07:33:58 +02:00
a66d5898a1 docs/administration-guide.rst: fix prune command output 2020-05-05 07:24:27 +02:00
db1e061dcb src/bin/proxmox-backup-client.rs: correctly format prune result list. 2020-05-05 06:45:37 +02:00
96feecd621 administration-guide.rst: update snapshot list output 2020-05-04 13:14:03 +02:00
f9dcfa4149 administration-guide.rst: add section "Proxmox VE integration" 2020-05-04 12:41:38 +02:00
25cf09065f docs: use OpenSans as main font
Most people also read PDFs online ...
2020-05-04 10:48:09 +02:00
fc598cdbe1 docs: use better fonts for PDFs
Font XCharter and Lato have better quality.
2020-05-04 10:15:27 +02:00
bca294a17c docs/conf.py: avoid font scale option 2020-05-04 09:02:10 +02:00
a02e8b1e95 docs/conf.py: fix baselineskip in code-blocks with scaled font 2020-05-03 09:22:01 +02:00
26d29e0ec7 pdf docs: scale down monospace font 2020-05-03 08:23:35 +02:00
15d74eaaf4 use xetex to generate pdf
To correctly handle unicode art in code blocks...
2020-05-03 07:48:55 +02:00
8df51d4852 administration-guide.rst: add role definitions 2020-05-02 16:40:20 +02:00
8f3b3cc1f9 administration-guide.rst: add example to disable/remove a user 2020-05-02 11:21:05 +02:00
17ec699d79 administration-guide.rst: start user management docs 2020-05-02 11:11:36 +02:00
b080583ba8 src/bin/proxmox-backup-manager.rs: improve user list output 2020-05-01 16:22:50 +02:00
32d83bb34c TODO: update 2020-05-01 09:02:36 +02:00
e325dbd4a3 www/Dashboard.js: fix status url 2020-04-30 12:58:41 +02:00
ecb53af6d9 add ServerStatus.js GUI with Reboot and Shutdown buttons 2020-04-30 12:12:20 +02:00
ed751dc2ab src/api2/node/status.rs: rework api, implement reboot and shutdown 2020-04-30 11:52:40 +02:00
ca9dfe5fa4 src/api2/node/tasks.rs: use api macro features for default values 2020-04-30 11:51:56 +02:00
720af9f69b src/api2/node/tasks.rs: allow users to list/access there own tasks 2020-04-30 10:05:50 +02:00
f1490da82a use resonable acl paths (fixup) 2020-04-30 09:32:13 +02:00
74c08a5782 use reasonable acl paths 2020-04-30 09:30:00 +02:00
7f402dafb7 TODO.rst: update 2020-04-30 07:42:57 +02:00
bd88dc4116 cached_config: avoid parsing non-existent files multiple times 2020-04-30 07:04:23 +02:00
ebe556d0e7 www/DataStoreStatus.js: define Model for datastorte list
We want to use the admin/datastore api (instead of config/datastore),
to get the restricted list of datastores.
2020-04-30 06:50:45 +02:00
f9e3b1104e change index to templates using handlebars
using a handlebars instance in ApiConfig, to cache the templates
as long as possible, this is currently ok, as the index template
can only change when the whole package changes

if we split this in the future, we have to trigger a reload of
the daemon on gui package upgrade (so that the template gets reloaded)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-04-29 17:05:53 +02:00
bc0d03885c use proxmox 0.1.25, use new EnumEntry feature 2020-04-29 13:01:24 +02:00
acb428cdec add DataStoreStatus.js dummy 2020-04-29 11:22:05 +02:00
de1f8f1d36 Revert "gui: display DataStoreConfig above DataStoreContent"
This reverts commit 555dfe7b8e.
2020-04-29 11:09:35 +02:00
b9f2f761bb avoid problems with missing acl.cfg and user.cfg 2020-04-29 10:40:42 +02:00
30fb602578 src/api2/admin/datastore.rs - get_datastore_list: only return name and comment
We dont want to leak the full configuration to users with limited access permission.
Please use the api2::config::datastore api to get the full configuration.
2020-04-29 09:21:34 +02:00
0a00f6e01c src/api2/config/datastore.rs_ add delete property to update method 2020-04-29 09:09:59 +02:00
30003baaa4 src/api2/config/remote.rs: fix white space 2020-04-29 09:09:39 +02:00
5211705ff1 src/api2/config/remote.rs: add delete parameter to update method 2020-04-29 09:04:17 +02:00
ec67af9af3 src/api2/pull.rs: require Datastore.Prune if delete flag is set. 2020-04-29 07:19:32 +02:00
8247db5b39 src/config/acl.rs: introduice privileges and roles for remotes 2020-04-29 07:03:44 +02:00
409f44247b fix api2::types::ACL_ROLE_SCHEMA
make sure we list all roles ...
2020-04-28 13:25:02 +02:00
dd335b77f5 src/config/acl.rs - fix regression tests 2020-04-28 11:16:15 +02:00
6f6aa95abb add Datastore.Backup, Datastore.PowerUser and Datastore.Reader role 2020-04-28 11:07:25 +02:00
54552dda59 implemnt backup ownership, improve datastore access permissions 2020-04-28 10:22:25 +02:00
21690bfaef depend on proxmox 0.1.24 2020-04-28 08:23:41 +02:00
1347b1152d src/config/cached_user_info.rs - lookup_privs: correctly handle superuser 2020-04-27 13:22:03 +02:00
d00e1a216f src/config/acl.rs: introduce more/better datastore privileges 2020-04-27 07:13:50 +02:00
9c7fe29dfc src/config/acl.rs: rtename PRTIV_DATASTORE_ALLOCATE to PRIV_DATASTORE_MODIFY 2020-04-27 06:50:35 +02:00
14627d671a src/bin/proxmox-backup-manager.rs: add dns sub command
Also improved the DNS api, added a --delete option.
2020-04-26 08:23:23 +02:00
76227a6acd src/bin/proxmox-backup-manager.rs: fix node parameter handling 2020-04-25 17:20:22 +02:00
6830608855 depend on proxmox 0.1.23 2020-04-25 17:12:15 +02:00
26d9aebc28 move src/api2/config/network.rs to src/api2/node/network.rs
So that we have the same api path for network config as pve.
2020-04-25 17:00:38 +02:00
1ca540a63b src/config/network.rs: auto-add lo, and implement a few regression tests 2020-04-24 12:57:11 +02:00
9094186a57 xattr: cleanup: don't use pxar types in the API
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-04-24 11:23:48 +02:00
27a3decbfe xattr: api cleanup
Make `flistxattr()` return a `ListXAttr` helper which
provides an iterator over `&CStr`.

This exposes the property that xattr names are a
zero-terminated string without simply being an opaque
"byte vector". Using &[u8] as a type here is too lax.

Also let `fgetxattr` take a `CStr`. While this may be a
burden on the caller, we usually already have
zero-terminated strings on the call site. Currently we only
use this method coming from `flistxattr` after all.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-04-24 10:56:52 +02:00
9af76ef075 xattr: use checked_mul to increase size
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-04-24 10:56:52 +02:00
00ec8d1685 tools: pub use Fd from proxmox crate
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-04-24 10:56:52 +02:00
fd7c0979b4 src/bin/proxmox-backup-manager.rs: implement netwerk revert 2020-04-24 10:45:49 +02:00
c67bc9c35c src/bin/proxmox-backup-manager.rs: new command to show pending network changes 2020-04-24 10:27:43 +02:00
3181f9b625 src/bin/proxmox-backup-manager.rs: only show pending changes with "text" format 2020-04-24 10:16:57 +02:00
2eefd9aee1 src/config/network.rs: implement network reload, set "changes" attribute 2020-04-24 09:55:46 +02:00
8a6b86b8a7 src/config/network.rs: use a simple String for comments 2020-04-24 07:46:08 +02:00
96d9478668 src/config/network/parser.rs: corectly detect vanished interfaces 2020-04-24 07:26:54 +02:00
10a9be45bd src/api2/config/network.rs: implement update/delete comments 2020-04-23 16:08:35 +02:00
5f60a58fd5 src/config/network.rs; support interface comments, cleanups 2020-04-23 15:54:30 +02:00
659c3be3d5 src/config/network.rs: avoid newline after family options 2020-04-23 11:30:41 +02:00
5e4e88e83f src/api2/config/network.rs: implement update/delete for bridge_ports and bond_slaves 2020-04-23 11:21:27 +02:00
339965d720 src/api2/config/network.rs: only allow one default gateway 2020-04-23 10:37:40 +02:00
c38b4bb8b2 src/config/network.rs: do not allow to change interface type 2020-04-23 09:43:38 +02:00
42fbe91a34 src/config/network.rs: parse bond-slaves 2020-04-23 09:31:10 +02:00
1d9a68c2fc src/config/network.rs: parse bridge-ports 2020-04-23 09:24:17 +02:00
02269f3dba src/config/network.rs: introduce NetworkInterfaceType 2020-04-23 08:45:03 +02:00
d5ca9bd5df src/config/network.rs: cleanup (new helper combine_entry) 2020-04-23 07:54:12 +02:00
02e36d96ad src/config/network.rs: write changes to interfaces.new 2020-04-23 07:19:29 +02:00
2c18efd902 src/config/network.rs: use a single mtu setting (instead of mtu_v4 and mtu_v6) 2020-04-23 07:07:14 +02:00
4cb6bd894c src/bin/proxmox-backup-manager.rs: improve network list output format 2020-04-23 06:44:55 +02:00
b1564af25a src/bin/proxmox-backup-manager.rs: format datastore list output 2020-04-22 17:37:20 +02:00
bf004ecd87 src/bin/proxmox-backup-manager.rs: format network list output 2020-04-22 17:14:52 +02:00
f1026a5aa9 src/api2/config/network.rs: allow to update 'auto' flag 2020-04-22 16:46:46 +02:00
3fce3bc36e src/config/network/parser.rs: parse MTU settings 2020-04-22 13:44:51 +02:00
f8e7ac686a src/config/network.rs: only save attriubutes used by configuration method 2020-04-22 12:42:09 +02:00
c016482c7a src/api2/config/network.rs: implement delete property 2020-04-22 12:19:31 +02:00
27f2c23049 src/api2/config/network.rs: allow to update configuration method 2020-04-22 11:32:36 +02:00
df6bb03d0e src/api2/config/network.rs: improve network api 2020-04-22 10:54:07 +02:00
e2d940b949 src/config/network/parser.rs: remove debug println 2020-04-22 10:53:26 +02:00
0c226bc173 src/config/network/helper.rs: fix CIDR regex 2020-04-22 10:52:31 +02:00
76cf5208cf src/api2/types.rs: add schemas for IP/CIDR 2020-04-22 10:28:53 +02:00
2ea7bf1b3d src/api2/config/datastore.rs_ fix method docs 2020-04-22 08:53:16 +02:00
8b57cd4441 src/config/network.rs: remove netmask support
rely on cidr instead.
2020-04-22 08:45:13 +02:00
68da20bf62 src/api2/types.rs: define NETWORK_INTERFACE_NAME_SCHEMA 2020-04-21 17:54:52 +02:00
c357260d09 src/config/network.rs: move type definitions to src/api2/types.rs 2020-04-21 17:25:05 +02:00
7e02d08cd0 rename ConfigMethod to NetworkConfigMethod 2020-04-21 17:17:57 +02:00
ca0e534796 src/api2/config/network.rs: start network configuration api 2020-04-21 14:28:26 +02:00
904e988667 src/config/network.rs: impleement load/save 2020-04-21 12:55:33 +02:00
3f129233be src/config/network.rs: add Interface flags 'exists' and 'active' 2020-04-21 11:46:56 +02:00
a9bb491e35 src/config/network.rs: cleanup autostart flag handling 2020-04-21 11:06:22 +02:00
1ec7f8a0dd src/config/network/helper.rs: new helper get_network_interfaces() 2020-04-21 10:32:54 +02:00
92310d585c src/config/network.rs: simplify code 2020-04-20 18:10:15 +02:00
f34d4401f7 src/config/network.rs: read/write /etc/network/interfaces
Start implementing a recursive descent parser.
2020-04-20 14:15:57 +02:00
6e695960ca src/config/cached_user_info.rs: cache it up to 5 seconds 2020-04-18 08:49:20 +02:00
365f0f720c fix permission tests using non-uri parameters
We nood to do those tests inside the fuction body instead...
2020-04-18 08:23:04 +02:00
a737179eb4 src/config/cached_user_info.rs: new check_privs helper 2020-04-18 08:09:34 +02:00
bb072ba49c src/api2/access.rs: cleanup 2020-04-18 07:28:25 +02:00
ff329f970b src/api2/types.rs: use anyhow::Error in test cases 2020-04-18 07:05:31 +02:00
f7d4e4b506 switch from failure to anyhow
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-04-17 18:43:30 +02:00
404d78c41e src/api2/pull.rs: add access permission 2020-04-17 15:27:04 +02:00
1bfc1efa50 src/api2/subscription.rs: add access permissions 2020-04-17 15:14:28 +02:00
73ce1d1146 src/api2/reader.rs: add access permissions 2020-04-17 15:01:56 +02:00
70e5f2461d src/api2/config/remote.rs: add access permissions 2020-04-17 14:57:26 +02:00
c0ef209aeb src/api2/config/datastore.rs: impl digest check for delete, add access permissions 2020-04-17 14:51:29 +02:00
9f9f7eefa3 src/api2/backup.rs: add access permissions 2020-04-17 14:40:20 +02:00
bb34b58910 src/api2/admin/datastore.rs: add access permissions - first try
We need to refine this later (introduce backup owner concept?)
2020-04-17 14:36:27 +02:00
5972def5ec acl: change path "storage" to "datastore" 2020-04-17 14:15:44 +02:00
aa90ced3bf src/api2/access/role.rs: use schema ACL_ROLE_SCHEMA 2020-04-17 14:14:06 +02:00
ca257c8097 move type defs from src/api2/access/acl.rs to src/api2/types.rs 2020-04-17 14:13:15 +02:00
3fff55b293 src/api2/access/role.rs: new api to list roles 2020-04-17 14:03:24 +02:00
4f66423fcc src/api2/access/user.rs: add access permissions 2020-04-17 11:04:36 +02:00
d4f020f4c5 src/api2/access/user.rs: add access permissions 2020-04-17 10:08:45 +02:00
d28ddb8e04 src/api2/access/acl.rs: add access permissions 2020-04-17 10:03:09 +02:00
83b6a7cf71 src/api2/node/tasks.rs: use api macro, implement access permissions 2020-04-16 17:47:21 +02:00
e4681f9f71 src/api2/node/syslog.rs: add access permissions 2020-04-16 17:08:19 +02:00
b5037fa8ed src/api2/node/status.rs: add access permissions 2020-04-16 17:05:09 +02:00
9989d2c4e9 src/server/rest.rs: reduce delay for permission error to 500ms 2020-04-16 12:56:34 +02:00
1cf7bbf412 src/api2/node/services.rs: add access permissions 2020-04-16 12:47:16 +02:00
68ed0c629d src/api2/node/journal.rs: add access permissions 2020-04-16 12:47:16 +02:00
4b40148caa start impl. access permissions 2020-04-16 12:47:16 +02:00
423e656163 src/config/cached_user_info.rs: new helper class 2020-04-16 10:05:16 +02:00
1ce8a5d0b7 depend on proxmox 0.1.21 2020-04-16 10:04:00 +02:00
109d7817cd src/config/user.rs - cached_config: do not store/return digest 2020-04-15 11:35:57 +02:00
5354511fd0 src/config/acl.rs: implement cached_config 2020-04-15 11:30:47 +02:00
bd098a7f77 src/api2/node/dns.rs: use api macro (cleanup) 2020-04-15 10:09:18 +02:00
8d048af2bf acl: improve NoAccess handling 2020-04-15 08:11:43 +02:00
4f3db187cf Docu: first proof reading
This is a first proof reading of the currently existing documentation.

fixes (hopefully all):
* spelling
* grammar

Tries to increase readabilty and ease of understanding by simplifying
and restructing some sentences and paragraphs. Filler words which add
to the cognitive load but don't add anything are removed
(most notably `also`).
2020-04-15 06:52:59 +02:00
9a328319dd pxar extract: remove pattern from arg_param, add target instead 2020-04-15 06:41:37 +02:00
7e3d2e5b41 pxar create: remove exclude from arg_param 2020-04-15 06:31:46 +02:00
9c06f6c292 fix previous commit - use result. 2020-04-14 17:48:10 +02:00
9f4e47dd93 acl update: check path 2020-04-14 17:23:48 +02:00
d83175dd69 acl update: check if user exist. 2020-04-14 13:46:27 +02:00
68ccdf09a4 src/config/user.rs: implement user config cache 2020-04-14 13:45:45 +02:00
9765092ede acl api: implement update 2020-04-14 10:16:49 +02:00
ed3e60ae69 start ACL api 2020-04-13 11:09:44 +02:00
a83eab3c4d acl: use BTreeMap and BTreeSet to avoid sort() 2020-04-12 17:13:53 +02:00
0815ec7e65 acl: implement roles(), add regression tests. 2020-04-12 13:06:50 +02:00
5c6cdf9815 add acl config 2020-04-11 12:24:26 +02:00
9abcae1b0e gui: improve login view (use realms) 2020-04-09 13:37:14 +02:00
b88f9c5b1e PASSWORD_SCHEMA: set max_length to 1024 (for tickets) 2020-04-09 13:35:58 +02:00
879546aff6 api: add default property to domain list 2020-04-09 13:35:08 +02:00
73b40e9b46 api: correctly sort access subdirmap 2020-04-09 13:34:07 +02:00
708db4b3ae api: add list_domains 2020-04-09 11:36:45 +02:00
685e13347e api: move config/user to access/users, implement change_password
To make it similar to the pve api
2020-04-09 10:21:24 +02:00
7d817b0358 implement auth framework 2020-04-08 14:06:15 +02:00
579728c641 add user configiguration 2020-04-08 14:06:15 +02:00
cf459b1982 gc: log pending removals 2020-04-06 09:50:40 +02:00
d16122cd87 gui: preview prune selection 2020-04-01 14:14:44 +02:00
dda7015497 prune api: return a usable result (we run synchronous anyways) 2020-04-01 12:24:28 +02:00
5b5ca60a07 fix 'keep-monthly' field name
else the backend complains about a non-existant parameter

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-03-31 08:46:52 +02:00
aeee4329b0 gui - DataStoreContent: avoid useless icons, display file path 2020-03-26 18:01:04 +01:00
5f44899207 gui - DataStoreContent: move control code into controller (cleanup) 2020-03-26 17:23:51 +01:00
b1127fd0d0 gui: add prune dialog 2020-03-26 13:23:28 +01:00
4299ca727c src/server/rest.rs: use correct formatter 2020-03-26 12:54:20 +01:00
3383973532 gui: cleanup DataStoreContent.js 2020-03-26 11:17:15 +01:00
555dfe7b8e gui: display DataStoreConfig above DataStoreContent 2020-03-26 08:38:35 +01:00
e8f0ad19af gui: use a tree panel for DataStoreContent 2020-03-25 15:17:28 +01:00
a83ee10c49 depend on proxmox 0.1.20 2020-03-25 15:17:16 +01:00
9abc1166b0 bump proxmox dependency to 0.1.19
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-03-19 10:12:33 +01:00
99c287861e add 'rsync' to build_depends
a 'make deb' fails without rsync installed (a pxar test needs it)

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-03-18 16:24:33 +01:00
6650a242fb rewrite future select in upgrade_to_backup_protocol using select macro
and handle all ok/err cases with at least logging

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-03-18 11:33:59 +01:00
66b4593b04 fix typo
s/Nuber/Number/
s/backups/Backups/

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2020-03-17 16:13:54 +01:00
0e7ab0567c buildsys: add missing dependency
required for the docs built when building the deb packages

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2020-03-16 14:54:24 +01:00
10426c1750 Makefile - upload: upload to correct product repos 2020-03-03 10:56:07 +01:00
244 changed files with 27269 additions and 10382 deletions

View File

@ -1,6 +1,6 @@
[package]
name = "proxmox-backup"
version = "0.1.3"
version = "0.7.0"
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
edition = "2018"
license = "AGPL-3"
@ -14,44 +14,53 @@ name = "proxmox_backup"
path = "src/lib.rs"
[dependencies]
base64 = "0.10"
base64 = "0.12"
bitflags = "1.2.1"
bytes = "0.5"
chrono = "0.4" # Date and time library for Rust
crc32fast = "1"
endian_trait = { version = "0.6", features = ["arrays"] }
failure = "0.1"
anyhow = "1.0"
futures = "0.3"
h2 = { version = "0.2", features = ["stream"] }
handlebars = "3.0"
http = "0.2"
hyper = "0.13"
lazy_static = "1.4"
libc = "0.2"
log = "0.4"
native-tls = "0.2"
nix = "0.16"
num-traits = "0.2"
once_cell = "1.3.1"
openssl = "0.10"
pam = "0.7"
pam-sys = "0.5"
percent-encoding = "2.1"
pin-utils = "0.1.0-alpha"
proxmox = { version = "0.1.18", features = [ "sortable-macro", "api-macro" ] }
pin-utils = "0.1.0"
pathpatterns = "0.1.1"
proxmox = { version = "0.1.42", features = [ "sortable-macro", "api-macro" ] }
#proxmox = { git = "ssh://gitolite3@proxdev.maurer-it.com/rust/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] }
#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro" ] }
proxmox-fuse = "0.1.0"
pxar = { version = "0.2.0", features = [ "tokio-io", "futures-io" ] }
#pxar = { path = "../pxar", features = [ "tokio-io", "futures-io" ] }
regex = "1.2"
rustyline = "5.0.5"
rustyline = "6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
siphasher = "0.3"
syslog = "4.0"
tokio = { version = "0.2.9", features = [ "blocking", "fs", "io-util", "macros", "rt-threaded", "signal", "stream", "tcp", "time", "uds" ] }
tokio-openssl = "0.4.0"
tokio-util = { version = "0.2.0", features = [ "codec" ] }
tokio-util = { version = "0.3", features = [ "codec" ] }
tower-service = "0.3.0"
udev = "0.3"
url = "2.1"
#valgrind_request = { git = "https://github.com/edef1c/libvalgrind_request", version = "1.1.0", optional = true }
walkdir = "2"
xdg = "2.2"
zstd = { version = "0.4", features = [ "bindgen" ] }
nom = "5.1"
[features]
default = []

View File

@ -37,10 +37,14 @@ CARGO ?= cargo
COMPILED_BINS := \
$(addprefix $(COMPILEDIR)/,$(USR_BIN) $(USR_SBIN) $(SERVICE_BIN))
DEBS= ${PACKAGE}-server_${DEB_VERSION}_${ARCH}.deb ${PACKAGE}-client_${DEB_VERSION}_${ARCH}.deb
export DEB_VERSION DEB_VERSION_UPSTREAM
SERVER_DEB=${PACKAGE}-server_${DEB_VERSION}_${ARCH}.deb
CLIENT_DEB=${PACKAGE}-client_${DEB_VERSION}_${ARCH}.deb
DOC_DEB=${PACKAGE}-docs_${DEB_VERSION}_all.deb
DEBS=${SERVER_DEB} ${CLIENT_DEB}
DSC = rust-${PACKAGE}_${DEB_VERSION}.dsc
DESTDIR=
@ -135,7 +139,8 @@ install: $(COMPILED_BINS)
$(MAKE) -C docs install
.PHONY: upload
upload: ${DEBS}
upload: ${SERVER_DEB} ${CLIENT_DEB} ${DOC_DEB}
# check if working directory is clean
git diff --exit-code --stat && git diff --exit-code --stat --staged
tar cf - ${DEBS} | ssh -X repoman@repo.proxmox.com upload --product pbs --dist buster
tar cf - ${SERVER_DEB} ${DOC_DEB} | ssh -X repoman@repo.proxmox.com upload --product pbs --dist buster
tar cf - ${CLIENT_DEB} | ssh -X repoman@repo.proxmox.com upload --product "pbs,pve" --dist buster

View File

@ -1,22 +1,37 @@
TODO list for Proxmox Backup
============================
* user management api
* disk management api
* start writing server GUI
* improve catalog shell commands
* improve user documentation
GUI
===
* fix network/dns GUI (network/dns api changed)
* user/acl/permission management GUI
* implement GUI to configure remotes
* implement fancy DatastoreStatus.js dashboard
* implement PVE GUI to add PBS storage (with convenient copy/paste
functionality, like we have for cluster join)
Chores:
=======
* move tools/xattr.rs and tools/acl.rs to proxmox/sys/linux/
* remove pbs-* systemd timers and services on package purge
Suggestions
===========

115
debian/changelog vendored
View File

@ -1,3 +1,118 @@
rust-proxmox-backup (0.7.0-1) unstable; urgency=medium
* implement clone for RemoteChunkReader
* improve docs
* client: add --encryption boolen parameter
* client: use default encryption key if it is available
* d/rules: do not compress .pdf files
* ui: various fixes
* add beta text with link to bugtracker
-- Proxmox Support Team <support@proxmox.com> Tue, 07 Jul 2020 07:40:05 +0200
rust-proxmox-backup (0.6.0-1) unstable; urgency=medium
* make ReadChunk not require mutable self.
* ui: increase timeout for snapshot listing
* ui: consistently spell Datastore without space between words
* ui: disk create: sync and improve 'add-datastore' checkbox label
* proxmox-backup-client: add benchmark command
* pxar: fixup 'vanished-file' logic a bit
* ui: add verify button
-- Proxmox Support Team <support@proxmox.com> Fri, 03 Jul 2020 09:45:52 +0200
rust-proxmox-backup (0.5.0-1) unstable; urgency=medium
* partially revert commit 1f82f9b7b5d231da22a541432d5617cb303c0000
* ui: allow to Forget (delete) backup snapshots
* pxar: deal with files changing size during archiving
-- Proxmox Support Team <support@proxmox.com> Mon, 29 Jun 2020 13:00:54 +0200
rust-proxmox-backup (0.4.0-1) unstable; urgency=medium
* change api for incremental backups mode
* zfs disk management gui
-- Proxmox Support Team <support@proxmox.com> Fri, 26 Jun 2020 10:43:27 +0200
rust-proxmox-backup (0.3.0-1) unstable; urgency=medium
* support incremental backups mode
* new disk management
* single file restore for container backups
-- Proxmox Support Team <support@proxmox.com> Wed, 24 Jun 2020 10:12:57 +0200
rust-proxmox-backup (0.2.3-1) unstable; urgency=medium
* tools/systemd/time: fix compute_next_event for weekdays
* improve display of 'next run' for sync jobs
* fix csum calculation for images which do not have a 'chunk_size' aligned
size
* add parser for zpool list output
-- Proxmox Support Team <support@proxmox.com> Thu, 04 Jun 2020 10:39:06 +0200
rust-proxmox-backup (0.2.2-1) unstable; urgency=medium
* proxmox-backup-client.rs: implement quiet flag
* client restore: don't add server file ending if already specified
* src/client/pull.rs: also download client.log.blob
* src/client/pull.rs: more verbose logging
* gui improvements
-- Proxmox Support Team <support@proxmox.com> Wed, 03 Jun 2020 10:37:12 +0200
rust-proxmox-backup (0.2.1-1) unstable; urgency=medium
* ui: move server RRD statistics to 'Server Status' panel
* ui/api: add more server statistics
* ui/api: add per-datastore usage and performance statistics over time
* ui: add initial remote config management panel
* remotes: save passwords as base64
* gather zpool io stats
* various fixes/improvements
-- Proxmox Support Team <support@proxmox.com> Thu, 28 May 2020 17:39:33 +0200
rust-proxmox-backup (0.2.0-1) unstable; urgency=medium
* see git changelog (too many changes)
-- Proxmox Support Team <support@proxmox.com> Mon, 25 May 2020 19:17:03 +0200
rust-proxmox-backup (0.1.3-1) unstable; urgency=medium
* use SectionConfig from proxmox 0.1.18-1

6
debian/control.in vendored
View File

@ -3,10 +3,14 @@ Architecture: any
Depends: fonts-font-awesome,
libjs-extjs (>= 6.0.1),
libzstd1 (>= 1.3.8),
lvm2,
proxmox-backup-docs,
proxmox-mini-journalreader,
proxmox-widget-toolkit,
proxmox-widget-toolkit (>= 2.2-4),
smartmontools,
${misc:Depends},
${shlibs:Depends},
Recommends: zfsutils-linux,
Description: Proxmox Backup Server daemon with tools and GUI
This package contains the Proxmox Backup Server daemons and related
tools. This includes a web-based graphical user interface.

29
debian/debcargo.toml vendored
View File

@ -11,8 +11,31 @@ vcs_git = ""
vcs_browser = ""
maintainer = "Proxmox Support Team <support@proxmox.com>"
section = "admin"
build_depends = [ "debhelper (>= 12~)", "bash-completion" ]
build_depends_excludes = [ "debhelper (>=11)" ]
build_depends = [
"debhelper (>= 12~)",
"bash-completion",
"python3-docutils",
"python3-pygments",
"rsync",
"fonts-dejavu-core <!nodoc>",
"fonts-lato <!nodoc>",
"fonts-open-sans <!nodoc>",
"graphviz <!nodoc>",
"latexmk <!nodoc>",
"python3-sphinx <!nodoc>",
"texlive-fonts-extra <!nodoc>",
"texlive-fonts-recommended <!nodoc>",
"texlive-xetex <!nodoc>",
"xindy <!nodoc>",
]
build_depends_excludes = [
"debhelper (>=11)",
]
[packages.lib]
depends = [ "libacl1-dev", "libsystemd-dev", "libfuse3-dev", "uuid-dev" ]
depends = [
"libacl1-dev",
"libfuse3-dev",
"libsystemd-dev",
"uuid-dev",
]

28
debian/postinst vendored Normal file
View File

@ -0,0 +1,28 @@
#!/bin/sh
set -e
#DEBHELPER#
case "$1" in
configure)
# modeled after dh_systemd_start output
systemctl --system daemon-reload >/dev/null || true
if [ -n "$2" ]; then
_dh_action=try-reload-or-restart
else
_dh_action=start
fi
deb-systemd-invoke $_dh_action proxmox-backup.service proxmox-backup-proxy.service >/dev/null || true
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
exit 0

10
debian/prerm vendored Normal file
View File

@ -0,0 +1,10 @@
#!/bin/sh
set -e
#DEBHELPER#
# modeled after dh_systemd_start output
if [ -d /run/systemd/system ] && [ "$1" = remove ]; then
deb-systemd-invoke stop 'proxmox-backup-banner.service' 'proxmox-backup-proxy.service' 'proxmox-backup.service' >/dev/null || true
fi

View File

@ -5,6 +5,7 @@ usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-api
usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-proxy
usr/lib/x86_64-linux-gnu/proxmox-backup/proxmox-backup-banner
usr/sbin/proxmox-backup-manager
usr/share/javascript/proxmox-backup/index.hbs
usr/share/javascript/proxmox-backup/css/ext6-pbs.css
usr/share/javascript/proxmox-backup/images/logo-128.png
usr/share/javascript/proxmox-backup/images/proxmox_logo.png

9
debian/rules vendored
View File

@ -37,11 +37,14 @@ override_dh_auto_install:
PROXY_USER=backup \
LIBDIR=/usr/lib/$(DEB_HOST_MULTIARCH)
override_dh_installinit:
dh_installinit
dh_installinit --name proxmox-backup-proxy
override_dh_installsystemd:
# note: we start/try-reload-restart services manually in postinst
dh_installsystemd --no-start --no-restart-after-upgrade
# workaround https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=933541
# TODO: remove once available (Debian 11 ?)
override_dh_dwz:
dh_dwz --no-dwz-multifile
override_dh_compress:
dh_compress -X.pdf

View File

@ -1,11 +1,5 @@
include ../defines.mk
ifeq ($(BUILD_MODE), release)
COMPILEDIR := ../target/release
else
COMPILEDIR := ../target/debug
endif
GENERATED_SYNOPSIS := \
proxmox-backup-client/synopsis.rst \
proxmox-backup-client/catalog-shell-synopsis.rst \
@ -26,6 +20,15 @@ SPHINXOPTS =
SPHINXBUILD = sphinx-build
BUILDDIR = output
ifeq ($(BUILD_MODE), release)
COMPILEDIR := ../target/release
SPHINXOPTS += -t release
else
COMPILEDIR := ../target/debug
SPHINXOPTS += -t devbuild
endif
# Sphinx internal variables.
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) .
@ -73,10 +76,11 @@ html: ${GENERATED_SYNOPSIS}
.PHONY: latexpdf
latexpdf: ${GENERATED_SYNOPSIS}
@echo "Requires python3-sphinx, texlive-xetex, xindy and texlive-fonts-extra"
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
@echo "Running LaTeX files through xelatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
@echo "xelatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: epub3
epub3: ${GENERATED_SYNOPSIS}

View File

@ -3,6 +3,7 @@ Administration Guide
The administration guide.
.. todo:: either add a bit more explanation or remove the previous sentence
Terminology
-----------
@ -12,7 +13,7 @@ Backup Content
When doing deduplication, there are different strategies to get
optimal results in terms of performance and/or deduplication rates.
Depending on the type of data, one can split data into fixed or variable
Depending on the type of data, one can split data into *fixed* or *variable*
sized chunks.
Fixed sized chunking needs almost no CPU performance, and is used to
@ -21,7 +22,7 @@ backup virtual machine images.
Variable sized chunking needs more CPU power, but is essential to get
good deduplication rates for file archives.
Therefore, the backup server supports both strategies.
The backup server supports both strategies.
File Archives: ``<name>.pxar``
@ -29,9 +30,9 @@ File Archives: ``<name>.pxar``
.. see https://moinakg.wordpress.com/2013/06/22/high-performance-content-defined-chunking/
A file archive stores a whole directory tree. Content is stored using
A file archive stores a full directory tree. Content is stored using
the :ref:`pxar-format`, split into variable sized chunks. The format
is specially optimized to achieve good deduplication rates.
is optimized to achieve good deduplication rates.
Image Archives: ``<name>.img``
@ -44,8 +45,8 @@ data. Content is split into fixed sized chunks.
Binary Data (BLOBs)
^^^^^^^^^^^^^^^^^^^
This type is used to store smaller (< 16MB) binaries like
configuration data. Larger files should be stored as image archive.
This type is used to store smaller (< 16MB) binary data such as
configuration files. Larger files should be stored as image archive.
.. caution:: Please do not store all files as BLOBs. Instead, use the
file archive to store whole directory trees.
@ -54,15 +55,15 @@ configuration data. Larger files should be stored as image archive.
Catalog File: ``catalog.pcat1``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The catalog file is basically an index for file archive. It contains
the list of files, and is used to speedup search operations.
The catalog file is an index for file archives. It contains
the list of files and is used to speed-up search operations.
The Manifest: ``index.json``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The manifest contains the list of all backup files, including
file sizes and checksums. It is used to verify the consistency of a
The manifest contains the list of all backup files, their
sizes and checksums. It is used to verify the consistency of a
backup.
@ -73,17 +74,17 @@ The backup server groups backups by *type*, where *type* is one of:
``vm``
This type is used for :term:`virtual machine`\ s. Typically
contains the virtual machine configuration and an image archive
contains the virtual machine's configuration and an image archive
for each disk.
``ct``
This type is used for :term:`container`\ s. Contains the container
This type is used for :term:`container`\ s. Contains the container's
configuration and a single file archive for the container content.
``host``
This type is used for physical host, or if you want to run backups
manually from inside virtual machines or containers. Such backups
may contain file and image archives (no restrictions here).
This type is used for backups created from within the backed up machine.
Typically this would be a physical host but could also be a virtual machine
or container. Such backups may contain file and image archives, there are no restrictions in this regard.
Backup ID
@ -102,14 +103,14 @@ The time when the backup was made.
Backup Group
~~~~~~~~~~~~
We call the tuple ``<type>/<ID>`` a backup group. Such group
may contains one or more backup snapshots.
The tuple ``<type>/<ID>`` is called a backup group. Such a group
may contain one or more backup snapshots.
Backup Snapshot
~~~~~~~~~~~~~~~
We call the triplet ``<type>/<ID>/<time>`` a backup snapshot. It
The triplet ``<type>/<ID>/<time>`` is called a backup snapshot. It
uniquely identifies a specific backup within a datastore.
.. code-block:: console
@ -118,25 +119,25 @@ uniquely identifies a specific backup within a datastore.
vm/104/2019-10-09T08:01:06Z
host/elsa/2019-11-08T09:48:14Z
As you can see, the time is formatted as RFC3399_ using Coordinated
As you can see, the time format is RFC3399_ with Coordinated
Universal Time (UTC_, identified by the trailing *Z*).
:term:`DataStore`
~~~~~~~~~~~~~~~~~
A datastore is a place to store backups. The current implementation
A datastore is a place where backups are stored. The current implementation
uses a directory inside a standard unix file system (``ext4``, ``xfs``
or ``zfs``) to store backup data.
or ``zfs``) to store the backup data.
Datastores are identified by a simple *ID*. You can configure that
Datastores are identified by a simple *ID*. You can configure it
when setting up the backup server.
Backup Server Management
------------------------
The command line tool to configure and manage the server is called
The command line tool to configure and manage the backup server is called
:command:`proxmox-backup-manager`.
@ -144,7 +145,9 @@ Datastore Configuration
~~~~~~~~~~~~~~~~~~~~~~~
A :term:`datastore` is a place to store backups. You can configure
several datastores, but you need at least one of them. The datastore is identified by a simple `name` and point to a directory.
multiple datastores. At least one datastore needs to be
configured. The datastore is identified by a simple `name` and points
to a directory.
The following command creates a new datastore called ``store1`` on :file:`/backup/disk1/store1`
@ -152,20 +155,24 @@ The following command creates a new datastore called ``store1`` on :file:`/backu
# proxmox-backup-manager datastore create store1 /backup/disk1/store1
To list existing datastores use:
To list existing datastores run:
.. code-block:: console
# proxmox-backup-manager datastore list
store1 /backup/disk1/store1
┌────────┬──────────────────────┬─────────────────────────────┐
│ name │ path │ comment │
╞════════╪══════════════════════╪═════════════════════════════╡
│ store1 │ /backup/disk1/store1 │ This is my default storage. │
└────────┴──────────────────────┴─────────────────────────────┘
Finally, it is also possible to remove the datastore configuration:
Finally, it is possible to remove the datastore configuration:
.. code-block:: console
# proxmox-backup-manager datastore remove store1
.. note:: Above command removes the datastore configuration. It does
.. note:: The above command removes only the datastore configuration. It does
not delete any data from the underlying directory.
@ -175,6 +182,126 @@ File Layout
.. todo:: Add datastore file layout example
User Management
~~~~~~~~~~~~~~~
Proxmox Backup support several authentication realms, and you need to
choose the realm when you add a new user. Possible realms are:
:pam: Linux PAM standard authentication. Use this if you want to
authenticate as Linux system user (Users needs to exist on the
system).
:pbs: Proxmox Backup Server realm. This type stores hashed passwords in
``/etc/proxmox-backup/shadow.json``.
After installation, there is a single user ``root@pam``, which
corresponds to the Unix superuser. You can use the
``proxmox-backup-manager`` command line tool to list or manipulate
users:
.. code-block:: console
# proxmox-backup-manager user list
┌─────────────┬────────┬────────┬───────────┬──────────┬────────────────┬────────────────────┐
│ userid │ enable │ expire │ firstname │ lastname │ email │ comment │
╞═════════════╪════════╪════════╪═══════════╪══════════╪════════════════╪════════════════════╡
│ root@pam │ 1 │ │ │ │ │ Superuser │
└─────────────┴────────┴────────┴───────────┴──────────┴────────────────┴────────────────────┘
The superuser has full administration rights on everything, so you
normally want to add other users with less privileges:
.. code-block:: console
# proxmox-backup-manager user create john@pbs --email john@example.com
The create command lets you specify many option like ``--email`` or
``--password``, but you can update or change any of them using the
update command later:
.. code-block:: console
# proxmox-backup-manager user update john@pbs --firstname John --lastname Smith
# proxmox-backup-manager user update john@pbs --comment "An example user."
.. todo:: Mention how to set password without passing plaintext password as cli argument.
The resulting use list looks like this:
.. code-block:: console
# proxmox-backup-manager user list
┌──────────┬────────┬────────┬───────────┬──────────┬──────────────────┬──────────────────┐
│ userid │ enable │ expire │ firstname │ lastname │ email │ comment │
╞══════════╪════════╪════════╪═══════════╪══════════╪══════════════════╪══════════════════╡
│ john@pbs │ 1 │ │ John │ Smith │ john@example.com │ An example user. │
├──────────┼────────┼────────┼───────────┼──────────┼──────────────────┼──────────────────┤
│ root@pam │ 1 │ │ │ │ │ Superuser │
└──────────┴────────┴────────┴───────────┴──────────┴──────────────────┴──────────────────┘
Newly created users do not have an permissions. Please read the next
section to learn how to set access permissions.
If you want to disable an user account, you can do that by setting ``--enable`` to ``0``
.. code-block:: console
# proxmox-backup-manager user update john@pbs --enable 0
Or completely remove the users with:
.. code-block:: console
# proxmox-backup-manager user remove john@pbs
Access Control
~~~~~~~~~~~~~~
Users do not have any permission by default. Instead you need to
specify what is allowed and what not. You can do this by assigning
roles to users on specific objects like datastores or remotes. The
following roles exist:
**Admin**
The Administrator can do anything.
**Audit**
An Auditor can view things, but is not allowed to change settings.
**NoAccess**
Disable Access - nothing is allowed.
**DatastoreAdmin**
Can do anything on datastores.
**DatastoreAudit**
Can view datastore settings and list content. But
is not allowed to read the actual data.
**DataStoreReader**
Can Inspect datastore content and can do restores.
**DataStoreBackup**
Can backup and restore owned backups.
**DatastorePowerUser**
Can backup, restore, and prune owned backups.
**RemoteAdmin**
Can do anything on remotes.
**RemoteAudit**
Can view remote settings.
**RemoteSyncOperator**
Is allowed to read data from a remote.
Backup Client usage
-------------------
@ -184,16 +311,16 @@ The command line client is called :command:`proxmox-backup-client`.
Respository Locations
~~~~~~~~~~~~~~~~~~~~~
The client uses a special repository notation to specify a datastore
The client uses the following notation to specify a datastore repository
on the backup server.
[[username@]server:]datastore
If you do not specify a ``username`` the default is ``root@pam``. The
default for server is to use the local host (``localhost``).
The default value for ``username`` ist ``root``. If no server is specified, the
default is the local host (``localhost``).
You can pass the repository by setting the ``--repository`` command
line options, or by setting the ``PBS_REPOSITORY`` environment
You can pass the repository with the ``--repository`` command
line option, or by setting the ``PBS_REPOSITORY`` environment
variable.
@ -219,8 +346,8 @@ Environment Variables
Output Format
~~~~~~~~~~~~~
Most commands support the ``--output-format`` parameter, which can be
set to the following values:
Most commands support the ``--output-format`` parameter. It accepts
the following values:
:``text``: Text format (default). Structured data is rendered as a table.
@ -240,9 +367,9 @@ Please use the following environment variables to modify output behavior:
``PROXMOX_OUTPUT_NO_HEADER``
If set (to any value), do not render table headers.
.. note:: The ``text`` format is designed to be human readable, but
.. note:: The ``text`` format is designed to be human readable, and
not meant to be parsed by automation tools. Please use the ``json``
format for such purpose because it is machine readable.
format if you need to process the output.
.. _creating-backups:
@ -250,15 +377,15 @@ Please use the following environment variables to modify output behavior:
Creating Backups
~~~~~~~~~~~~~~~~
This section explains how to create backup on physical host, or from
inside virtual machines or containers. Such backups may contain file
and image archives (no restrictions here).
This section explains how to create a backup from within the machine. This can
be a physical host, a virtual machine, or a container. Such backups may contain file
and image archives. There are no restrictions in this case.
.. note:: If you want to backup virtual machines or containers see :ref:`pve-integration`.
.. note:: If you want to backup virtual machines or containers on Proxmov VE, see :ref:`pve-integration`.
The prerequisite is that you have already set up (or can access) a
backup server. It is assumed that you know the repository name and
credentials. In the following examples we simply use ``backup-server:store1``.
For the following example you need to have a backup server set up, working
credentials and need to know the repository name.
In the following examples we use ``backup-server:store1``.
.. code-block:: console
@ -275,15 +402,15 @@ credentials. In the following examples we simply use ``backup-server:store1``.
This will prompt you for a password and then uploads a file archive named
``root.pxar`` containing all the files in the ``/`` directory.
.. Caution:: Please note that proxmox-backup-client does not
.. Caution:: Please note that the proxmox-backup-client does not
automatically include mount points. Instead, you will see a short
``skip mount point`` notice for each of them. The idea is that you
create a separate file archive for each mounted disk. You can also
``skip mount point`` notice for each of them. The idea is to
create a separate file archive for each mounted disk. You can
explicitly include them using the ``--include-dev`` option
(i.e. ``--include-dev /boot/efi``). You can use this option
multiple times, once for each mount point you want to include.
multiple times for each mount point that should be included.
The ``--repository`` option is sometimes quite long and is used by all
The ``--repository`` option can get quite long and is used by all
commands. You can avoid having to enter this value by setting the
environment variable ``PBS_REPOSITORY``.
@ -291,26 +418,26 @@ environment variable ``PBS_REPOSITORY``.
# export PBS_REPOSTORY=backup-server:store1
You can then execute all commands without specifying the ``--repository``
After this you can execute all commands without specifying the ``--repository``
option.
One single backup is allowed to contain more than one archive. For example, assume you want to backup two disks mounted at ``/mmt/disk1`` and ``/mnt/disk2``:
One single backup is allowed to contain more than one archive. For example, if
you want to backup two disks mounted at ``/mmt/disk1`` and ``/mnt/disk2``:
.. code-block:: console
# proxmox-backup-client backup disk1.pxar:/mnt/disk1 disk2.pxar:/mnt/disk2
This create a backup of both disks.
This creates a backup of both disks.
The backup command takes a list of backup specifications, which
include archive name on the server, the type of the archive, and the
archive source at the client. The format is quite simple to understand:
include the archive name on the server, the type of the archive, and the
archive source at the client. The format is:
<archive-name>.<type>:<source-path>
Common types are ``.pxar`` for file archives, and ``.img`` for block
device images. Thus it is quite easy to create a backup for a block
device:
device images. To create a backup of a block device run the following command:
.. code-block:: console
@ -320,42 +447,43 @@ Excluding files/folders from a backup
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Sometimes it is desired to exclude certain files or folders from a backup archive.
Using the proxmox backup client this is possible via simple text based
``.pxarexclude`` files placed in the filesystem hierarchy.
To tell the Proxmox backup client when and how to ignore files and directories,
place a text file called ``.pxarexclude`` in the filesystem hierarchy.
Whenever the backup client encounters such a file in a directory, it interprets
each line as glob match patterns for files and directories that are to be excluded
from the backup.
Whenever such a file is encountered in a directory, the backup client reads its
content and lines are interpreted as glob match patterns for files/directories
to exclude from the archive.
The file must contain a single glob pattern on each line. Empty lines are ignored.
The same is true for lines starting with ``#``, indicating a line containing comments.
Lines starting with ``!`` correspond to glob match patterns for explicit inclusion
of files previously excluded by a match. This allows for example to exclude
all entries in a directory except for a few single files.
Lines ending in ``/`` match directory entries only.
The folder containing the ``.pxarexclude`` file is considered to be the root of
the given patterns. It is only possible to match files in this or below this folder.
The file must contain a single glob pattern per line. Empty lines are ignored.
The same is true for lines starting with ``#``, which indicates a comment.
A ``!`` at the beginning of a line reverses the glob match pattern from an exclusion
to an explicit inclusion. This makes it possible to exclude all entries in a
directory except for a few single files/subdirectories.
Lines ending in ``/`` match only on directories.
The directory containing the ``.pxarexclude`` file is considered to be the root of
the given patterns. It is only possible to match files in this directory and its subdirectories.
``\`` is used to escape glob characters. ``?`` matches any single character,
``*`` matches any character including the empty string.
``**`` is used to match also subdirectories and can be used to exclude for example
all files ending in ``.tmp`` within the directory or a subdirectory by the
``\`` is used to escape special glob characters.
``?`` matches any single character.
``*`` matches any character, including an empty string.
``**`` is used to match subdirectories. It can be used to, for example, exclude
all files ending in ``.tmp`` within the directory or subdirectories with the
following pattern ``**/*.tmp``.
``[...]`` matches a single character from any of the provided characters within
the brackets. ``[!...]`` does the complementary and matches any singe character
not contained within the brackets. It is also possible to specify ranges by two
characters separated by ``-``. For example ``[a-z]`` matches any lowercase
alphabetic character, ``[0-9]`` matches any one single digit.
not contained within the brackets. It is also possible to specify ranges with two
characters separated by ``-``. For example, ``[a-z]`` matches any lowercase
alphabetic character and ``[0-9]`` matches any one single digit.
The order of the glob match patterns defines if the file is finally included or
The order of the glob match patterns defines if a file is included or
excluded, later entries win over previous ones.
This is also true for match patterns encountered deeper down the directory tree,
which may then override a previous exclusion.
Note however that folders marked for exclusion are not read by the client,
so ``.pxarexclude`` files contained within have no effect.
``.pxarexclude`` files are treated as regular files and are also included in the
which can override a previous exclusion.
Be aware that excluded directories will **not** be read by the backup client.
A ``.pxarexclude`` file in a subdirectory will have no effect.
``.pxarexclude`` files are treated as regular files and will be included in the
backup archive.
For example, consider the following folder structure:
For example, consider the following directory structure:
.. code-block:: console
@ -369,7 +497,7 @@ For example, consider the following folder structure:
folder/subfolder1:
. .. file0 file1 file2 file3
The ``.pxarexclude`` files containing the following:
The different ``.pxarexclude`` files contain the following:
.. code-block:: console
@ -386,7 +514,7 @@ The ``.pxarexclude`` files containing the following:
This would exclude ``file1`` and ``file3`` in ``subfolder0`` and all of
``subfolder1`` except ``file2``.
Restoring this archive form backup results in:
Restoring this backup will result in:
.. code-block:: console
@ -403,8 +531,8 @@ Restoring this archive form backup results in:
Encryption
^^^^^^^^^^
Proxmox backup support client side encryption using AES-256 in GCM_
mode. You first need to create an encryption key in order to use that:
Proxmox backup supports client side encryption with AES-256 in GCM_
mode. First you need to create an encryption key:
.. code-block:: console
@ -416,7 +544,7 @@ extra protection, you can also create it without a password:
.. code-block:: console
# proxmox-backup-client key create /path/to/my-backup.key --kdf none
# proxmox-backup-client key create /path/to/my-backup.key --kdf none
.. code-block:: console
@ -427,7 +555,7 @@ extra protection, you can also create it without a password:
...
You can avoid having to enter the passwords by setting the environment
You can avoid entering the passwords by setting the environment
variables ``PBS_PASSWORD`` and ``PBS_ENCRYPTION_PASSWORD``.
.. todo:: Explain master-key
@ -437,22 +565,26 @@ Restoring Data
~~~~~~~~~~~~~~
The regular creation of backups is a necessary step to avoid data
loss. More important, however, is the restoration. Be sure to perform
periodic recovery tests to ensure that you can access your data in
loss. More important, however, is the restoration. It is good practice to perform
periodic recovery tests to ensure that you can access the data in
case of problems.
First, you need to find the snapshot you want to restore. The snapshot
command gives you a list of all snapshots on the server:
First, you need to find the snapshot which you want to restore. The snapshot
command gives a list of all snapshots on the server:
.. code-block:: console
# proxmox-backup-client snapshots
...
host/elsa/2019-12-03T09:30:15Z | 51788646825 | root.pxar catalog.pcat1 index.json
host/elsa/2019-12-03T09:35:01Z | 51790622048 | root.pxar catalog.pcat1 index.json
┌────────────────────────────────┬─────────────┬────────────────────────────────────┐
│ snapshot │ size │ files │
╞════════════════════════════════╪═════════════╪════════════════════════════════════╡
│ host/elsa/2019-12-03T09:30:15Z │ 51788646825 │ root.pxar catalog.pcat1 index.json │
├────────────────────────────────┼─────────────┼────────────────────────────────────┤
│ host/elsa/2019-12-03T09:35:01Z │ 51790622048 │ root.pxar catalog.pcat1 index.json │
├────────────────────────────────┼─────────────┼────────────────────────────────────┤
...
You can also inspect the catalog to find specific files.
You can inspect the catalog to find specific files.
.. code-block:: console
@ -470,9 +602,8 @@ backup.
# proxmox-backup-client restore host/elsa/2019-12-03T09:35:01Z root.pxar /target/path/
You can instead simply download the contents of any archive using '-'
instead of ``/target/path``. This dumps the content to standard
output:
To get the contents of any archive you can restore the ``ìndex.json`` file in the
repository and restore it to '-'. This will dump the content to the standard output.
.. code-block:: console
@ -494,20 +625,18 @@ to use the interactive recovery shell.
...
The interactive recovery shell is a minimalistic command line interface that
utilizes the metadata stored in the catalog for you to quickly list, navigate and
search files contained within a file archive.
You can select individual files as well as select files matched by a glob pattern
for restore.
utilizes the metadata stored in the catalog to quickly list, navigate and
search files in a file archive.
To restore files, you can select them individually or match them with a glob
pattern.
The use of the catalog for navigation reduces the overhead otherwise caused by
network traffic and decryption, as instead of downloading and decrypting
individual encrypted chunks from the chunk-store to access the metadata, we only
need to download and decrypt the catalog.
Using the catalog for navigation reduces the overhead considerably because only
the catalog needs to be downloaded and, optionally, decrypted.
The actual chunks are only accessed if the metadata in the catalog is not enough
or for the actual restore.
Similar to common UNIX shells ``cd`` and ``ls`` are the commands used to change
working directory and list directory contents of the archive.
working directory and list directory contents in the archive.
``pwd`` shows the full path of the current working directory with respect to the
archive root.
@ -567,7 +696,7 @@ This allows you to access the full content of the archive in a seamless manner.
load on your host, depending on the operations you perform on the mounted
filesystem.
To unmount the filesystem simply use the ``umount`` command on the mountpoint:
To unmount the filesystem use the ``umount`` command on the mountpoint:
.. code-block:: console
@ -579,7 +708,7 @@ Login and Logout
The client tool prompts you to enter the logon password as soon as you
want to access the backup server. The server checks your credentials
and responds with a ticket that is valid for two hours. The client
tool automatically stores that ticket and use it for further requests
tool automatically stores that ticket and uses it for further requests
to this server.
You can also manually trigger this login/logout using the login and
@ -590,7 +719,7 @@ logout commands:
# proxmox-backup-client login
Password: **********
To remove the ticket, simply issue a logout:
To remove the ticket, issue a logout:
.. code-block:: console
@ -608,76 +737,78 @@ command:
# proxmox-backup-client forget <snapshot>
.. caution:: This command removes all the archives in this backup
snapshot so that they are inaccessible and unrecoverable.
.. caution:: This command removes all archives in this backup
snapshot. They will be inaccessible and unrecoverable.
Such manual removal is sometimes required, but normally the prune
The manual removal is sometimes required, but normally the prune
command is used to systematically delete older backups. Prune lets
you specify which backup snapshots you want to keep. There are the
following retention options:
you specify which backup snapshots you want to keep. The
following retention options are available:
``--keep-last <N>``
Keep the last ``<N>`` backup snapshots.
``--keep-hourly <N>``
Keep backups for the last ``<N>`` different hours. If there is more than one
backup for a single hour, only the latest one is kept.
Keep backups for the last ``<N>`` hours. If there is more than one
backup for a single hour, only the latest is kept.
``--keep-daily <N>``
Keep backups for the last ``<N>`` different days. If there is more than one
backup for a single day, only the latest one is kept.
Keep backups for the last ``<N>`` days. If there is more than one
backup for a single day, only the latest is kept.
``--keep-weekly <N>``
Keep backups for the last ``<N>`` different weeks. If there is more than one
backup for a single week, only the latest one is kept.
Keep backups for the last ``<N>`` weeks. If there is more than one
backup for a single week, only the latest is kept.
.. note:: The weeks start on Monday and end on Sunday. The software
uses the `ISO week date`_ system and correctly handles weeks at
the end of the year.
.. note:: Weeks start on Monday and end on Sunday. The software
uses the `ISO week date`_ system and handles weeks at
the end of the year correctly.
``--keep-monthly <N>``
Keep backups for the last ``<N>`` different months. If there is more than one
backup for a single month, only the latest one is kept.
Keep backups for the last ``<N>`` months. If there is more than one
backup for a single month, only the latest is kept.
``--keep-yearly <N>``
Keep backups for the last ``<N>`` different years. If there is more than one
backup for a single year, only the latest one is kept.
Keep backups for the last ``<N>`` years. If there is more than one
backup for a single year, only the latest is kept.
The retention options are processed in the order given above. Each option
only covers backups within its time period. The next option does not take care
of already covered backups. It will only consider older backups.
Those retention options are processed in the order given above. Each
option covers a specific period of time. We say that backups within
this period are covered by this option. The next option does not take
care of already covered backups and only considers older backups.
The prune command also looks for unfinished and incomplete backups and
removes them unless they are newer than the last successful backup. In
this case, the last failed backup is retained.
Unfinished and incomplete backups will be removed by the prune command unless
they are newer than the last successful backup. In this case, the last failed
backup is retained.
.. code-block:: console
# proxmox-backup-client prune <group> --keep-daily 7 --keep-weekly 4 --keep-monthly 3
You can use the ``--dry-run`` option to test your settings. This just
shows the list of existing snapshots and what action prune would take
on that.
You can use the ``--dry-run`` option to test your settings. This only
shows the list of existing snapshots and which action prune would take.
.. code-block:: console
# proxmox-backup-client prune host/elsa --dry-run --keep-daily 1 --keep-weekly 3
retention options: --keep-daily 1 --keep-weekly 3
Testing prune on store "store2" group "host/elsa"
host/elsa/2019-12-04T13:20:37Z keep
host/elsa/2019-12-03T09:35:01Z remove
host/elsa/2019-11-22T11:54:47Z keep
host/elsa/2019-11-21T12:36:25Z remove
host/elsa/2019-11-10T10:42:20Z keep
┌────────────────────────────────┬──────┐
│ snapshot │ keep │
╞════════════════════════════════╪══════╡
host/elsa/2019-12-04T13:20:37Z │ 1 │
├────────────────────────────────┼──────┤
host/elsa/2019-12-03T09:35:01Z │ 0 │
├────────────────────────────────┼──────┤
│ host/elsa/2019-11-22T11:54:47Z │ 1 │
├────────────────────────────────┼──────┤
│ host/elsa/2019-11-21T12:36:25Z │ 0 │
├────────────────────────────────┼──────┤
│ host/elsa/2019-11-10T10:42:20Z │ 1 │
└────────────────────────────────┴──────┘
.. note:: Neither the ``prune`` command nor the ``forget`` command free space
in the chunk-store. The chunk-store still contains the data blocks
unless you are performing :ref:`garbage-collection`.
in the chunk-store. The chunk-store still contains the data blocks. To free
space you need to perform :ref:`garbage-collection`.
.. _garbage-collection:
@ -687,8 +818,7 @@ Garbage Collection
The ``prune`` command removes only the backup index files, not the data
from the data store. This task is left to the garbage collection
command. It is therefore recommended to carry out garbage collection
regularly.
command. It is recommended to carry out garbage collection on a regular basis.
The garbage collection works in two phases. In the first phase, all
data blocks that are still in use are marked. In the second phase,
@ -727,6 +857,41 @@ unused data blocks are removed.
`Proxmox VE`_ integration
-------------------------
You need to define a new storage with type 'pbs' on your `Proxmox VE`_
node. The following example uses ``store2`` as storage name, and
assumes the server address is ``localhost``, and you want to connect
as ``user1@pbs``.
.. code-block:: console
# pvesm add pbs store2 --server localhost --datastore store2
# pvesm set store2 --username user1@pbs --password <secret>
If your backup server uses a self signed certificate, you need to add
the certificate fingerprint to the configuration. You can get the
fingerprint by running the following command on the backup server:
.. code-block:: console
# proxmox-backup-manager cert info |grep Fingerprint
Fingerprint (sha256): 64:d3:ff:3a:50:38:53:5a:9b:f7:50:...:ab:fe
Please add that fingerprint to your configuration to establish a trust
relationship:
.. code-block:: console
# pvesm set store2 --fingerprint 64:d3:ff:3a:50:38:53:5a:9b:f7:50:...:ab:fe
After that you should be able to see storage status with:
.. code-block:: console
# pvesm status --storage store2
Name Type Status Total Used Available %
store2 pbs active 3905109820 1336687816 2568422004 34.23%
.. include:: command-line-tools.rst

View File

@ -17,10 +17,25 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Implement custom formatter for code-blocks ---------------------------
#
# * use smaller font
# * avoid space between lines to nicely format utf8 tables
from sphinx.highlighting import PygmentsBridge
from pygments.formatters.latex import LatexFormatter
class CustomLatexFormatter(LatexFormatter):
def __init__(self, **options):
super(CustomLatexFormatter, self).__init__(**options)
self.verboptions = r"formatcom=\footnotesize\relax\let\strut\empty"
PygmentsBridge.latex_formatter = CustomLatexFormatter
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
@ -30,8 +45,11 @@
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = ["sphinx.ext.graphviz", "sphinx.ext.todo"]
todo_link_only = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@ -53,7 +71,7 @@ rst_epilog = epilog_file.read()
# General information about the project.
project = 'Proxmox Backup'
copyright = '2019, Proxmox Support Team'
copyright = '2019-2020, Proxmox Support Team'
author = 'Proxmox Support Team'
# The version info for the project you're documenting, acts as replacement for
@ -61,9 +79,11 @@ author = 'Proxmox Support Team'
# built documents.
#
# The short X.Y version.
version = '1.0'
vstr = lambda s: '<devbuild>' if s is None else str(s)
version = vstr(os.getenv('DEB_VERSION_UPSTREAM'))
# The full version, including alpha/beta/rc tags.
release = '1.0-1'
release = vstr(os.getenv('DEB_VERSION'))
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
@ -251,14 +271,24 @@ htmlhelp_basename = 'ProxmoxBackupdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_engine = 'xelatex'
latex_elements = {
'fontenc': '\\usepackage{fontspec}',
# The paper size ('letterpaper' or 'a4paper').
#
'papersize': 'a4paper',
# The font size ('10pt', '11pt' or '12pt').
#
'pointsize': '12pt',
'pointsize': '10pt',
'fontpkg': r'''
\setmainfont{Open Sans}
\setsansfont{Lato}
\setmonofont{DejaVu Sans Mono}
''',
# Additional stuff for the LaTeX preamble.
#

View File

@ -5,24 +5,23 @@ Glossary
`Virtual machine`_
A Virtual machine is a program that can execute an entire
operatin system inside an emulated hardware environment.
A virtual machine is a program that can execute an entire
operating system inside an emulated hardware environment.
`Container`_
A Container is an isolated user space. Programs runs directly on
the hosts kernel, but with limited access to the host resources.
A container is an isolated user space. Programs run directly on
the host's kernel, but with limited access to the host resources.
Datastore
A place to store backups. The current implemenation is
file-system based, so this refers to a directory containing the
backup data.
A place to store backups. A directory which contains the backup data.
The current implemenation is file-system based.
`Rust`_
Rust is a new, fast and memory-efficient system programming
language, with no runtime or garbage collector. Rusts rich type
language. It has no runtime or garbage collector. Rusts rich type
system and ownership model guarantee memory-safety and
thread-safety. I can eliminate many classes of bugs
at compile-time.
@ -31,11 +30,9 @@ Glossary
Is a tool that makes it easy to create intelligent and
beautiful documentation. It was originally created for the
Python documentation, and it has excellent facilities for the
documentation of the Python programming language. It has excellent facilities for the
documentation of software projects in a range of languages.
`reStructuredText`_
Is an easy-to-read, what-you-see-is-what-you-get plaintext
@ -44,8 +41,8 @@ Glossary
`FUSE`
Filesystem in Userspace (`FUSE <https://en.wikipedia.org/wiki/Filesystem_in_Userspace>`_)
defines an interface which allows to implement a filesystem in
defines an interface which makes it possible to implement a filesystem in
userspace as opposed to implementing it in the kernel. The fuse
kernel driver handles filesystem requests and sends them to an
userspace application for reply.
kernel driver handles filesystem requests and sends them to a
userspace application.

View File

@ -1,18 +1,15 @@
.. Proxmox Backup documentation master file
Welcome to Proxmox Backup's documentation!
==========================================
Welcome to the Proxmox Backup documentation!
============================================
Copyright (C) 2019 Proxmox Server Solutions GmbH
Copyright (C) 2019-2020 Proxmox Server Solutions GmbH
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 published by the Free Software Foundation; with no
Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A
copy of the license is included in the section entitled "GNU Free
Documentation License".
.. todolist::
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
published by the Free Software Foundation; with no Invariant Sections, no
Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included
in the section entitled "GNU Free Documentation License".
.. toctree::
@ -37,5 +34,14 @@ Documentation License".
glossary.rst
GFDL.rst
.. only:: html and devbuild
.. toctree::
:maxdepth: 2
:caption: Developer Appendix
todos.rst
* :ref:`genindex`

View File

@ -1,55 +1,50 @@
Installation
============
`Proxmox Backup`_ is split into a server part and a client part. The
server part comes with it's own graphical installer, but we also
ship Debian_ package repositories, so you can easily install those
packages on any Debian_ based system.
`Proxmox Backup`_ is split into a server and client part. The server part
can either be installed with a graphical installer or on top of
Debian_ from the provided package repository.
.. include:: package-repositories.rst
Server installation
-------------------
The backup server stores the actual backup data, but also provides a
web based GUI for various management tasks, for example disk
management.
The backup server stores the actual backed up data and provides a web based GUI
for various management tasks such as disk management.
.. note:: You always need a backup server. It is not possible to use
`Proxmox Backup`_ without the server part.
The server is based on Debian, therefore the disk image (ISO file) provided
by us includes a complete Debian system ("buster" for version 1.x) as
well as all necessary backup packages.
The disk image (ISO file) provided by Proxmox includes a complete Debian system
("buster" for version 1.x) as well as all necessary packages for the `Proxmox Backup`_ server.
Using the installer will guide you through the setup, allowing
The installer will guide you through the setup process and allows
you to partition the local disk(s), apply basic system configurations
(e.g. timezone, language, network) and install all required packages.
Using the provided ISO will get you started in just a few minutes,
that's why we recommend this method for new and existing users.
(e.g. timezone, language, network), and installs all required packages.
The provided ISO will get you started in just a few minutes, and is the
recommended method for new and existing users.
Alternatively, `Proxmox Backup`_ server can be installed on top of an
existing Debian system.
Using the `Proxmox Backup`_ Installer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Install `Proxmox Backup`_ with the Installer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can download the ISO from |DOWNLOADS|.
Download the ISO from |DOWNLOADS|.
It includes the following:
* The `Proxmox Backup`_ server installer, which partitions the local
disk(s) with ext4, ext3, xfs or ZFS, and installs the operating
system.
* Complete operating system (Debian Linux, 64-bit)
* The `Proxmox Backup`_ server installer, which partitions the local
disk(s) with ext4, ext3, xfs or ZFS and installs the operating
system.
* Our Linux kernel with ZFS support.
* Complete toolset for administering backups and all necessary
resources
* Complete tool-set to administer backups and all necessary resources
* Web based management interface for using the toolset
* Web based GUI management interface
.. note:: During the installation process, the complete server
is used by default and all existing data is removed.
@ -58,8 +53,8 @@ It includes the following:
Install `Proxmox Backup`_ server on Debian
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxmox ships as a set of Debian packages, so you can install it on
top of a standard Debian installation. After configuring the
Proxmox ships as a set of Debian packages which can be installed on top of a
standard Debian installation. After configuring the
:ref:`sysadmin_package_repositories`, you need to run:
.. code-block:: console
@ -67,7 +62,7 @@ top of a standard Debian installation. After configuring the
# apt-get update
# apt-get install proxmox-backup-server
Above code keeps the current (Debian) kernel and installs a minimal
The commands above keep the current (Debian) kernel and install a minimal
set of required packages.
If you want to install the same set of packages as the installer
@ -78,16 +73,15 @@ does, please use the following:
# apt-get update
# apt-get install proxmox-backup
This installs all required packages, the Proxmox kernel with ZFS_
support, and a set of commonly useful packages.
This will install all required packages, the Proxmox kernel with ZFS_
support, and a set of common and useful packages.
Installing on top of an existing Debian_ installation looks easy, but
it presumes that you have correctly installed the base system, and you
know how you want to configure and use the local storage. Network
configuration is also completely up to you.
Installing `Proxmox Backup`_ on top of an existing Debian_ installation looks easy, but
it presumes that the base system and local storage has been set up correctly.
In general, this is not trivial, especially when you use LVM_ or
ZFS_.
In general this is not trivial, especially when LVM_ or ZFS_ is used.
The network configuration is completely up to you as well.
Install Proxmox Backup server on `Proxmox VE`_
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -101,9 +95,9 @@ After configuring the
# apt-get install proxmox-backup-server
.. caution:: Installing the backup server directly on the hypervisor
is not recommended. It is more secure to use a separate physical
server to store backups. If the hypervisor server fails, you can
still access your backups.
is not recommended. It is safer to use a separate physical
server to store backups. Should the hypervisor server fail, you can
still access the backups.
Client installation
-------------------
@ -111,8 +105,8 @@ Client installation
Install `Proxmox Backup`_ client on Debian
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Proxmox ships as a set of Debian packages, so you can install it on
top of a standard Debian installation. After configuring the
Proxmox ships as a set of Debian packages to be installed on
top of a standard Debian installation. After configuring the
:ref:`sysadmin_package_repositories`, you need to run:
.. code-block:: console

View File

@ -1,101 +1,105 @@
Introduction
============
This documentationm is written in :term:`reStructuredText` and formatted with :term:`Sphinx`.
This documentation is written in :term:`reStructuredText` and formatted with
:term:`Sphinx`.
What is Proxmox Backup
----------------------
What is Proxmox Backup Server
-----------------------------
Proxmox Backup is an enterprise class client-server backup software,
specially optimized for `Proxmox Virtual Environment`_ to backup
:term:`virtual machine`\ s and :term:`container`\ s. It is also
possible to backup physical hosts.
Proxmox Backup Server is an enterprise-class client-server backup software that
backups :term:`virtual machine`\ s, :term:`container`\ s, and physical hosts.
It is specially optimized for the `Proxmox Virtual Environment`_ platform and
allows you to backup your data securely, even between remote sites, providing
easy management with a web-based user interface.
It supports deduplication, compression and authenticated encryption
(AE_). Using :term:`Rust` as implementation language guarantees high
Proxmox Backup Server supports deduplication, compression, and authenticated
encryption (AE_). Using :term:`Rust` as implementation language guarantees high
performance, low resource usage, and a safe, high quality code base.
Encryption is done at the client side. This makes backups to not fully
trusted targets possible.
It features strong encryption done on the client side. Thus, it's possible to
backup data to not fully trusted targets.
Architecture
------------
Proxmox Backup uses a `Client-server model`_. The server is
responsible to store the backup data, and provides an API to create
backups and restore data. It is also possible to manage disks and
other server side resources using this API.
Proxmox Backup Server uses a `client-server model`_. The server stores the
backup data and provides an API to create backups and restore data. With the
API it's also possible to manage disks and other server side resources.
A backup client uses this API to access the backed up data,
i.e. ``proxmox-backup-client`` is a command line tool to create
backups and restore data. We also deliver an integrated client for
QEMU_ with `Proxmox Virtual Environment`_.
The backup client uses this API to access the backed up data. With the command
line tool ``proxmox-backup-client`` you can create backups and restore data.
For QEMU_ with `Proxmox Virtual Environment`_ we deliver an integrated client.
A single backup is allowed to contain several archives. For example,
when you backup a :term:`virtual machine`, each disk is stored as a
separate archive inside that backup. The VM configuration also gets an
extra file. This way, it is easy to access and restore important parts
of the backup, without having to scan the whole backup.
A single backup is allowed to contain several archives. For example, when you
backup a :term:`virtual machine`, each disk is stored as a separate archive
inside that backup. The VM configuration itself is stored as an extra file.
This way, it is easy to access and restore only important parts of the backup
without the need to scan the whole backup.
Main features
Main Features
-------------
:Proxmox VE: The `Proxmox Virtual Environment`_ is fully
supported. You can backup :term:`virtual machine`\ s and
:Support for Proxmox VE: The `Proxmox Virtual Environment`_ is fully
supported and you can easily backup :term:`virtual machine`\ s and
:term:`container`\ s.
:GUI: We provide a graphical, web based user interface.
:Performance: The whole software stack is written in :term:`Rust`,
to provide high speed and memory efficiency.
:Deduplication: Incremental backup produces large amounts of duplicate
data. The deduplication layer removes that redundancy and makes
inkremental backup small and space efficient.
:Deduplication: Periodic backups produce large amounts of duplicate
data. The deduplication layer avoids redundancy and minimizes the used
storage space.
:Incremental backups: Changes between backups are typically low. Reading and
sending only the delta reduces storage and network impact of backups.
:Data Integrity: The built in `SHA-256`_ checksum algorithm assures the
accuracy and consistency of your backups.
:Remote Sync: It is possible to efficently synchronize data to remote
sites. Only deltas containing new data are transfered.
:Remote Sync: It is possible to efficiently synchronize data to remote
sites. Only deltas containing new data are transferred.
:Performance: The whole software stack is written in :term:`Rust`,
which provides high speed and memory efficiency.
:Compression: Ultra fast Zstandard_ compression is able to compress
:Compression: The ultra fast Zstandard_ compression is able to compress
several gigabytes of data per second.
:Encryption: Backups can be encrypted at client side using AES-256 in
:Encryption: Backups can be encrypted on the client-side using AES-256 in
GCM_ mode. This authenticated encryption mode (AE_) provides very
high performance on modern hardware.
:Open Source: No secrets. You have access to the whole source tree.
:Web interface: Manage Proxmox backups with the integrated web-based user
interface.
:Support: Commercial support options available from `Proxmox`_.
:Open Source: No secrets. Proxmox Backup Server is free and open-source
software. The source code is licensed under AGPL, v3.
:Support: Enterprise support is available from `Proxmox`_.
Why Backup?
-----------
Reasons for Data Backup?
------------------------
The primary purpose of backup is to protect against data loss. Data
loss can happen because of faulty hardware, but also by human errors.
The main purpose of a backup is to protect against data loss. Data loss can be
caused by faulty hardware but also by human error.
A common mistake is to delete a file or folder which is still
required. Virtualization can amplify this problem, because it is now
easy to delete a whole virtual machine by a single button press.
A common mistake is to accidentally delete a file or folder which is still
required. Virtualization can even amplify this problem; it easily happens that
a whole virtual machine is deleted by just pressing a single button.
Backups can also serve as a toolkit for administrators to temporarily
store data. For example, it is common practice to perform full backups
before installing major software updates. If something goes wrong, you
can just restore the previous state.
For administrators, backups can serve as a useful toolkit for temporarily
storing data. For example, it is common practice to perform full backups before
installing major software updates. If something goes wrong, you can easily
restore the previous state.
Another reason for backups are legal requirements. Some data must be
kept in a safe place for several years so that you can access it if
required by law.
Another reason for backups are legal requirements. Some data, especially
business records, must be kept in a safe place for several years by law, so
that they can be accessed if required.
Data loss can be very costly as it can severely restrict your
business. Therefore, make sure that you regularly perform a backup
and run restore tests.
In general, data loss is very costly as it can severely damage your business.
Therefore, ensure that you perform regular backups and run restore tests.
Software Stack
@ -107,14 +111,14 @@ Software Stack
License
-------
Copyright (C) 2019 Proxmox Server Solutions GmbH
Copyright (C) 2019-2020 Proxmox Server Solutions GmbH
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
Proxmox Backup is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
Proxmox Backup Server is free and open source software: you can use it,
redistribute it, and/or modify it under the terms of the GNU Affero General
Public License as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but
``WITHOUT ANY WARRANTY``; without even the implied warranty of

View File

@ -5,12 +5,12 @@ Debian Package Repositories
All Debian based systems use APT_ as package
management tool. The list of repositories is defined in
``/etc/apt/sources.list`` and ``.list`` files found inside
``/etc/apt/sources.d/``. Updates can be installed directly using
``/etc/apt/sources.list`` and ``.list`` files found in the
``/etc/apt/sources.d/`` directory. Updates can be installed directly with
the ``apt`` command line tool, or via the GUI.
APT_ ``sources.list`` files list one package repository per line, with
the most preferred source listed first. Empty lines are ignored, and a
the most preferred source listed first. Empty lines are ignored and a
``#`` character anywhere on a line marks the remainder of that line as a
comment. The information available from the configured sources is
acquired by ``apt update``.
@ -33,7 +33,7 @@ the backup server binaries.
`Proxmox Backup`_ Enterprise Repository
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This is the default, stable and recommended repository, available for
This is the default, stable, and recommended repository. It is available for
all `Proxmox Backup`_ subscription users. It contains the most stable packages,
and is suitable for production use. The ``pbs-enterprise`` repository is
enabled by default:
@ -44,15 +44,13 @@ enabled by default:
deb https://enterprise.proxmox.com/debian/pbs buster pbs-enterprise
As soon as updates are available, the superuser (``root@pam`` user) is
notified via email about the available new packages. On the GUI, the
change-log of each package can be viewed (if available), showing all
details of the update. So you will never miss important security
fixes.
To never miss important security fixes, the superuser (``root@pam`` user) is
notified via email about new packages as soon as they are available. The
change-log and details of each package can be viewed in the GUI (if available).
Please note that you need a valid subscription key to access this
repository. We offer different support levels, and you can find further
details at https://www.proxmox.com/en/proxmox-backup/pricing.
repository. More information regarding subscription levels and pricing can be
found at https://www.proxmox.com/en/proxmox-backup/pricing.
.. note:: You can disable this repository by commenting out the above
line using a `#` (at the start of the line). This prevents error
@ -65,7 +63,7 @@ details at https://www.proxmox.com/en/proxmox-backup/pricing.
As the name suggests, you do not need a subscription key to access
this repository. It can be used for testing and non-production
use. Its not recommended to run on production servers, as these
use. It is not recommended to use it on production servers, because these
packages are not always heavily tested and validated.
We recommend to configure this repository in ``/etc/apt/sources.list``.
@ -92,9 +90,9 @@ latest packages and is heavily used by developers to test new
features.
.. warning:: the ``pbstest`` repository should (as the name implies)
only be used for testing new features or bug fixes.
only be used to test new features or bug fixes.
As usual, you can configure this using ``/etc/apt/sources.list`` by
You can configure this using ``/etc/apt/sources.list`` by
adding the following line:
.. code-block:: sources.list

View File

@ -1,15 +1,15 @@
Description
^^^^^^^^^^^
``pxar`` is a command line utility used to create and manipulate archives in the
``pxar`` is a command line utility to create and manipulate archives in the
:ref:`pxar-format`.
It is inspired by `casync file archive format
<http://0pointer.net/blog/casync-a-tool-for-distributing-file-system-images.html>`_,
which has a similar use-case.
The ``.pxar`` format is adapted to fulfill the specific needs of the proxmox
backup server, for example efficient storage of hardlinks.
which caters to a similar use-case.
The ``.pxar`` format is adapted to fulfill the specific needs of the Proxmox
Backup Server, for example, efficient storage of hardlinks.
The format is designed to reduce storage space needed on the server by achieving
high de-duplication.
a high level of de-duplication.
Creating an Archive
^^^^^^^^^^^^^^^^^^^
@ -20,23 +20,23 @@ Run the following command to create an archive of a folder named ``source``:
# pxar create archive.pxar source
This will create a new archive called ``archive.pxar`` from the contents of the
This will create a new archive called ``archive.pxar`` with the contents of the
``source`` folder.
.. NOTE:: ``pxar`` will not overwrite any existing archives. If an archive with
the same name is already present in the target folder, the creation will
fail.
By default, ``pxar`` will skip certain mountpoints and not follow device
By default, ``pxar`` will skip certain mountpoints and will not follow device
boundaries. This design decision is based on the primary use case of creating
archives for backups, where it makes no sense to store the content of certain
archives for backups. It is sensible to not back up the contents of certain
temporary or system specific files.
In order to alter this behavior and follow device boundaries, use the
To alter this behavior and follow device boundaries, use the
``--all-file-systems`` flag.
It is possible to exclude certain files and/or folders from the archive by
passing glob match patterns as additional parameters. Whenever a file is matched
by one of the patterns, you will get a warning saying that this file is skipped
by one of the patterns, you will get a warning stating that this file is skipped
and therefore not included in the archive.
For example, you can exclude all files ending in ``.txt`` from the archive
@ -50,7 +50,7 @@ Be aware that the shell itself will try to expand all of the glob patterns befor
invoking ``pxar``.
In order to avoid this, all globs have to be quoted correctly.
It is also possible to pass a list of match pattern to fulfill more complex
It is possible to pass a list of match patterns to fulfill more complex
file exclusion/inclusion behavior, although it is recommended to use the
``.pxarexclude`` files instead for such cases.
@ -67,7 +67,7 @@ All the glob pattern are relative to the ``source`` directory.
previous ones. Permutations of the same patterns lead to different results.
``pxar`` will store the list of glob match patterns passed as parameters via the
command line in a file called ``.pxarexclude-cli`` and store it at the root of
command line in a file called ``.pxarexclude-cli`` and stores it at the root of
the archive.
If a file with this name is already present in the source folder during archive
creation, this file is not included in the archive and the file containing the
@ -79,9 +79,9 @@ It is possible to create and place these files in any directory of the filesyste
tree.
These files must contain one pattern per line, again later patterns win over
previous ones.
The patterns control file exclusion of files present within the given directory
The patterns control file exclusions of files present within the given directory
or further below it in the tree.
The behaviour is the same as described in :ref:`creating-backups`.
The behavior is the same as described in :ref:`creating-backups`.
Extracting an Archive
^^^^^^^^^^^^^^^^^^^^^
@ -96,7 +96,7 @@ with the following command:
If no target is provided, the content of the archive is extracted to the current
working directory.
In order to restore only part of an archive or single files and/or folders,
In order to restore only parts of an archive, single files and/or folders,
it is possible to pass the corresponding glob match patterns as additional
parameters or use the patterns stored in a file:
@ -109,8 +109,8 @@ sub-folders in the archive ``etc.pxar`` to the target ``/restore/target/etc``.
A path to the file containing match patterns can be specified using the
``--files-from`` parameter.
List the Content of an Archive
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
List the Contents of an Archive
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
To display the files and directories contained in an archive ``archive.pxar``,
run the following command:
@ -126,7 +126,7 @@ Mounting an Archive
^^^^^^^^^^^^^^^^^^^
``pxar`` allows you to mount and inspect the contents of an archive via _`FUSE`.
In order to mount an archive named ``archive.pxar`` to the mountpoint ``mnt``,
In order to mount an archive named ``archive.pxar`` to the mountpoint ``/mnt``,
run the command:
.. code-block:: console

View File

@ -2,7 +2,7 @@
Description=Proxmox Backup API Proxy Server
Wants=network-online.target
After=network.target
Requires=proxmox-backup.service
Wants=proxmox-backup.service
After=proxmox-backup.service
[Service]

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
// chacha20-poly1305

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use proxmox::api::{*, cli::*};
@ -49,7 +49,7 @@ fn hello_command(
}
#[api(input: { properties: {} })]
/// Quit command. Exit the programm.
/// Quit command. Exit the program.
///
/// Returns: nothing
fn quit_command() -> Result<(), Error> {
@ -83,7 +83,8 @@ fn main() -> Result<(), Error> {
let args = shellword_split(&line)?;
let _ = handle_command(helper.cmd_def(), "", args, None);
let rpcenv = CliEnvironment::new();
let _ = handle_command(helper.cmd_def(), "", args, rpcenv, None);
rl.add_history_entry(line);
}

View File

@ -1,6 +1,6 @@
use std::io::Write;
use failure::*;
use anyhow::{Error};
use chrono::{DateTime, Utc};
@ -44,8 +44,8 @@ async fn run() -> Result<(), Error> {
let mut bytes = 0;
for _ in 0..100 {
let writer = DummyWriter { bytes: 0 };
let writer = client.speedtest(writer).await?;
let mut writer = DummyWriter { bytes: 0 };
client.speedtest(&mut writer).await?;
println!("Received {} bytes", writer.bytes);
bytes += writer.bytes;
}
@ -59,8 +59,7 @@ async fn run() -> Result<(), Error> {
Ok(())
}
#[tokio::main]
async fn main() {
fn main() {
if let Err(err) = proxmox_backup::tools::runtime::main(run()) {
eprintln!("ERROR: {}", err);
}

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, Error};
use std::thread;
use std::path::PathBuf;
@ -16,7 +16,7 @@ use std::io::Write;
// tar: dyntest1/testfile7.dat: File shrank by 2833252864 bytes; padding with zeros
// # pxar create test.pxar ./dyntest1/
// Error: detected shrinked file "./dyntest1/testfile0.dat" (22020096 < 12679380992)
// Error: detected shrunk file "./dyntest1/testfile0.dat" (22020096 < 12679380992)
fn create_large_file(path: PathBuf) {

View File

@ -2,7 +2,7 @@ use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use failure::*;
use anyhow::{Error};
use futures::future::TryFutureExt;
use futures::stream::Stream;
use tokio::net::TcpStream;

View File

@ -2,7 +2,7 @@ use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use failure::*;
use anyhow::{format_err, Error};
use futures::future::TryFutureExt;
use futures::stream::Stream;

View File

@ -1,6 +1,6 @@
use std::sync::Arc;
use failure::*;
use anyhow::{format_err, Error};
use futures::*;
use hyper::{Request, Response, Body};
use openssl::ssl::{SslMethod, SslAcceptor, SslFiletype};

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use futures::*;
// Simple H2 server to test H2 speed with h2client.rs

View File

@ -2,7 +2,7 @@ extern crate proxmox_backup;
// also see https://www.johndcook.com/blog/standard_deviation/
use failure::*;
use anyhow::{Error};
use std::io::{Read, Write};
use proxmox_backup::backup::*;

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use futures::*;
extern crate proxmox_backup;

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use proxmox_backup::client::*;
@ -17,7 +17,7 @@ async fn upload_speed() -> Result<usize, Error> {
let backup_time = chrono::Utc::now();
let client = BackupWriter::start(client, datastore, "host", "speedtest", backup_time, false).await?;
let client = BackupWriter::start(client, None, datastore, "host", "speedtest", backup_time, false).await?;
println!("start upload speed test");
let res = client.upload_speedtest().await?;

View File

@ -1,13 +1,15 @@
mod access;
pub mod access;
pub mod admin;
pub mod backup;
pub mod config;
pub mod node;
pub mod reader;
mod subscription;
pub mod status;
pub mod types;
pub mod version;
pub mod pull;
mod helpers;
use proxmox::api::router::SubdirMap;
use proxmox::api::Router;
@ -23,6 +25,7 @@ pub const SUBDIRS: SubdirMap = &[
("nodes", &NODES_ROUTER),
("pull", &pull::ROUTER),
("reader", &reader::ROUTER),
("status", &status::ROUTER),
("subscription", &subscription::ROUTER),
("version", &version::ROUTER),
];

View File

@ -1,18 +1,33 @@
use failure::*;
use anyhow::{bail, format_err, Error};
use serde_json::{json, Value};
use proxmox::api::api;
use proxmox::api::{api, RpcEnvironment, Permission, UserInformation};
use proxmox::api::router::{Router, SubdirMap};
use proxmox::sortable;
use proxmox::{sortable, identity};
use proxmox::{http_err, list_subdirs_api_method};
use crate::tools;
use crate::tools::ticket::*;
use crate::auth_helpers::*;
use crate::api2::types::*;
use crate::config::cached_user_info::CachedUserInfo;
use crate::config::acl::PRIV_PERMISSIONS_MODIFY;
pub mod user;
pub mod domain;
pub mod acl;
pub mod role;
fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
let user_info = CachedUserInfo::new()?;
if !user_info.is_active_user(&username) {
bail!("user account disabled or expired.");
}
let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
if password.starts_with("PBS:") {
@ -25,27 +40,17 @@ fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
}
}
if username == "root@pam" {
let mut auth = pam::Authenticator::with_password("proxmox-backup-auth").unwrap();
auth.get_handler().set_credentials("root", password);
auth.authenticate()?;
return Ok(());
}
bail!("inavlid credentials");
crate::auth::authenticate_user(username, password)
}
#[api(
input: {
properties: {
username: {
type: String,
description: "User name.",
max_length: 64,
schema: PROXMOX_USER_ID_SCHEMA,
},
password: {
type: String,
description: "The secret password. This can also be a valid ticket.",
schema: PASSWORD_SCHEMA,
},
},
},
@ -66,6 +71,9 @@ fn authenticate_user(username: &str, password: &str) -> Result<(), Error> {
},
},
protected: true,
access: {
permission: &Permission::World,
},
)]
/// Create or verify authentication ticket.
///
@ -94,13 +102,72 @@ fn create_ticket(username: String, password: String) -> Result<Value, Error> {
}
}
#[api(
input: {
properties: {
userid: {
schema: PROXMOX_USER_ID_SCHEMA,
},
password: {
schema: PASSWORD_SCHEMA,
},
},
},
access: {
description: "Anybody is allowed to change there own password. In addition, users with 'Permissions:Modify' privilege may change any password.",
permission: &Permission::Anybody,
},
)]
/// Change user password
///
/// Each user is allowed to change his own password. Superuser
/// can change all passwords.
fn change_password(
userid: String,
password: String,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
let current_user = rpcenv.get_user()
.ok_or_else(|| format_err!("unknown user"))?;
let mut allowed = userid == current_user;
if userid == "root@pam" { allowed = true; }
if !allowed {
let user_info = CachedUserInfo::new()?;
let privs = user_info.lookup_privs(&current_user, &[]);
if (privs & PRIV_PERMISSIONS_MODIFY) != 0 { allowed = true; }
}
if !allowed {
bail!("you are not authorized to change the password.");
}
let (username, realm) = crate::auth::parse_userid(&userid)?;
let authenticator = crate::auth::lookup_authenticator(&realm)?;
authenticator.store_password(&username, &password)?;
Ok(Value::Null)
}
#[sortable]
const SUBDIRS: SubdirMap = &[
const SUBDIRS: SubdirMap = &sorted!([
("acl", &acl::ROUTER),
(
"password", &Router::new()
.put(&API_METHOD_CHANGE_PASSWORD)
),
(
"ticket", &Router::new()
.post(&API_METHOD_CREATE_TICKET)
)
];
),
("domains", &domain::ROUTER),
("roles", &role::ROUTER),
("users", &user::ROUTER),
]);
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))

228
src/api2/access/acl.rs Normal file
View File

@ -0,0 +1,228 @@
use anyhow::{bail, Error};
use ::serde::{Deserialize, Serialize};
use proxmox::api::{api, Router, RpcEnvironment, Permission};
use crate::api2::types::*;
use crate::config::acl;
use crate::config::acl::{Role, PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY};
#[api(
properties: {
propagate: {
schema: ACL_PROPAGATE_SCHEMA,
},
path: {
schema: ACL_PATH_SCHEMA,
},
ugid_type: {
schema: ACL_UGID_TYPE_SCHEMA,
},
ugid: {
type: String,
description: "User or Group ID.",
},
roleid: {
type: Role,
}
}
)]
#[derive(Serialize, Deserialize)]
/// ACL list entry.
pub struct AclListItem {
path: String,
ugid: String,
ugid_type: String,
propagate: bool,
roleid: String,
}
fn extract_acl_node_data(
node: &acl::AclTreeNode,
path: &str,
list: &mut Vec<AclListItem>,
exact: bool,
) {
for (user, roles) in &node.users {
for (role, propagate) in roles {
list.push(AclListItem {
path: if path.is_empty() { String::from("/") } else { path.to_string() },
propagate: *propagate,
ugid_type: String::from("user"),
ugid: user.to_string(),
roleid: role.to_string(),
});
}
}
for (group, roles) in &node.groups {
for (role, propagate) in roles {
list.push(AclListItem {
path: if path.is_empty() { String::from("/") } else { path.to_string() },
propagate: *propagate,
ugid_type: String::from("group"),
ugid: group.to_string(),
roleid: role.to_string(),
});
}
}
if exact {
return;
}
for (comp, child) in &node.children {
let new_path = format!("{}/{}", path, comp);
extract_acl_node_data(child, &new_path, list, exact);
}
}
#[api(
input: {
properties: {
path: {
schema: ACL_PATH_SCHEMA,
optional: true,
},
exact: {
description: "If set, returns only ACL for the exact path.",
type: bool,
optional: true,
default: false,
},
},
},
returns: {
description: "ACL entry list.",
type: Array,
items: {
type: AclListItem,
}
},
access: {
permission: &Permission::Privilege(&["access", "acl"], PRIV_SYS_AUDIT, false),
},
)]
/// Read Access Control List (ACLs).
pub fn read_acl(
path: Option<String>,
exact: bool,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<AclListItem>, Error> {
//let auth_user = rpcenv.get_user().unwrap();
let (mut tree, digest) = acl::config()?;
let mut list: Vec<AclListItem> = Vec::new();
if let Some(path) = &path {
if let Some(node) = &tree.find_node(path) {
extract_acl_node_data(&node, path, &mut list, exact);
}
} else {
extract_acl_node_data(&tree.root, "", &mut list, exact);
}
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list)
}
#[api(
protected: true,
input: {
properties: {
path: {
schema: ACL_PATH_SCHEMA,
},
role: {
type: Role,
},
propagate: {
optional: true,
schema: ACL_PROPAGATE_SCHEMA,
},
userid: {
optional: true,
schema: PROXMOX_USER_ID_SCHEMA,
},
group: {
optional: true,
schema: PROXMOX_GROUP_ID_SCHEMA,
},
delete: {
optional: true,
description: "Remove permissions (instead of adding it).",
type: bool,
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["access", "acl"], PRIV_PERMISSIONS_MODIFY, false),
},
)]
/// Update Access Control List (ACLs).
pub fn update_acl(
path: String,
role: String,
propagate: Option<bool>,
userid: Option<String>,
group: Option<String>,
delete: Option<bool>,
digest: Option<String>,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(acl::ACL_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut tree, expected_digest) = acl::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
let propagate = propagate.unwrap_or(true);
let delete = delete.unwrap_or(false);
if let Some(ref _group) = group {
bail!("parameter 'group' - groups are currently not supported.");
} else if let Some(ref userid) = userid {
if !delete { // Note: we allow to delete non-existent users
let user_cfg = crate::config::user::cached_config()?;
if user_cfg.sections.get(userid).is_none() {
bail!("no such user.");
}
}
} else {
bail!("missing 'userid' or 'group' parameter.");
}
if !delete { // Note: we allow to delete entries with invalid path
acl::check_acl_path(&path)?;
}
if let Some(userid) = userid {
if delete {
tree.delete_user_role(&path, &userid, &role);
} else {
tree.insert_user_role(&path, &userid, &role, propagate);
}
} else if let Some(group) = group {
if delete {
tree.delete_group_role(&path, &group, &role);
} else {
tree.insert_group_role(&path, &group, &role, propagate);
}
}
acl::save_config(&tree)?;
Ok(())
}
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_READ_ACL)
.put(&API_METHOD_UPDATE_ACL);

47
src/api2/access/domain.rs Normal file
View File

@ -0,0 +1,47 @@
use anyhow::{Error};
use serde_json::{json, Value};
use proxmox::api::{api, Permission};
use proxmox::api::router::Router;
use crate::api2::types::*;
#[api(
returns: {
description: "List of realms.",
type: Array,
items: {
type: Object,
description: "User configuration (without password).",
properties: {
realm: {
description: "Realm ID.",
type: String,
},
comment: {
schema: SINGLE_LINE_COMMENT_SCHEMA,
optional: true,
},
default: {
description: "Default realm.",
type: bool,
}
},
}
},
access: {
description: "Anyone can access this, because we need that list for the login box (before the user is authenticated).",
permission: &Permission::World,
}
)]
/// Authentication domain/realm index.
fn list_domains() -> Result<Value, Error> {
let mut list = Vec::new();
list.push(json!({ "realm": "pam", "comment": "Linux PAM standard authentication", "default": true }));
list.push(json!({ "realm": "pbs", "comment": "Proxmox Backup authentication server" }));
Ok(list.into())
}
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_LIST_DOMAINS);

58
src/api2/access/role.rs Normal file
View File

@ -0,0 +1,58 @@
use anyhow::Error;
use serde_json::{json, Value};
use proxmox::api::{api, Permission};
use proxmox::api::router::Router;
use crate::api2::types::*;
use crate::config::acl::{Role, ROLE_NAMES, PRIVILEGES};
#[api(
returns: {
description: "List of roles.",
type: Array,
items: {
type: Object,
description: "User name with description.",
properties: {
roleid: {
type: Role,
},
privs: {
type: Array,
description: "List of Privileges",
items: {
type: String,
description: "A Privilege",
},
},
comment: {
schema: SINGLE_LINE_COMMENT_SCHEMA,
optional: true,
},
},
}
},
access: {
permission: &Permission::Anybody,
}
)]
/// Role list
fn list_roles() -> Result<Value, Error> {
let mut list = Vec::new();
for (role, (privs, comment)) in ROLE_NAMES.iter() {
let mut priv_list = Vec::new();
for (name, privilege) in PRIVILEGES.iter() {
if privs & privilege > 0 {
priv_list.push(name.clone());
}
}
list.push(json!({ "roleid": role, "privs": priv_list, "comment": comment }));
}
Ok(list.into())
}
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_LIST_ROLES);

295
src/api2/access/user.rs Normal file
View File

@ -0,0 +1,295 @@
use anyhow::{bail, Error};
use serde_json::Value;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
use proxmox::api::schema::{Schema, StringSchema};
use crate::api2::types::*;
use crate::config::user;
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_PERMISSIONS_MODIFY};
pub const PBS_PASSWORD_SCHEMA: Schema = StringSchema::new("User Password.")
.format(&PASSWORD_FORMAT)
.min_length(5)
.max_length(64)
.schema();
#[api(
input: {
properties: {},
},
returns: {
description: "List users (with config digest).",
type: Array,
items: { type: user::User },
},
access: {
permission: &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false),
},
)]
/// List all users
pub fn list_users(
_param: Value,
_info: &ApiMethod,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<user::User>, Error> {
let (config, digest) = user::config()?;
let list = config.convert_to_typed_array("user")?;
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list)
}
#[api(
protected: true,
input: {
properties: {
userid: {
schema: PROXMOX_USER_ID_SCHEMA,
},
comment: {
schema: SINGLE_LINE_COMMENT_SCHEMA,
optional: true,
},
password: {
schema: PBS_PASSWORD_SCHEMA,
optional: true,
},
enable: {
schema: user::ENABLE_USER_SCHEMA,
optional: true,
},
expire: {
schema: user::EXPIRE_USER_SCHEMA,
optional: true,
},
firstname: {
schema: user::FIRST_NAME_SCHEMA,
optional: true,
},
lastname: {
schema: user::LAST_NAME_SCHEMA,
optional: true,
},
email: {
schema: user::EMAIL_SCHEMA,
optional: true,
},
},
},
access: {
permission: &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
},
)]
/// Create new user.
pub fn create_user(password: Option<String>, param: Value) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let user: user::User = serde_json::from_value(param)?;
let (mut config, _digest) = user::config()?;
if let Some(_) = config.sections.get(&user.userid) {
bail!("user '{}' already exists.", user.userid);
}
let (username, realm) = crate::auth::parse_userid(&user.userid)?;
let authenticator = crate::auth::lookup_authenticator(&realm)?;
config.set_data(&user.userid, "user", &user)?;
user::save_config(&config)?;
if let Some(password) = password {
authenticator.store_password(&username, &password)?;
}
Ok(())
}
#[api(
input: {
properties: {
userid: {
schema: PROXMOX_USER_ID_SCHEMA,
},
},
},
returns: {
description: "The user configuration (with config digest).",
type: user::User,
},
access: {
permission: &Permission::Privilege(&["access", "users"], PRIV_SYS_AUDIT, false),
},
)]
/// Read user configuration data.
pub fn read_user(userid: String, mut rpcenv: &mut dyn RpcEnvironment) -> Result<user::User, Error> {
let (config, digest) = user::config()?;
let user = config.lookup("user", &userid)?;
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(user)
}
#[api(
protected: true,
input: {
properties: {
userid: {
schema: PROXMOX_USER_ID_SCHEMA,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
password: {
schema: PBS_PASSWORD_SCHEMA,
optional: true,
},
enable: {
schema: user::ENABLE_USER_SCHEMA,
optional: true,
},
expire: {
schema: user::EXPIRE_USER_SCHEMA,
optional: true,
},
firstname: {
schema: user::FIRST_NAME_SCHEMA,
optional: true,
},
lastname: {
schema: user::LAST_NAME_SCHEMA,
optional: true,
},
email: {
schema: user::EMAIL_SCHEMA,
optional: true,
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
},
)]
/// Update user configuration.
pub fn update_user(
userid: String,
comment: Option<String>,
enable: Option<bool>,
expire: Option<i64>,
password: Option<String>,
firstname: Option<String>,
lastname: Option<String>,
email: Option<String>,
digest: Option<String>,
) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut config, expected_digest) = user::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
let mut data: user::User = config.lookup("user", &userid)?;
if let Some(comment) = comment {
let comment = comment.trim().to_string();
if comment.is_empty() {
data.comment = None;
} else {
data.comment = Some(comment);
}
}
if let Some(enable) = enable {
data.enable = if enable { None } else { Some(false) };
}
if let Some(expire) = expire {
data.expire = if expire > 0 { Some(expire) } else { None };
}
if let Some(password) = password {
let (username, realm) = crate::auth::parse_userid(&userid)?;
let authenticator = crate::auth::lookup_authenticator(&realm)?;
authenticator.store_password(&username, &password)?;
}
if let Some(firstname) = firstname {
data.firstname = if firstname.is_empty() { None } else { Some(firstname) };
}
if let Some(lastname) = lastname {
data.lastname = if lastname.is_empty() { None } else { Some(lastname) };
}
if let Some(email) = email {
data.email = if email.is_empty() { None } else { Some(email) };
}
config.set_data(&userid, "user", &data)?;
user::save_config(&config)?;
Ok(())
}
#[api(
protected: true,
input: {
properties: {
userid: {
schema: PROXMOX_USER_ID_SCHEMA,
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["access", "users"], PRIV_PERMISSIONS_MODIFY, false),
},
)]
/// Remove a user from the configuration file.
pub fn delete_user(userid: String, digest: Option<String>) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(user::USER_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut config, expected_digest) = user::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
match config.sections.get(&userid) {
Some(_) => { config.sections.remove(&userid); },
None => bail!("user '{}' does not exist.", userid),
}
user::save_config(&config)?;
Ok(())
}
const ITEM_ROUTER: Router = Router::new()
.get(&API_METHOD_READ_USER)
.put(&API_METHOD_UPDATE_USER)
.delete(&API_METHOD_DELETE_USER);
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_LIST_USERS)
.post(&API_METHOD_CREATE_USER)
.match_all("userid", &ITEM_ROUTER);

View File

@ -2,9 +2,11 @@ use proxmox::api::router::{Router, SubdirMap};
use proxmox::list_subdirs_api_method;
pub mod datastore;
pub mod sync;
const SUBDIRS: SubdirMap = &[
("datastore", &datastore::ROUTER)
("datastore", &datastore::ROUTER),
("sync", &sync::ROUTER)
];
pub const ROUTER: Router = Router::new()

File diff suppressed because it is too large Load Diff

130
src/api2/admin/sync.rs Normal file
View File

@ -0,0 +1,130 @@
use anyhow::{Error};
use serde_json::Value;
use std::collections::HashMap;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment};
use proxmox::api::router::SubdirMap;
use proxmox::{list_subdirs_api_method, sortable};
use crate::api2::types::*;
use crate::api2::pull::{get_pull_parameters};
use crate::config::sync::{self, SyncJobStatus, SyncJobConfig};
use crate::server::{self, TaskListInfo, WorkerTask};
use crate::tools::systemd::time::{
parse_calendar_event, compute_next_event};
#[api(
input: {
properties: {},
},
returns: {
description: "List configured jobs and their status.",
type: Array,
items: { type: sync::SyncJobStatus },
},
)]
/// List all sync jobs
pub fn list_sync_jobs(
_param: Value,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<SyncJobStatus>, Error> {
let (config, digest) = sync::config()?;
let mut list: Vec<SyncJobStatus> = config.convert_to_typed_array("sync")?;
let mut last_tasks: HashMap<String, &TaskListInfo> = HashMap::new();
let tasks = server::read_task_list()?;
for info in tasks.iter() {
let worker_id = match &info.upid.worker_id {
Some(id) => id,
_ => { continue; },
};
if let Some(last) = last_tasks.get(worker_id) {
if last.upid.starttime < info.upid.starttime {
last_tasks.insert(worker_id.to_string(), &info);
}
} else {
last_tasks.insert(worker_id.to_string(), &info);
}
}
for job in &mut list {
let mut last = 0;
if let Some(task) = last_tasks.get(&job.id) {
job.last_run_upid = Some(task.upid_str.clone());
if let Some((endtime, status)) = &task.state {
job.last_run_state = Some(String::from(status));
job.last_run_endtime = Some(*endtime);
last = *endtime;
}
}
job.next_run = (|| -> Option<i64> {
let schedule = job.schedule.as_ref()?;
let event = parse_calendar_event(&schedule).ok()?;
compute_next_event(&event, last, false).ok()
})();
}
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list)
}
#[api(
input: {
properties: {
id: {
schema: JOB_ID_SCHEMA,
}
}
}
)]
/// Runs the sync jobs manually.
async fn run_sync_job(
id: String,
_info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> {
let (config, _digest) = sync::config()?;
let sync_job: SyncJobConfig = config.lookup("sync", &id)?;
let username = rpcenv.get_user().unwrap();
let delete = sync_job.remove_vanished.unwrap_or(true);
let (client, src_repo, tgt_store) = get_pull_parameters(&sync_job.store, &sync_job.remote, &sync_job.remote_store).await?;
let upid_str = WorkerTask::spawn("syncjob", Some(id.clone()), &username.clone(), false, move |worker| async move {
worker.log(format!("sync job '{}' start", &id));
crate::client::pull::pull_store(&worker, &client, &src_repo, tgt_store.clone(), delete, String::from("backup@pam")).await?;
worker.log(format!("sync job '{}' end", &id));
Ok(())
})?;
Ok(upid_str)
}
#[sortable]
const SYNC_INFO_SUBDIRS: SubdirMap = &[
(
"run",
&Router::new()
.post(&API_METHOD_RUN_SYNC_JOB)
),
];
const SYNC_INFO_ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SYNC_INFO_SUBDIRS))
.subdirs(SYNC_INFO_SUBDIRS);
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_LIST_SYNC_JOBS)
.match_all("id", &SYNC_INFO_ROUTER);

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, format_err, Error};
use futures::*;
use hyper::header::{HeaderValue, UPGRADE};
use hyper::http::request::Parts;
@ -6,14 +6,16 @@ use hyper::{Body, Response, StatusCode};
use serde_json::{json, Value};
use proxmox::{sortable, identity, list_subdirs_api_method};
use proxmox::api::{ApiResponseFuture, ApiHandler, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{ApiResponseFuture, ApiHandler, ApiMethod, Router, RpcEnvironment, Permission};
use proxmox::api::router::SubdirMap;
use proxmox::api::schema::*;
use crate::tools::{self, WrappedReaderStream};
use crate::tools;
use crate::server::{WorkerTask, H2Service};
use crate::backup::*;
use crate::api2::types::*;
use crate::config::acl::PRIV_DATASTORE_BACKUP;
use crate::config::cached_user_info::CachedUserInfo;
mod environment;
use environment::*;
@ -37,6 +39,10 @@ pub const API_METHOD_UPGRADE_BACKUP: ApiMethod = ApiMethod::new(
("debug", true, &BooleanSchema::new("Enable verbose debug logging.").schema()),
]),
)
).access(
// Note: parameter 'store' is no uri parameter, so we need to test inside function body
Some("The user needs Datastore.Backup privilege on /datastore/{store} and needs to own the backup group."),
&Permission::Anybody
);
fn upgrade_to_backup_protocol(
@ -47,10 +53,16 @@ fn upgrade_to_backup_protocol(
rpcenv: Box<dyn RpcEnvironment>,
) -> ApiResponseFuture {
async move {
async move {
let debug = param["debug"].as_bool().unwrap_or(false);
let username = rpcenv.get_user().unwrap();
let store = tools::required_string_param(&param, "store")?.to_owned();
let user_info = CachedUserInfo::new()?;
user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_BACKUP, false)?;
let datastore = DataStore::lookup_datastore(&store)?;
let backup_type = tools::required_string_param(&param, "backup-type")?;
@ -73,10 +85,15 @@ fn upgrade_to_backup_protocol(
let worker_id = format!("{}_{}_{}", store, backup_type, backup_id);
let username = rpcenv.get_user().unwrap();
let env_type = rpcenv.env_type();
let backup_group = BackupGroup::new(backup_type, backup_id);
let owner = datastore.create_backup_group(&backup_group, &username)?;
// permission check
if owner != username { // only the owner is allowed to create additional snapshots
bail!("backup owner check failed ({} != {})", username, owner);
}
let last_backup = BackupInfo::last_backup(&datastore.base_path(), &backup_group).unwrap_or(None);
let backup_dir = BackupDir::new_with_group(backup_group, backup_time);
@ -90,7 +107,7 @@ fn upgrade_to_backup_protocol(
}
let (path, is_new) = datastore.create_backup_dir(&backup_dir)?;
if !is_new { bail!("backup directorty already exists."); }
if !is_new { bail!("backup directory already exists."); }
WorkerTask::spawn("backup", Some(worker_id), &username.clone(), true, move |worker| {
let mut env = BackupEnvironment::new(
@ -106,13 +123,12 @@ fn upgrade_to_backup_protocol(
let abort_future = worker.abort_future();
let env2 = env.clone();
let env3 = env.clone();
let req_fut = req_body
let mut req_fut = req_body
.on_upgrade()
.map_err(Error::from)
.and_then(move |conn| {
env3.debug("protocol upgrade done");
env2.debug("protocol upgrade done");
let mut http = hyper::server::conn::Http::new();
http.http2_only(true);
@ -124,36 +140,39 @@ fn upgrade_to_backup_protocol(
http.serve_connection(conn, service)
.map_err(Error::from)
});
let abort_future = abort_future
let mut abort_future = abort_future
.map(|_| Err(format_err!("task aborted")));
use futures::future::Either;
future::select(req_fut, abort_future)
.map(|res| match res {
Either::Left((Ok(res), _)) => Ok(res),
Either::Left((Err(err), _)) => Err(err),
Either::Right((Ok(res), _)) => Ok(res),
Either::Right((Err(err), _)) => Err(err),
})
.and_then(move |_result| async move {
env.ensure_finished()?;
env.log("backup finished sucessfully");
Ok(())
})
.then(move |result| async move {
if let Err(err) = result {
match env2.ensure_finished() {
Ok(()) => {}, // ignore error after finish
_ => {
env2.log(format!("backup failed: {}", err));
env2.log("removing failed backup");
env2.remove_backup()?;
return Err(err);
}
}
}
Ok(())
})
async move {
let res = select!{
req = req_fut => req,
abrt = abort_future => abrt,
};
match (res, env.ensure_finished()) {
(Ok(_), Ok(())) => {
env.log("backup finished successfully");
Ok(())
},
(Err(err), Ok(())) => {
// ignore errors after finish
env.log(format!("backup had errors but finished: {}", err));
Ok(())
},
(Ok(_), Err(err)) => {
env.log(format!("backup ended and finish failed: {}", err));
env.log("removing unfinished backup");
env.remove_backup()?;
Err(err)
},
(Err(err), Err(_)) => {
env.log(format!("backup failed: {}", err));
env.log("removing failed backup");
env.remove_backup()?;
Err(err)
},
}
}
})?;
let response = Response::builder()
@ -180,7 +199,6 @@ pub const BACKUP_API_SUBDIRS: SubdirMap = &[
),
(
"dynamic_index", &Router::new()
.download(&API_METHOD_DYNAMIC_CHUNK_INDEX)
.post(&API_METHOD_CREATE_DYNAMIC_INDEX)
.put(&API_METHOD_DYNAMIC_APPEND)
),
@ -203,10 +221,13 @@ pub const BACKUP_API_SUBDIRS: SubdirMap = &[
),
(
"fixed_index", &Router::new()
.download(&API_METHOD_FIXED_CHUNK_INDEX)
.post(&API_METHOD_CREATE_FIXED_INDEX)
.put(&API_METHOD_FIXED_APPEND)
),
(
"previous", &Router::new()
.download(&API_METHOD_DOWNLOAD_PREVIOUS)
),
(
"speedtest", &Router::new()
.upload(&API_METHOD_UPLOAD_SPEEDTEST)
@ -265,6 +286,8 @@ pub const API_METHOD_CREATE_FIXED_INDEX: ApiMethod = ApiMethod::new(
.minimum(1)
.schema()
),
("reuse-csum", true, &StringSchema::new("If set, compare last backup's \
csum and reuse index for incremental backup if it matches.").schema()),
]),
)
);
@ -277,10 +300,9 @@ fn create_fixed_index(
let env: &BackupEnvironment = rpcenv.as_ref();
println!("PARAM: {:?}", param);
let name = tools::required_string_param(&param, "archive-name")?.to_owned();
let size = tools::required_integer_param(&param, "size")? as usize;
let reuse_csum = param["reuse-csum"].as_str();
let archive_name = name.clone();
if !archive_name.ends_with(".fidx") {
@ -288,12 +310,49 @@ fn create_fixed_index(
}
let mut path = env.backup_dir.relative_path();
path.push(archive_name);
path.push(&archive_name);
let chunk_size = 4096*1024; // todo: ??
let index = env.datastore.create_fixed_writer(&path, size, chunk_size)?;
let wid = env.register_fixed_writer(index, name, size, chunk_size as u32)?;
// do incremental backup if csum is set
let mut reader = None;
let mut incremental = false;
if let Some(csum) = reuse_csum {
incremental = true;
let last_backup = match &env.last_backup {
Some(info) => info,
None => {
bail!("cannot reuse index - no previous backup exists");
}
};
let mut last_path = last_backup.backup_dir.relative_path();
last_path.push(&archive_name);
let index = match env.datastore.open_fixed_reader(last_path) {
Ok(index) => index,
Err(_) => {
bail!("cannot reuse index - no previous backup exists for archive");
}
};
let (old_csum, _) = index.compute_csum();
let old_csum = proxmox::tools::digest_to_hex(&old_csum);
if old_csum != csum {
bail!("expected csum ({}) doesn't match last backup's ({}), cannot do incremental backup",
csum, old_csum);
}
reader = Some(index);
}
let mut writer = env.datastore.create_fixed_writer(&path, size, chunk_size)?;
if let Some(reader) = reader {
writer.clone_data_from(&reader)?;
}
let wid = env.register_fixed_writer(writer, name, size, chunk_size as u32, incremental)?;
env.log(format!("created new fixed index {} ({:?})", wid, path));
@ -359,7 +418,7 @@ fn dynamic_append (
env.dynamic_writer_append_chunk(wid, offset, size, &digest)?;
env.debug(format!("sucessfully added chunk {} to dynamic index {} (offset {}, size {})", digest_str, wid, offset, size));
env.debug(format!("successfully added chunk {} to dynamic index {} (offset {}, size {})", digest_str, wid, offset, size));
}
Ok(Value::Null)
@ -424,7 +483,7 @@ fn fixed_append (
env.fixed_writer_append_chunk(wid, offset, size, &digest)?;
env.debug(format!("sucessfully added chunk {} to fixed index {} (offset {}, size {})", digest_str, wid, offset, size));
env.debug(format!("successfully added chunk {} to fixed index {} (offset {}, size {})", digest_str, wid, offset, size));
}
Ok(Value::Null)
@ -479,7 +538,7 @@ fn close_dynamic_index (
env.dynamic_writer_close(wid, chunk_count, size, csum)?;
env.log(format!("sucessfully closed dynamic index {}", wid));
env.log(format!("successfully closed dynamic index {}", wid));
Ok(Value::Null)
}
@ -501,15 +560,15 @@ pub const API_METHOD_CLOSE_FIXED_INDEX: ApiMethod = ApiMethod::new(
(
"chunk-count",
false,
&IntegerSchema::new("Chunk count. This is used to verify that the server got all chunks.")
.minimum(1)
&IntegerSchema::new("Chunk count. This is used to verify that the server got all chunks. Ignored for incremental backups.")
.minimum(0)
.schema()
),
(
"size",
false,
&IntegerSchema::new("File size. This is used to verify that the server got all data.")
.minimum(1)
&IntegerSchema::new("File size. This is used to verify that the server got all data. Ignored for incremental backups.")
.minimum(0)
.schema()
),
("csum", false, &StringSchema::new("Digest list checksum.").schema()),
@ -533,7 +592,7 @@ fn close_fixed_index (
env.fixed_writer_close(wid, chunk_count, size, csum)?;
env.log(format!("sucessfully closed fixed index {}", wid));
env.log(format!("successfully closed fixed index {}", wid));
Ok(Value::Null)
}
@ -547,26 +606,23 @@ fn finish_backup (
let env: &BackupEnvironment = rpcenv.as_ref();
env.finish_backup()?;
env.log("sucessfully finished backup");
env.log("successfully finished backup");
Ok(Value::Null)
}
#[sortable]
pub const API_METHOD_DYNAMIC_CHUNK_INDEX: ApiMethod = ApiMethod::new(
&ApiHandler::AsyncHttp(&dynamic_chunk_index),
pub const API_METHOD_DOWNLOAD_PREVIOUS: ApiMethod = ApiMethod::new(
&ApiHandler::AsyncHttp(&download_previous),
&ObjectSchema::new(
r###"
Download the dynamic chunk index from the previous backup.
Simply returns an empty list if this is the first backup.
"### ,
"Download archive from previous backup.",
&sorted!([
("archive-name", false, &crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA)
]),
)
);
fn dynamic_chunk_index(
fn download_previous(
_parts: Parts,
_req_body: Body,
param: Value,
@ -579,130 +635,38 @@ fn dynamic_chunk_index(
let archive_name = tools::required_string_param(&param, "archive-name")?.to_owned();
if !archive_name.ends_with(".didx") {
bail!("wrong archive extension: '{}'", archive_name);
}
let empty_response = {
Response::builder()
.status(StatusCode::OK)
.body(Body::empty())?
};
let last_backup = match &env.last_backup {
Some(info) => info,
None => return Ok(empty_response),
None => bail!("no previous backup"),
};
let mut path = last_backup.backup_dir.relative_path();
let mut path = env.datastore.snapshot_path(&last_backup.backup_dir);
path.push(&archive_name);
let index = match env.datastore.open_dynamic_reader(path) {
Ok(index) => index,
Err(_) => {
env.log(format!("there is no last backup for archive '{}'", archive_name));
return Ok(empty_response);
{
let index: Option<Box<dyn IndexFile>> = match archive_type(&archive_name)? {
ArchiveType::FixedIndex => {
let index = env.datastore.open_fixed_reader(&path)?;
Some(Box::new(index))
}
ArchiveType::DynamicIndex => {
let index = env.datastore.open_dynamic_reader(&path)?;
Some(Box::new(index))
}
_ => { None }
};
if let Some(index) = index {
env.log(format!("register chunks in '{}' from previous backup.", archive_name));
for pos in 0..index.index_count() {
let info = index.chunk_info(pos).unwrap();
let size = info.range.end - info.range.start;
env.register_chunk(info.digest, size as u32)?;
}
}
};
env.log(format!("download last backup index for archive '{}'", archive_name));
let count = index.index_count();
for pos in 0..count {
let (start, end, digest) = index.chunk_info(pos)?;
let size = (end - start) as u32;
env.register_chunk(digest, size)?;
}
let reader = DigestListEncoder::new(Box::new(index));
let stream = WrappedReaderStream::new(reader);
// fixme: set size, content type?
let response = http::Response::builder()
.status(200)
.body(Body::wrap_stream(stream))?;
Ok(response)
}.boxed()
}
#[sortable]
pub const API_METHOD_FIXED_CHUNK_INDEX: ApiMethod = ApiMethod::new(
&ApiHandler::AsyncHttp(&fixed_chunk_index),
&ObjectSchema::new(
r###"
Download the fixed chunk index from the previous backup.
Simply returns an empty list if this is the first backup.
"### ,
&sorted!([
("archive-name", false, &crate::api2::types::BACKUP_ARCHIVE_NAME_SCHEMA)
]),
)
);
fn fixed_chunk_index(
_parts: Parts,
_req_body: Body,
param: Value,
_info: &ApiMethod,
rpcenv: Box<dyn RpcEnvironment>,
) -> ApiResponseFuture {
async move {
let env: &BackupEnvironment = rpcenv.as_ref();
let archive_name = tools::required_string_param(&param, "archive-name")?.to_owned();
if !archive_name.ends_with(".fidx") {
bail!("wrong archive extension: '{}'", archive_name);
}
let empty_response = {
Response::builder()
.status(StatusCode::OK)
.body(Body::empty())?
};
let last_backup = match &env.last_backup {
Some(info) => info,
None => return Ok(empty_response),
};
let mut path = last_backup.backup_dir.relative_path();
path.push(&archive_name);
let index = match env.datastore.open_fixed_reader(path) {
Ok(index) => index,
Err(_) => {
env.log(format!("there is no last backup for archive '{}'", archive_name));
return Ok(empty_response);
}
};
env.log(format!("download last backup index for archive '{}'", archive_name));
let count = index.index_count();
let image_size = index.index_bytes();
for pos in 0..count {
let digest = index.index_digest(pos).unwrap();
// Note: last chunk can be smaller
let start = (pos*index.chunk_size) as u64;
let mut end = start + index.chunk_size as u64;
if end > image_size { end = image_size; }
let size = (end - start) as u32;
env.register_chunk(*digest, size)?;
}
let reader = DigestListEncoder::new(Box::new(index));
let stream = WrappedReaderStream::new(reader);
// fixme: set size, content type?
let response = http::Response::builder()
.status(200)
.body(Body::wrap_stream(stream))?;
Ok(response)
env.log(format!("download '{}' from previous backup.", archive_name));
crate::api2::helpers::create_download_response(path).await
}.boxed()
}

View File

@ -1,8 +1,8 @@
use failure::*;
use anyhow::{bail, Error};
use std::sync::{Arc, Mutex};
use std::collections::HashMap;
use serde_json::Value;
use serde_json::{json, Value};
use proxmox::tools::digest_to_hex;
use proxmox::tools::fs::{replace_file, CreateOptions};
@ -47,12 +47,13 @@ struct FixedWriterState {
chunk_count: u64,
small_chunk_count: usize, // allow 0..1 small chunks (last chunk may be smaller)
upload_stat: UploadStatistic,
incremental: bool,
}
struct SharedBackupState {
finished: bool,
uid_counter: usize,
file_counter: usize, // sucessfully uploaded files
file_counter: usize, // successfully uploaded files
dynamic_writers: HashMap<usize, DynamicWriterState>,
fixed_writers: HashMap<usize, FixedWriterState>,
known_chunks: HashMap<[u8;32], u32>,
@ -80,7 +81,7 @@ impl SharedBackupState {
#[derive(Clone)]
pub struct BackupEnvironment {
env_type: RpcEnvironmentType,
result_attributes: HashMap<String, Value>,
result_attributes: Value,
user: String,
pub debug: bool,
pub formatter: &'static OutputFormatter,
@ -110,7 +111,7 @@ impl BackupEnvironment {
};
Self {
result_attributes: HashMap::new(),
result_attributes: json!({}),
env_type,
user,
worker,
@ -237,7 +238,7 @@ impl BackupEnvironment {
}
/// Store the writer with an unique ID
pub fn register_fixed_writer(&self, index: FixedIndexWriter, name: String, size: usize, chunk_size: u32) -> Result<usize, Error> {
pub fn register_fixed_writer(&self, index: FixedIndexWriter, name: String, size: usize, chunk_size: u32, incremental: bool) -> Result<usize, Error> {
let mut state = self.state.lock().unwrap();
state.ensure_unfinished()?;
@ -245,7 +246,7 @@ impl BackupEnvironment {
let uid = state.next_uid();
state.fixed_writers.insert(uid, FixedWriterState {
index, name, chunk_count: 0, size, chunk_size, small_chunk_count: 0, upload_stat: UploadStatistic::new(),
index, name, chunk_count: 0, size, chunk_size, small_chunk_count: 0, upload_stat: UploadStatistic::new(), incremental,
});
Ok(uid)
@ -310,7 +311,13 @@ impl BackupEnvironment {
self.log(format!("Upload size: {} ({}%)", upload_stat.size, (upload_stat.size*100)/size));
let client_side_duplicates = chunk_count - upload_stat.count;
// account for zero chunk, which might be uploaded but never used
let client_side_duplicates = if chunk_count < upload_stat.count {
0
} else {
chunk_count - upload_stat.count
};
let server_side_duplicates = upload_stat.duplicates;
if (client_side_duplicates + server_side_duplicates) > 0 {
@ -373,21 +380,22 @@ impl BackupEnvironment {
bail!("fixed writer '{}' close failed - received wrong number of chunk ({} != {})", data.name, data.chunk_count, chunk_count);
}
let expected_count = data.index.index_length();
if !data.incremental {
let expected_count = data.index.index_length();
if chunk_count != (expected_count as u64) {
bail!("fixed writer '{}' close failed - unexpected chunk count ({} != {})", data.name, expected_count, chunk_count);
}
if chunk_count != (expected_count as u64) {
bail!("fixed writer '{}' close failed - unexpected chunk count ({} != {})", data.name, expected_count, chunk_count);
}
if size != (data.size as u64) {
bail!("fixed writer '{}' close failed - unexpected file size ({} != {})", data.name, data.size, size);
if size != (data.size as u64) {
bail!("fixed writer '{}' close failed - unexpected file size ({} != {})", data.name, data.size, size);
}
}
let uuid = data.index.uuid;
let expected_csum = data.index.close()?;
println!("server checksum {:?} client: {:?}", expected_csum, csum);
println!("server checksum: {:?} client: {:?} (incremental: {})", expected_csum, csum, data.incremental);
if csum != expected_csum {
bail!("fixed writer '{}' close failed - got unexpected checksum", data.name);
}
@ -430,8 +438,6 @@ impl BackupEnvironment {
state.ensure_unfinished()?;
state.finished = true;
if state.dynamic_writers.len() != 0 {
bail!("found open index writer - unable to finish backup");
}
@ -440,6 +446,8 @@ impl BackupEnvironment {
bail!("backup does not contain valid files (file count == 0)");
}
state.finished = true;
Ok(())
}
@ -480,12 +488,12 @@ impl BackupEnvironment {
impl RpcEnvironment for BackupEnvironment {
fn set_result_attrib(&mut self, name: &str, value: Value) {
self.result_attributes.insert(name.into(), value);
fn result_attrib_mut(&mut self) -> &mut Value {
&mut self.result_attributes
}
fn get_result_attrib(&self, name: &str) -> Option<&Value> {
self.result_attributes.get(name)
fn result_attrib(&self) -> &Value {
&self.result_attributes
}
fn env_type(&self) -> RpcEnvironmentType {

View File

@ -2,7 +2,7 @@ use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use failure::*;
use anyhow::{bail, format_err, Error};
use futures::*;
use hyper::Body;
use hyper::http::request::Parts;

View File

@ -3,10 +3,12 @@ use proxmox::list_subdirs_api_method;
pub mod datastore;
pub mod remote;
pub mod sync;
const SUBDIRS: SubdirMap = &[
("datastore", &datastore::ROUTER),
("remote", &remote::ROUTER),
("sync", &sync::ROUTER),
];
pub const ROUTER: Router = Router::new()

View File

@ -1,13 +1,15 @@
use std::path::PathBuf;
use failure::*;
use anyhow::{bail, Error};
use serde_json::Value;
use ::serde::{Deserialize, Serialize};
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{api, Router, RpcEnvironment, Permission};
use crate::api2::types::*;
use crate::backup::*;
use crate::config::datastore;
use crate::config::datastore::{self, DataStoreConfig, DIR_NAME_SCHEMA};
use crate::config::acl::{PRIV_DATASTORE_AUDIT, PRIV_DATASTORE_MODIFY};
#[api(
input: {
@ -16,23 +18,32 @@ use crate::config::datastore;
returns: {
description: "List the configured datastores (with config digest).",
type: Array,
items: {
type: datastore::DataStoreConfig,
},
items: { type: datastore::DataStoreConfig },
},
access: {
permission: &Permission::Privilege(&["datastore"], PRIV_DATASTORE_AUDIT, false),
},
)]
/// List all datastores
pub fn list_datastores(
_param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<DataStoreConfig>, Error> {
let (config, digest) = datastore::config()?;
Ok(config.convert_to_array("name", Some(&digest), &[]))
let list = config.convert_to_typed_array("datastore")?;
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list)
}
// fixme: impl. const fn get_object_schema(datastore::DataStoreConfig::API_SCHEMA),
// but this need support for match inside const fn
// see: https://github.com/rust-lang/rust/issues/49146
#[api(
protected: true,
input: {
@ -40,18 +51,53 @@ pub fn list_datastores(
name: {
schema: DATASTORE_SCHEMA,
},
path: {
schema: DIR_NAME_SCHEMA,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
path: {
schema: datastore::DIR_NAME_SCHEMA,
"gc-schedule": {
optional: true,
schema: GC_SCHEDULE_SCHEMA,
},
"prune-schedule": {
optional: true,
schema: PRUNE_SCHEDULE_SCHEMA,
},
"keep-last": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_LAST,
},
"keep-hourly": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_HOURLY,
},
"keep-daily": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_DAILY,
},
"keep-weekly": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_WEEKLY,
},
"keep-monthly": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_MONTHLY,
},
"keep-yearly": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_YEARLY,
},
},
},
access: {
permission: &Permission::Privilege(&["datastore"], PRIV_DATASTORE_MODIFY, false),
},
)]
/// Create new datastore config.
pub fn create_datastore(name: String, param: Value) -> Result<(), Error> {
pub fn create_datastore(param: Value) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
@ -59,16 +105,16 @@ pub fn create_datastore(name: String, param: Value) -> Result<(), Error> {
let (mut config, _digest) = datastore::config()?;
if let Some(_) = config.sections.get(&name) {
bail!("datastore '{}' already exists.", name);
if let Some(_) = config.sections.get(&datastore.name) {
bail!("datastore '{}' already exists.", datastore.name);
}
let path: PathBuf = datastore.path.clone().into();
let backup_user = crate::backup::backup_user()?;
let _store = ChunkStore::create(&name, path, backup_user.uid, backup_user.gid)?;
let _store = ChunkStore::create(&datastore.name, path, backup_user.uid, backup_user.gid)?;
config.set_data(&name, "datastore", &datastore)?;
config.set_data(&datastore.name, "datastore", &datastore)?;
datastore::save_config(&config)?;
@ -87,14 +133,47 @@ pub fn create_datastore(name: String, param: Value) -> Result<(), Error> {
description: "The datastore configuration (with config digest).",
type: datastore::DataStoreConfig,
},
access: {
permission: &Permission::Privilege(&["datastore", "{name}"], PRIV_DATASTORE_AUDIT, false),
},
)]
/// Read a datastore configuration.
pub fn read_datastore(name: String) -> Result<Value, Error> {
pub fn read_datastore(
name: String,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<DataStoreConfig, Error> {
let (config, digest) = datastore::config()?;
let mut data = config.lookup_json("datastore", &name)?;
data.as_object_mut().unwrap()
.insert("digest".into(), proxmox::tools::digest_to_hex(&digest).into());
Ok(data)
let store_config = config.lookup("datastore", &name)?;
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(store_config)
}
#[api()]
#[derive(Serialize, Deserialize)]
#[serde(rename_all="kebab-case")]
#[allow(non_camel_case_types)]
/// Deletable property name
pub enum DeletableProperty {
/// Delete the comment property.
comment,
/// Delete the garbage collection schedule.
gc_schedule,
/// Delete the prune job schedule.
prune_schedule,
/// Delete the keep-last property
keep_last,
/// Delete the keep-hourly property
keep_hourly,
/// Delete the keep-daily property
keep_daily,
/// Delete the keep-weekly property
keep_weekly,
/// Delete the keep-monthly property
keep_monthly,
/// Delete the keep-yearly property
keep_yearly,
}
#[api(
@ -108,17 +187,69 @@ pub fn read_datastore(name: String) -> Result<Value, Error> {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
"gc-schedule": {
optional: true,
schema: GC_SCHEDULE_SCHEMA,
},
"prune-schedule": {
optional: true,
schema: PRUNE_SCHEDULE_SCHEMA,
},
"keep-last": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_LAST,
},
"keep-hourly": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_HOURLY,
},
"keep-daily": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_DAILY,
},
"keep-weekly": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_WEEKLY,
},
"keep-monthly": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_MONTHLY,
},
"keep-yearly": {
optional: true,
schema: PRUNE_SCHEMA_KEEP_YEARLY,
},
delete: {
description: "List of properties to delete.",
type: Array,
optional: true,
items: {
type: DeletableProperty,
}
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["datastore", "{name}"], PRIV_DATASTORE_MODIFY, false),
},
)]
/// Create new datastore config.
/// Update datastore config.
pub fn update_datastore(
name: String,
comment: Option<String>,
gc_schedule: Option<String>,
prune_schedule: Option<String>,
keep_last: Option<u64>,
keep_hourly: Option<u64>,
keep_daily: Option<u64>,
keep_weekly: Option<u64>,
keep_monthly: Option<u64>,
keep_yearly: Option<u64>,
delete: Option<Vec<DeletableProperty>>,
digest: Option<String>,
) -> Result<(), Error> {
@ -134,6 +265,22 @@ pub fn update_datastore(
let mut data: datastore::DataStoreConfig = config.lookup("datastore", &name)?;
if let Some(delete) = delete {
for delete_prop in delete {
match delete_prop {
DeletableProperty::comment => { data.comment = None; },
DeletableProperty::gc_schedule => { data.gc_schedule = None; },
DeletableProperty::prune_schedule => { data.prune_schedule = None; },
DeletableProperty::keep_last => { data.keep_last = None; },
DeletableProperty::keep_hourly => { data.keep_hourly = None; },
DeletableProperty::keep_daily => { data.keep_daily = None; },
DeletableProperty::keep_weekly => { data.keep_weekly = None; },
DeletableProperty::keep_monthly => { data.keep_monthly = None; },
DeletableProperty::keep_yearly => { data.keep_yearly = None; },
}
}
}
if let Some(comment) = comment {
let comment = comment.trim().to_string();
if comment.is_empty() {
@ -143,6 +290,16 @@ pub fn update_datastore(
}
}
if gc_schedule.is_some() { data.gc_schedule = gc_schedule; }
if prune_schedule.is_some() { data.prune_schedule = prune_schedule; }
if keep_last.is_some() { data.keep_last = keep_last; }
if keep_hourly.is_some() { data.keep_hourly = keep_hourly; }
if keep_daily.is_some() { data.keep_daily = keep_daily; }
if keep_weekly.is_some() { data.keep_weekly = keep_weekly; }
if keep_monthly.is_some() { data.keep_monthly = keep_monthly; }
if keep_yearly.is_some() { data.keep_yearly = keep_yearly; }
config.set_data(&name, "datastore", &data)?;
datastore::save_config(&config)?;
@ -157,16 +314,27 @@ pub fn update_datastore(
name: {
schema: DATASTORE_SCHEMA,
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["datastore", "{name}"], PRIV_DATASTORE_MODIFY, false),
},
)]
/// Remove a datastore configuration.
pub fn delete_datastore(name: String) -> Result<(), Error> {
pub fn delete_datastore(name: String, digest: Option<String>) -> Result<(), Error> {
// fixme: locking ?
// fixme: check digest ?
let _lock = crate::tools::open_file_locked(datastore::DATASTORE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut config, _digest) = datastore::config()?;
let (mut config, expected_digest) = datastore::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
match config.sections.get(&name) {
Some(_) => { config.sections.remove(&name); },

View File

@ -1,10 +1,13 @@
use failure::*;
use anyhow::{bail, Error};
use serde_json::Value;
use ::serde::{Deserialize, Serialize};
use base64;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
use crate::api2::types::*;
use crate::config::remote;
use crate::config::acl::{PRIV_REMOTE_AUDIT, PRIV_REMOTE_MODIFY};
#[api(
input: {
@ -14,42 +17,32 @@ use crate::config::remote;
description: "The list of configured remotes (with config digest).",
type: Array,
items: {
type: Object,
type: remote::Remote,
description: "Remote configuration (without password).",
properties: {
name: {
schema: REMOTE_ID_SCHEMA,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
host: {
schema: DNS_NAME_OR_IP_SCHEMA,
},
userid: {
schema: PROXMOX_USER_ID_SCHEMA,
},
fingerprint: {
optional: true,
schema: CERT_FINGERPRINT_SHA256_SCHEMA,
},
},
},
},
access: {
permission: &Permission::Privilege(&["remote"], PRIV_REMOTE_AUDIT, false),
},
)]
/// List all remotes
pub fn list_remotes(
_param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<remote::Remote>, Error> {
let (config, digest) = remote::config()?;
let value = config.convert_to_array("name", Some(&digest), &["password"]);
Ok(value.into())
let mut list: Vec<remote::Remote> = config.convert_to_typed_array("remote")?;
// don't return password in api
for remote in &mut list {
remote.password = "".to_string();
}
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list)
}
#[api(
@ -78,21 +71,26 @@ pub fn list_remotes(
},
},
},
access: {
permission: &Permission::Privilege(&["remote"], PRIV_REMOTE_MODIFY, false),
},
)]
/// Create new remote.
pub fn create_remote(name: String, param: Value) -> Result<(), Error> {
pub fn create_remote(password: String, param: Value) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let remote: remote::Remote = serde_json::from_value(param.clone())?;
let mut data = param.clone();
data["password"] = Value::from(base64::encode(password.as_bytes()));
let remote: remote::Remote = serde_json::from_value(data)?;
let (mut config, _digest) = remote::config()?;
if let Some(_) = config.sections.get(&name) {
bail!("remote '{}' already exists.", name);
if let Some(_) = config.sections.get(&remote.name) {
bail!("remote '{}' already exists.", remote.name);
}
config.set_data(&name, "remote", &remote)?;
config.set_data(&remote.name, "remote", &remote)?;
remote::save_config(&config)?;
@ -111,16 +109,34 @@ pub fn create_remote(name: String, param: Value) -> Result<(), Error> {
description: "The remote configuration (with config digest).",
type: remote::Remote,
},
access: {
permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_AUDIT, false),
}
)]
/// Read remote configuration data.
pub fn read_remote(name: String) -> Result<Value, Error> {
pub fn read_remote(
name: String,
_info: &ApiMethod,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<remote::Remote, Error> {
let (config, digest) = remote::config()?;
let mut data = config.lookup_json("remote", &name)?;
data.as_object_mut().unwrap()
.insert("digest".into(), proxmox::tools::digest_to_hex(&digest).into());
let mut data: remote::Remote = config.lookup("remote", &name)?;
data.password = "".to_string(); // do not return password in api
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(data)
}
#[api()]
#[derive(Serialize, Deserialize)]
#[allow(non_camel_case_types)]
/// Deletable property name
pub enum DeletableProperty {
/// Delete the comment property.
comment,
/// Delete the fingerprint property.
fingerprint,
}
#[api(
protected: true,
input: {
@ -148,12 +164,23 @@ pub fn read_remote(name: String) -> Result<Value, Error> {
optional: true,
schema: CERT_FINGERPRINT_SHA256_SCHEMA,
},
delete: {
description: "List of properties to delete.",
type: Array,
optional: true,
items: {
type: DeletableProperty,
}
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false),
},
)]
/// Update remote configuration.
pub fn update_remote(
@ -163,6 +190,7 @@ pub fn update_remote(
userid: Option<String>,
password: Option<String>,
fingerprint: Option<String>,
delete: Option<Vec<DeletableProperty>>,
digest: Option<String>,
) -> Result<(), Error> {
@ -177,6 +205,15 @@ pub fn update_remote(
let mut data: remote::Remote = config.lookup("remote", &name)?;
if let Some(delete) = delete {
for delete_prop in delete {
match delete_prop {
DeletableProperty::comment => { data.comment = None; },
DeletableProperty::fingerprint => { data.fingerprint = None; },
}
}
}
if let Some(comment) = comment {
let comment = comment.trim().to_string();
if comment.is_empty() {
@ -189,7 +226,6 @@ pub fn update_remote(
if let Some(userid) = userid { data.userid = userid; }
if let Some(password) = password { data.password = password; }
// fixme: howto delete a fingeprint?
if let Some(fingerprint) = fingerprint { data.fingerprint = Some(fingerprint); }
config.set_data(&name, "remote", &data)?;
@ -206,22 +242,35 @@ pub fn update_remote(
name: {
schema: REMOTE_ID_SCHEMA,
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["remote", "{name}"], PRIV_REMOTE_MODIFY, false),
},
)]
/// Remove a remote from the configuration file.
pub fn delete_remote(name: String) -> Result<(), Error> {
pub fn delete_remote(name: String, digest: Option<String>) -> Result<(), Error> {
// fixme: locking ?
// fixme: check digest ?
let _lock = crate::tools::open_file_locked(remote::REMOTE_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut config, _digest) = remote::config()?;
let (mut config, expected_digest) = remote::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
match config.sections.get(&name) {
Some(_) => { config.sections.remove(&name); },
None => bail!("remote '{}' does not exist.", name),
}
remote::save_config(&config)?;
Ok(())
}

277
src/api2/config/sync.rs Normal file
View File

@ -0,0 +1,277 @@
use anyhow::{bail, Error};
use serde_json::Value;
use ::serde::{Deserialize, Serialize};
use proxmox::api::{api, Router, RpcEnvironment};
use crate::api2::types::*;
use crate::config::sync::{self, SyncJobConfig};
// fixme: add access permissions
#[api(
input: {
properties: {},
},
returns: {
description: "List configured jobs.",
type: Array,
items: { type: sync::SyncJobConfig },
},
)]
/// List all sync jobs
pub fn list_sync_jobs(
_param: Value,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<SyncJobConfig>, Error> {
let (config, digest) = sync::config()?;
let list = config.convert_to_typed_array("sync")?;
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(list)
}
#[api(
protected: true,
input: {
properties: {
id: {
schema: JOB_ID_SCHEMA,
},
store: {
schema: DATASTORE_SCHEMA,
},
remote: {
schema: REMOTE_ID_SCHEMA,
},
"remote-store": {
schema: DATASTORE_SCHEMA,
},
"remove-vanished": {
schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
optional: true,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
schedule: {
optional: true,
schema: SYNC_SCHEDULE_SCHEMA,
},
},
},
)]
/// Create a new sync job.
pub fn create_sync_job(param: Value) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let sync_job: sync::SyncJobConfig = serde_json::from_value(param.clone())?;
let (mut config, _digest) = sync::config()?;
if let Some(_) = config.sections.get(&sync_job.id) {
bail!("job '{}' already exists.", sync_job.id);
}
config.set_data(&sync_job.id, "sync", &sync_job)?;
sync::save_config(&config)?;
Ok(())
}
#[api(
input: {
properties: {
id: {
schema: JOB_ID_SCHEMA,
},
},
},
returns: {
description: "The sync job configuration.",
type: sync::SyncJobConfig,
},
)]
/// Read a sync job configuration.
pub fn read_sync_job(
id: String,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<SyncJobConfig, Error> {
let (config, digest) = sync::config()?;
let sync_job = config.lookup("sync", &id)?;
rpcenv["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(sync_job)
}
#[api()]
#[derive(Serialize, Deserialize)]
#[serde(rename_all="kebab-case")]
#[allow(non_camel_case_types)]
/// Deletable property name
pub enum DeletableProperty {
/// Delete the comment property.
comment,
/// Delete the job schedule.
schedule,
/// Delete the remove-vanished flag.
remove_vanished,
}
#[api(
protected: true,
input: {
properties: {
id: {
schema: JOB_ID_SCHEMA,
},
store: {
schema: DATASTORE_SCHEMA,
optional: true,
},
remote: {
schema: REMOTE_ID_SCHEMA,
optional: true,
},
"remote-store": {
schema: DATASTORE_SCHEMA,
optional: true,
},
"remove-vanished": {
schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
optional: true,
},
comment: {
optional: true,
schema: SINGLE_LINE_COMMENT_SCHEMA,
},
schedule: {
optional: true,
schema: SYNC_SCHEDULE_SCHEMA,
},
delete: {
description: "List of properties to delete.",
type: Array,
optional: true,
items: {
type: DeletableProperty,
}
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
)]
/// Update sync job config.
pub fn update_sync_job(
id: String,
store: Option<String>,
remote: Option<String>,
remote_store: Option<String>,
remove_vanished: Option<bool>,
comment: Option<String>,
schedule: Option<String>,
delete: Option<Vec<DeletableProperty>>,
digest: Option<String>,
) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
// pass/compare digest
let (mut config, expected_digest) = sync::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
let mut data: sync::SyncJobConfig = config.lookup("sync", &id)?;
if let Some(delete) = delete {
for delete_prop in delete {
match delete_prop {
DeletableProperty::comment => { data.comment = None; },
DeletableProperty::schedule => { data.schedule = None; },
DeletableProperty::remove_vanished => { data.remove_vanished = None; },
}
}
}
if let Some(comment) = comment {
let comment = comment.trim().to_string();
if comment.is_empty() {
data.comment = None;
} else {
data.comment = Some(comment);
}
}
if let Some(store) = store { data.store = store; }
if let Some(remote) = remote { data.remote = remote; }
if let Some(remote_store) = remote_store { data.remote_store = remote_store; }
if schedule.is_some() { data.schedule = schedule; }
if remove_vanished.is_some() { data.remove_vanished = remove_vanished; }
config.set_data(&id, "sync", &data)?;
sync::save_config(&config)?;
Ok(())
}
#[api(
protected: true,
input: {
properties: {
id: {
schema: JOB_ID_SCHEMA,
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
)]
/// Remove a sync job configuration
pub fn delete_sync_job(id: String, digest: Option<String>) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(sync::SYNC_CFG_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut config, expected_digest) = sync::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
match config.sections.get(&id) {
Some(_) => { config.sections.remove(&id); },
None => bail!("job '{}' does not exist.", id),
}
sync::save_config(&config)?;
Ok(())
}
const ITEM_ROUTER: Router = Router::new()
.get(&API_METHOD_READ_SYNC_JOB)
.put(&API_METHOD_UPDATE_SYNC_JOB)
.delete(&API_METHOD_DELETE_SYNC_JOB);
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_LIST_SYNC_JOBS)
.post(&API_METHOD_CREATE_SYNC_JOB)
.match_all("id", &ITEM_ROUTER);

23
src/api2/helpers.rs Normal file
View File

@ -0,0 +1,23 @@
use std::path::PathBuf;
use anyhow::Error;
use futures::*;
use hyper::{Body, Response, StatusCode, header};
use proxmox::http_err;
pub async fn create_download_response(path: PathBuf) -> Result<Response<Body>, Error> {
let file = tokio::fs::File::open(path.clone())
.map_err(move |err| http_err!(BAD_REQUEST, format!("open file {:?} failed: {}", path.clone(), err)))
.await?;
let payload = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new())
.map_ok(|bytes| hyper::body::Bytes::from(bytes.freeze()));
let body = Body::wrap_stream(payload);
// fixme: set other headers ?
Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/octet-stream")
.body(body)
.unwrap())
}

View File

@ -3,17 +3,21 @@ use proxmox::list_subdirs_api_method;
pub mod tasks;
mod time;
mod network;
pub mod network;
pub mod dns;
mod syslog;
mod journal;
mod services;
mod status;
pub(crate) mod rrd;
pub mod disks;
pub const SUBDIRS: SubdirMap = &[
("disks", &disks::ROUTER),
("dns", &dns::ROUTER),
("journal", &journal::ROUTER),
("network", &network::ROUTER),
("rrd", &rrd::ROUTER),
("services", &services::ROUTER),
("status", &status::ROUTER),
("syslog", &syslog::ROUTER),

188
src/api2/node/disks.rs Normal file
View File

@ -0,0 +1,188 @@
use anyhow::{bail, Error};
use serde_json::{json, Value};
use proxmox::api::{api, Permission, RpcEnvironment, RpcEnvironmentType};
use proxmox::api::router::{Router, SubdirMap};
use proxmox::{sortable, identity};
use proxmox::{list_subdirs_api_method};
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
use crate::tools::disks::{
DiskUsageInfo, DiskUsageType, DiskManage, SmartData,
get_disks, get_smart_data, get_disk_usage_info, inititialize_gpt_disk,
};
use crate::server::WorkerTask;
use crate::api2::types::{UPID_SCHEMA, NODE_SCHEMA, BLOCKDEVICE_NAME_SCHEMA};
pub mod directory;
pub mod zfs;
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
skipsmart: {
description: "Skip smart checks.",
type: bool,
optional: true,
default: false,
},
"usage-type": {
type: DiskUsageType,
optional: true,
},
},
},
returns: {
description: "Local disk list.",
type: Array,
items: {
type: DiskUsageInfo,
},
},
access: {
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_AUDIT, false),
},
)]
/// List local disks
pub fn list_disks(
skipsmart: bool,
usage_type: Option<DiskUsageType>,
) -> Result<Vec<DiskUsageInfo>, Error> {
let mut list = Vec::new();
for (_, info) in get_disks(None, skipsmart)? {
if let Some(ref usage_type) = usage_type {
if info.used == *usage_type {
list.push(info);
}
} else {
list.push(info);
}
}
Ok(list)
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
disk: {
schema: BLOCKDEVICE_NAME_SCHEMA,
},
healthonly: {
description: "If true returns only the health status.",
type: bool,
optional: true,
},
},
},
returns: {
type: SmartData,
},
access: {
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_AUDIT, false),
},
)]
/// Get SMART attributes and health of a disk.
pub fn smart_status(
disk: String,
healthonly: Option<bool>,
) -> Result<SmartData, Error> {
let healthonly = healthonly.unwrap_or(false);
let manager = DiskManage::new();
let disk = manager.disk_by_name(&disk)?;
get_smart_data(&disk, healthonly)
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
disk: {
schema: BLOCKDEVICE_NAME_SCHEMA,
},
uuid: {
description: "UUID for the GPT table.",
type: String,
optional: true,
max_length: 36,
},
},
},
returns: {
schema: UPID_SCHEMA,
},
access: {
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_MODIFY, false),
},
)]
/// Initialize empty Disk with GPT
pub fn initialize_disk(
disk: String,
uuid: Option<String>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
let username = rpcenv.get_user().unwrap();
let info = get_disk_usage_info(&disk, true)?;
if info.used != DiskUsageType::Unused {
bail!("disk '{}' is already in use.", disk);
}
let upid_str = WorkerTask::new_thread(
"diskinit", Some(disk.clone()), &username.clone(), to_stdout, move |worker|
{
worker.log(format!("initialize disk {}", disk));
let disk_manager = DiskManage::new();
let disk_info = disk_manager.disk_by_name(&disk)?;
inititialize_gpt_disk(&disk_info, uuid.as_deref())?;
Ok(())
})?;
Ok(json!(upid_str))
}
#[sortable]
const SUBDIRS: SubdirMap = &sorted!([
// ("lvm", &lvm::ROUTER),
("directory", &directory::ROUTER),
("zfs", &zfs::ROUTER),
(
"initgpt", &Router::new()
.post(&API_METHOD_INITIALIZE_DISK)
),
(
"list", &Router::new()
.get(&API_METHOD_LIST_DISKS)
),
(
"smart", &Router::new()
.get(&API_METHOD_SMART_STATUS)
),
]);
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);

View File

@ -0,0 +1,221 @@
use anyhow::{bail, Error};
use serde_json::json;
use ::serde::{Deserialize, Serialize};
use proxmox::api::{api, Permission, RpcEnvironment, RpcEnvironmentType};
use proxmox::api::section_config::SectionConfigData;
use proxmox::api::router::Router;
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
use crate::tools::disks::{
DiskManage, FileSystemType, DiskUsageType,
create_file_system, create_single_linux_partition, get_fs_uuid, get_disk_usage_info,
};
use crate::tools::systemd::{self, types::*};
use crate::server::WorkerTask;
use crate::api2::types::*;
#[api(
properties: {
"filesystem": {
type: FileSystemType,
optional: true,
},
},
)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all="kebab-case")]
/// Datastore mount info.
pub struct DatastoreMountInfo {
/// The path of the mount unit.
pub unitfile: String,
/// The mount path.
pub path: String,
/// The mounted device.
pub device: String,
/// File system type
pub filesystem: Option<String>,
/// Mount options
pub options: Option<String>,
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
}
},
returns: {
description: "List of systemd datastore mount units.",
type: Array,
items: {
type: DatastoreMountInfo,
},
},
access: {
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_AUDIT, false),
},
)]
/// List systemd datastore mount units.
pub fn list_datastore_mounts() -> Result<Vec<DatastoreMountInfo>, Error> {
lazy_static::lazy_static! {
static ref MOUNT_NAME_REGEX: regex::Regex = regex::Regex::new(r"^mnt-datastore-(.+)\.mount$").unwrap();
}
let mut list = Vec::new();
let basedir = "/etc/systemd/system";
for item in crate::tools::fs::scan_subdir(libc::AT_FDCWD, basedir, &MOUNT_NAME_REGEX)? {
let item = item?;
let name = item.file_name().to_string_lossy().to_string();
let unitfile = format!("{}/{}", basedir, name);
let config = systemd::config::parse_systemd_mount(&unitfile)?;
let data: SystemdMountSection = config.lookup("Mount", "Mount")?;
list.push(DatastoreMountInfo {
unitfile,
device: data.What,
path: data.Where,
filesystem: data.Type,
options: data.Options,
});
}
Ok(list)
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
name: {
schema: DATASTORE_SCHEMA,
},
disk: {
schema: BLOCKDEVICE_NAME_SCHEMA,
},
"add-datastore": {
description: "Configure a datastore using the directory.",
type: bool,
optional: true,
},
filesystem: {
type: FileSystemType,
optional: true,
},
}
},
returns: {
schema: UPID_SCHEMA,
},
access: {
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_MODIFY, false),
},
)]
/// Create a Filesystem on an unused disk. Will be mounted under '/mnt/datastore/<name>'.".
pub fn create_datastore_disk(
name: String,
disk: String,
add_datastore: Option<bool>,
filesystem: Option<FileSystemType>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> {
let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
let username = rpcenv.get_user().unwrap();
let info = get_disk_usage_info(&disk, true)?;
if info.used != DiskUsageType::Unused {
bail!("disk '{}' is already in use.", disk);
}
let upid_str = WorkerTask::new_thread(
"dircreate", Some(name.clone()), &username.clone(), to_stdout, move |worker|
{
worker.log(format!("create datastore '{}' on disk {}", name, disk));
let add_datastore = add_datastore.unwrap_or(false);
let filesystem = filesystem.unwrap_or(FileSystemType::Ext4);
let manager = DiskManage::new();
let disk = manager.clone().disk_by_name(&disk)?;
let partition = create_single_linux_partition(&disk)?;
create_file_system(&partition, filesystem)?;
let uuid = get_fs_uuid(&partition)?;
let uuid_path = format!("/dev/disk/by-uuid/{}", uuid);
let (mount_unit_name, mount_point) = create_datastore_mount_unit(&name, filesystem, &uuid_path)?;
systemd::reload_daemon()?;
systemd::enable_unit(&mount_unit_name)?;
systemd::start_unit(&mount_unit_name)?;
if add_datastore {
crate::api2::config::datastore::create_datastore(json!({ "name": name, "path": mount_point }))?
}
Ok(())
})?;
Ok(upid_str)
}
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_LIST_DATASTORE_MOUNTS)
.post(&API_METHOD_CREATE_DATASTORE_DISK);
fn create_datastore_mount_unit(
datastore_name: &str,
fs_type: FileSystemType,
what: &str,
) -> Result<(String, String), Error> {
let mount_point = format!("/mnt/datastore/{}", datastore_name);
let mut mount_unit_name = systemd::escape_unit(&mount_point, true);
mount_unit_name.push_str(".mount");
let mount_unit_path = format!("/etc/systemd/system/{}", mount_unit_name);
let unit = SystemdUnitSection {
Description: format!("Mount datatstore '{}' under '{}'", datastore_name, mount_point),
..Default::default()
};
let install = SystemdInstallSection {
WantedBy: Some(vec!["multi-user.target".to_string()]),
..Default::default()
};
let mount = SystemdMountSection {
What: what.to_string(),
Where: mount_point.clone(),
Type: Some(fs_type.to_string()),
Options: Some(String::from("defaults")),
..Default::default()
};
let mut config = SectionConfigData::new();
config.set_data("Unit", "Unit", unit)?;
config.set_data("Install", "Install", install)?;
config.set_data("Mount", "Mount", mount)?;
systemd::config::save_systemd_mount(&mount_unit_path, &config)?;
Ok((mount_unit_name, mount_point))
}

380
src/api2/node/disks/zfs.rs Normal file
View File

@ -0,0 +1,380 @@
use anyhow::{bail, Error};
use serde_json::{json, Value};
use ::serde::{Deserialize, Serialize};
use proxmox::api::{
api, Permission, RpcEnvironment, RpcEnvironmentType,
schema::{
Schema,
StringSchema,
ArraySchema,
IntegerSchema,
ApiStringFormat,
parse_property_string,
},
};
use proxmox::api::router::Router;
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
use crate::tools::disks::{
zpool_list, zpool_status, parse_zpool_status_config_tree, vdev_list_to_tree,
DiskUsageType,
};
use crate::server::WorkerTask;
use crate::api2::types::*;
pub const DISK_ARRAY_SCHEMA: Schema = ArraySchema::new(
"Disk name list.", &BLOCKDEVICE_NAME_SCHEMA)
.schema();
pub const DISK_LIST_SCHEMA: Schema = StringSchema::new(
"A list of disk names, comma separated.")
.format(&ApiStringFormat::PropertyString(&DISK_ARRAY_SCHEMA))
.schema();
pub const ZFS_ASHIFT_SCHEMA: Schema = IntegerSchema::new(
"Pool sector size exponent.")
.minimum(9)
.maximum(16)
.default(12)
.schema();
#[api(
default: "On",
)]
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// The ZFS compression algorithm to use.
pub enum ZfsCompressionType {
/// Gnu Zip
Gzip,
/// LZ4
Lz4,
/// LZJB
Lzjb,
/// ZLE
Zle,
/// Enable compression using the default algorithm.
On,
/// Disable compression.
Off,
}
#[api()]
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// The ZFS RAID level to use.
pub enum ZfsRaidLevel {
/// Single Disk
Single,
/// Mirror
Mirror,
/// Raid10
Raid10,
/// RaidZ
RaidZ,
/// RaidZ2
RaidZ2,
/// RaidZ3
RaidZ3,
}
#[api()]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all="kebab-case")]
/// zpool list item
pub struct ZpoolListItem {
/// zpool name
pub name: String,
/// Health
pub health: String,
/// Total size
pub size: u64,
/// Used size
pub alloc: u64,
/// Free space
pub free: u64,
/// ZFS fragnentation level
pub frag: u64,
/// ZFS deduplication ratio
pub dedup: f64,
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
},
},
returns: {
description: "List of zpools.",
type: Array,
items: {
type: ZpoolListItem,
},
},
access: {
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_AUDIT, false),
},
)]
/// List zfs pools.
pub fn list_zpools() -> Result<Vec<ZpoolListItem>, Error> {
let data = zpool_list(None, false)?;
let mut list = Vec::new();
for item in data {
if let Some(usage) = item.usage {
list.push(ZpoolListItem {
name: item.name,
health: item.health,
size: usage.size,
alloc: usage.alloc,
free: usage.free,
frag: usage.frag,
dedup: usage.dedup,
});
}
}
Ok(list)
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
name: {
schema: DATASTORE_SCHEMA,
},
},
},
returns: {
description: "zpool vdev tree with status",
properties: {
},
},
access: {
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_AUDIT, false),
},
)]
/// Get zpool status details.
pub fn zpool_details(
name: String,
) -> Result<Value, Error> {
let key_value_list = zpool_status(&name)?;
let config = match key_value_list.iter().find(|(k, _)| k == "config") {
Some((_, v)) => v,
None => bail!("got zpool status without config key"),
};
let vdev_list = parse_zpool_status_config_tree(config)?;
let mut tree = vdev_list_to_tree(&vdev_list)?;
for (k, v) in key_value_list {
if k != "config" {
tree[k] = v.into();
}
}
tree["name"] = tree.as_object_mut().unwrap()
.remove("pool")
.unwrap_or_else(|| name.into());
Ok(tree)
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
name: {
schema: DATASTORE_SCHEMA,
},
devices: {
schema: DISK_LIST_SCHEMA,
},
raidlevel: {
type: ZfsRaidLevel,
},
ashift: {
schema: ZFS_ASHIFT_SCHEMA,
optional: true,
},
compression: {
type: ZfsCompressionType,
optional: true,
},
"add-datastore": {
description: "Configure a datastore using the zpool.",
type: bool,
optional: true,
},
},
},
returns: {
schema: UPID_SCHEMA,
},
access: {
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_MODIFY, false),
},
)]
/// Create a new ZFS pool.
pub fn create_zpool(
name: String,
devices: String,
raidlevel: ZfsRaidLevel,
compression: Option<String>,
ashift: Option<usize>,
add_datastore: Option<bool>,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> {
let to_stdout = if rpcenv.env_type() == RpcEnvironmentType::CLI { true } else { false };
let username = rpcenv.get_user().unwrap();
let add_datastore = add_datastore.unwrap_or(false);
let ashift = ashift.unwrap_or(12);
let devices_text = devices.clone();
let devices = parse_property_string(&devices, &DISK_ARRAY_SCHEMA)?;
let devices: Vec<String> = devices.as_array().unwrap().iter()
.map(|v| v.as_str().unwrap().to_string()).collect();
let disk_map = crate::tools::disks::get_disks(None, true)?;
for disk in devices.iter() {
match disk_map.get(disk) {
Some(info) => {
if info.used != DiskUsageType::Unused {
bail!("disk '{}' is already in use.", disk);
}
}
None => {
bail!("no such disk '{}'", disk);
}
}
}
let min_disks = match raidlevel {
ZfsRaidLevel::Single => 1,
ZfsRaidLevel::Mirror => 2,
ZfsRaidLevel::Raid10 => 4,
ZfsRaidLevel::RaidZ => 3,
ZfsRaidLevel::RaidZ2 => 4,
ZfsRaidLevel::RaidZ3 => 5,
};
// Sanity checks
if raidlevel == ZfsRaidLevel::Raid10 && devices.len() % 2 != 0 {
bail!("Raid10 needs an even number of disks.");
}
if raidlevel == ZfsRaidLevel::Single && devices.len() > 1 {
bail!("Please give only one disk for single disk mode.");
}
if devices.len() < min_disks {
bail!("{:?} needs at least {} disks.", raidlevel, min_disks);
}
// check if the default path does exist already and bail if it does
// otherwise we get an error on mounting
let mut default_path = std::path::PathBuf::from("/");
default_path.push(&name);
match std::fs::metadata(&default_path) {
Err(_) => {}, // path does not exist
Ok(_) => {
bail!("path {:?} already exists", default_path);
}
}
let upid_str = WorkerTask::new_thread(
"zfscreate", Some(name.clone()), &username.clone(), to_stdout, move |worker|
{
worker.log(format!("create {:?} zpool '{}' on devices '{}'", raidlevel, name, devices_text));
let mut command = std::process::Command::new("zpool");
command.args(&["create", "-o", &format!("ashift={}", ashift), &name]);
match raidlevel {
ZfsRaidLevel::Single => {
command.arg(&devices[0]);
}
ZfsRaidLevel::Mirror => {
command.arg("mirror");
command.args(devices);
}
ZfsRaidLevel::Raid10 => {
devices.chunks(2).for_each(|pair| {
command.arg("mirror");
command.args(pair);
});
}
ZfsRaidLevel::RaidZ => {
command.arg("raidz");
command.args(devices);
}
ZfsRaidLevel::RaidZ2 => {
command.arg("raidz2");
command.args(devices);
}
ZfsRaidLevel::RaidZ3 => {
command.arg("raidz3");
command.args(devices);
}
}
worker.log(format!("# {:?}", command));
let output = crate::tools::run_command(command, None)?;
worker.log(output);
if let Some(compression) = compression {
let mut command = std::process::Command::new("zfs");
command.args(&["set", &format!("compression={}", compression), &name]);
worker.log(format!("# {:?}", command));
let output = crate::tools::run_command(command, None)?;
worker.log(output);
}
if add_datastore {
let mount_point = format!("/{}", name);
crate::api2::config::datastore::create_datastore(json!({ "name": name, "path": mount_point }))?
}
Ok(())
})?;
Ok(upid_str)
}
pub const POOL_ROUTER: Router = Router::new()
.get(&API_METHOD_ZPOOL_DETAILS);
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_LIST_ZPOOLS)
.post(&API_METHOD_CREATE_ZPOOL)
.match_all("name", &POOL_ROUTER);

View File

@ -1,21 +1,34 @@
use std::sync::{Arc, Mutex};
use failure::*;
use anyhow::{Error};
use lazy_static::lazy_static;
use openssl::sha;
use regex::Regex;
use serde_json::{json, Value};
use ::serde::{Deserialize, Serialize};
use proxmox::{sortable, identity};
use proxmox::api::{ApiHandler, ApiMethod, Router, RpcEnvironment};
use proxmox::api::schema::*;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
use proxmox::{IPRE, IPV4RE, IPV6RE, IPV4OCTET, IPV6H16, IPV6LS32};
use crate::api2::types::*;
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
static RESOLV_CONF_FN: &str = "/etc/resolv.conf";
#[api()]
#[derive(Serialize, Deserialize)]
#[allow(non_camel_case_types)]
/// Deletable property name
pub enum DeletableProperty {
/// Delete first nameserver entry
dns1,
/// Delete second nameserver entry
dns2,
/// Delete third nameserver entry
dns3,
}
pub fn read_etc_resolv_conf() -> Result<Value, Error> {
let mut result = json!({});
@ -34,6 +47,8 @@ pub fn read_etc_resolv_conf() -> Result<Value, Error> {
concat!(r"^\s*nameserver\s+(", IPRE!(), r")\s*")).unwrap();
}
let mut options = String::new();
for line in data.lines() {
if let Some(caps) = DOMAIN_REGEX.captures(&line) {
@ -44,16 +59,69 @@ pub fn read_etc_resolv_conf() -> Result<Value, Error> {
let nameserver = &caps[1];
let id = format!("dns{}", nscount);
result[id] = Value::from(nameserver);
} else {
if !options.is_empty() { options.push('\n'); }
options.push_str(line);
}
}
if !options.is_empty() {
result["options"] = options.into();
}
Ok(result)
}
fn update_dns(
param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
#[api(
protected: true,
input: {
description: "Update DNS settings.",
properties: {
node: {
schema: NODE_SCHEMA,
},
search: {
schema: SEARCH_DOMAIN_SCHEMA,
optional: true,
},
dns1: {
optional: true,
schema: FIRST_DNS_SERVER_SCHEMA,
},
dns2: {
optional: true,
schema: SECOND_DNS_SERVER_SCHEMA,
},
dns3: {
optional: true,
schema: THIRD_DNS_SERVER_SCHEMA,
},
delete: {
description: "List of properties to delete.",
type: Array,
optional: true,
items: {
type: DeletableProperty,
}
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "network", "dns"], PRIV_SYS_MODIFY, false),
}
)]
/// Update DNS settings
pub fn update_dns(
search: Option<String>,
dns1: Option<String>,
dns2: Option<String>,
dns3: Option<String>,
delete: Option<Vec<DeletableProperty>>,
digest: Option<String>,
) -> Result<Value, Error> {
lazy_static! {
@ -62,33 +130,41 @@ fn update_dns(
let _guard = MUTEX.lock();
let search = crate::tools::required_string_param(&param, "search")?;
let mut config = read_etc_resolv_conf()?;
let old_digest = config["digest"].as_str().unwrap();
let raw = file_get_contents(RESOLV_CONF_FN)?;
let old_digest = proxmox::tools::digest_to_hex(&sha::sha256(&raw));
if let Some(digest) = param["digest"].as_str() {
crate::tools::assert_if_modified(&old_digest, &digest)?;
if let Some(digest) = digest {
crate::tools::assert_if_modified(old_digest, &digest)?;
}
let old_data = String::from_utf8(raw)?;
let mut data = format!("search {}\n", search);
for opt in &["dns1", "dns2", "dns3"] {
if let Some(server) = param[opt].as_str() {
data.push_str(&format!("nameserver {}\n", server));
if let Some(delete) = delete {
for delete_prop in delete {
let config = config.as_object_mut().unwrap();
match delete_prop {
DeletableProperty::dns1 => { config.remove("dns1"); },
DeletableProperty::dns2 => { config.remove("dns2"); },
DeletableProperty::dns3 => { config.remove("dns3"); },
}
}
}
// append other data
lazy_static! {
static ref SKIP_REGEX: Regex = Regex::new(r"^(search|domain|nameserver)\s+").unwrap();
if let Some(search) = search { config["search"] = search.into(); }
if let Some(dns1) = dns1 { config["dns1"] = dns1.into(); }
if let Some(dns2) = dns2 { config["dns2"] = dns2.into(); }
if let Some(dns3) = dns3 { config["dns3"] = dns3.into(); }
let mut data = String::new();
if let Some(search) = config["search"].as_str() {
data.push_str(&format!("search {}\n", search));
}
for line in old_data.lines() {
if SKIP_REGEX.is_match(line) { continue; }
data.push_str(line);
data.push('\n');
for opt in &["dns1", "dns2", "dns3"] {
if let Some(server) = config[opt].as_str() {
data.push_str(&format!("nameserver {}\n", server));
}
}
if let Some(options) = config["options"].as_str() {
data.push_str(options);
}
replace_file(RESOLV_CONF_FN, data.as_bytes(), CreateOptions::new())?;
@ -96,7 +172,45 @@ fn update_dns(
Ok(Value::Null)
}
fn get_dns(
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
},
},
returns: {
description: "Returns DNS server IPs and sreach domain.",
type: Object,
properties: {
digest: {
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
search: {
optional: true,
schema: SEARCH_DOMAIN_SCHEMA,
},
dns1: {
optional: true,
schema: FIRST_DNS_SERVER_SCHEMA,
},
dns2: {
optional: true,
schema: SECOND_DNS_SERVER_SCHEMA,
},
dns3: {
optional: true,
schema: THIRD_DNS_SERVER_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "network", "dns"], PRIV_SYS_AUDIT, false),
}
)]
/// Read DNS settings.
pub fn get_dns(
_param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
@ -105,41 +219,6 @@ fn get_dns(
read_etc_resolv_conf()
}
#[sortable]
pub const ROUTER: Router = Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&get_dns),
&ObjectSchema::new(
"Read DNS settings.",
&sorted!([ ("node", false, &NODE_SCHEMA) ]),
)
).returns(
&ObjectSchema::new(
"Returns DNS server IPs and sreach domain.",
&sorted!([
("digest", false, &PROXMOX_CONFIG_DIGEST_SCHEMA),
("search", true, &SEARCH_DOMAIN_SCHEMA),
("dns1", true, &FIRST_DNS_SERVER_SCHEMA),
("dns2", true, &SECOND_DNS_SERVER_SCHEMA),
("dns3", true, &THIRD_DNS_SERVER_SCHEMA),
]),
).schema()
)
)
.put(
&ApiMethod::new(
&ApiHandler::Sync(&update_dns),
&ObjectSchema::new(
"Returns DNS server IPs and sreach domain.",
&sorted!([
("node", false, &NODE_SCHEMA),
("search", false, &SEARCH_DOMAIN_SCHEMA),
("dns1", true, &FIRST_DNS_SERVER_SCHEMA),
("dns2", true, &SECOND_DNS_SERVER_SCHEMA),
("dns3", true, &THIRD_DNS_SERVER_SCHEMA),
("digest", true, &PROXMOX_CONFIG_DIGEST_SCHEMA),
]),
)
).protected(true)
);
.get(&API_METHOD_GET_DNS)
.put(&API_METHOD_UPDATE_DNS);

View File

@ -1,12 +1,13 @@
use std::process::{Command, Stdio};
use failure::*;
use anyhow::{Error};
use serde_json::{json, Value};
use std::io::{BufRead,BufReader};
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
use crate::api2::types::*;
use crate::config::acl::PRIV_SYS_AUDIT;
#[api(
protected: true,
@ -53,6 +54,9 @@ use crate::api2::types::*;
description: "Line text.",
},
},
access: {
permission: &Permission::Privilege(&["system", "log"], PRIV_SYS_AUDIT, false),
},
)]
/// Read syslog entries.
fn get_journal(
@ -90,7 +94,7 @@ fn get_journal(
let mut lines: Vec<String> = vec![];
let mut child = Command::new("/usr/bin/mini-journalreader")
let mut child = Command::new("mini-journalreader")
.args(&args)
.stdout(Stdio::piped())
.spawn()?;

View File

@ -1,28 +1,671 @@
use failure::*;
use serde_json::{json, Value};
use anyhow::{Error, bail};
use serde_json::{Value, to_value};
use ::serde::{Deserialize, Serialize};
use proxmox::api::{ApiHandler, ApiMethod, Router, RpcEnvironment};
use proxmox::api::schema::ObjectSchema;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
use proxmox::api::schema::parse_property_string;
use crate::config::network::{self, NetworkConfig};
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
use crate::api2::types::*;
use crate::server::{WorkerTask};
fn get_network_config(
_param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
Ok(json!({}))
fn split_interface_list(list: &str) -> Result<Vec<String>, Error> {
let value = parse_property_string(&list, &NETWORK_INTERFACE_ARRAY_SCHEMA)?;
Ok(value.as_array().unwrap().iter().map(|v| v.as_str().unwrap().to_string()).collect())
}
fn check_duplicate_gateway_v4(config: &NetworkConfig, iface: &str) -> Result<(), Error> {
let current_gateway_v4 = config.interfaces.iter()
.find(|(_, interface)| interface.gateway.is_some())
.map(|(name, _)| name.to_string());
if let Some(current_gateway_v4) = current_gateway_v4 {
if current_gateway_v4 != iface {
bail!("Default IPv4 gateway already exists on interface '{}'", current_gateway_v4);
}
}
Ok(())
}
fn check_duplicate_gateway_v6(config: &NetworkConfig, iface: &str) -> Result<(), Error> {
let current_gateway_v6 = config.interfaces.iter()
.find(|(_, interface)| interface.gateway6.is_some())
.map(|(name, _)| name.to_string());
if let Some(current_gateway_v6) = current_gateway_v6 {
if current_gateway_v6 != iface {
bail!("Default IPv6 gateway already exists on interface '{}'", current_gateway_v6);
}
}
Ok(())
}
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
},
},
returns: {
description: "List network devices (with config digest).",
type: Array,
items: {
type: Interface,
},
},
access: {
permission: &Permission::Privilege(&["system", "network", "interfaces"], PRIV_SYS_AUDIT, false),
},
)]
/// List all datastores
pub fn list_network_devices(
_param: Value,
_info: &ApiMethod,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
let (config, digest) = network::config()?;
let digest = proxmox::tools::digest_to_hex(&digest);
let mut list = Vec::new();
for (iface, interface) in config.interfaces.iter() {
if iface == "lo" { continue; } // do not list lo
let mut item: Value = to_value(interface)?;
item["digest"] = digest.clone().into();
item["iface"] = iface.to_string().into();
list.push(item);
}
let diff = network::changes()?;
if !diff.is_empty() {
rpcenv["changes"] = diff.into();
}
Ok(list.into())
}
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
iface: {
schema: NETWORK_INTERFACE_NAME_SCHEMA,
},
},
},
returns: {
description: "The network interface configuration (with config digest).",
type: Interface,
},
access: {
permission: &Permission::Privilege(&["system", "network", "interfaces", "{name}"], PRIV_SYS_AUDIT, false),
},
)]
/// Read a network interface configuration.
pub fn read_interface(iface: String) -> Result<Value, Error> {
let (config, digest) = network::config()?;
let interface = config.lookup(&iface)?;
let mut data: Value = to_value(interface)?;
data["digest"] = proxmox::tools::digest_to_hex(&digest).into();
Ok(data)
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
iface: {
schema: NETWORK_INTERFACE_NAME_SCHEMA,
},
"type": {
description: "Interface type.",
type: NetworkInterfaceType,
optional: true,
},
autostart: {
description: "Autostart interface.",
type: bool,
optional: true,
},
method: {
type: NetworkConfigMethod,
optional: true,
},
method6: {
type: NetworkConfigMethod,
optional: true,
},
comments: {
description: "Comments (inet, may span multiple lines)",
type: String,
optional: true,
},
comments6: {
description: "Comments (inet5, may span multiple lines)",
type: String,
optional: true,
},
cidr: {
schema: CIDR_V4_SCHEMA,
optional: true,
},
cidr6: {
schema: CIDR_V6_SCHEMA,
optional: true,
},
gateway: {
schema: IP_V4_SCHEMA,
optional: true,
},
gateway6: {
schema: IP_V6_SCHEMA,
optional: true,
},
mtu: {
description: "Maximum Transmission Unit.",
optional: true,
minimum: 46,
maximum: 65535,
default: 1500,
},
bridge_ports: {
schema: NETWORK_INTERFACE_LIST_SCHEMA,
optional: true,
},
bridge_vlan_aware: {
description: "Enable bridge vlan support.",
type: bool,
optional: true,
},
bond_mode: {
type: LinuxBondMode,
optional: true,
},
slaves: {
schema: NETWORK_INTERFACE_LIST_SCHEMA,
optional: true,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "network", "interfaces", "{iface}"], PRIV_SYS_MODIFY, false),
},
)]
/// Create network interface configuration.
pub fn create_interface(
iface: String,
autostart: Option<bool>,
method: Option<NetworkConfigMethod>,
method6: Option<NetworkConfigMethod>,
comments: Option<String>,
comments6: Option<String>,
cidr: Option<String>,
gateway: Option<String>,
cidr6: Option<String>,
gateway6: Option<String>,
mtu: Option<u64>,
bridge_ports: Option<String>,
bridge_vlan_aware: Option<bool>,
bond_mode: Option<LinuxBondMode>,
slaves: Option<String>,
param: Value,
) -> Result<(), Error> {
let interface_type = crate::tools::required_string_param(&param, "type")?;
let interface_type: NetworkInterfaceType = serde_json::from_value(interface_type.into())?;
let _lock = crate::tools::open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut config, _digest) = network::config()?;
if config.interfaces.contains_key(&iface) {
bail!("interface '{}' already exists", iface);
}
let mut interface = Interface::new(iface.clone());
interface.interface_type = interface_type;
if let Some(autostart) = autostart { interface.autostart = autostart; }
if method.is_some() { interface.method = method; }
if method6.is_some() { interface.method6 = method6; }
if mtu.is_some() { interface.mtu = mtu; }
if comments.is_some() { interface.comments = comments; }
if comments6.is_some() { interface.comments6 = comments6; }
if let Some(cidr) = cidr {
let (_, _, is_v6) = network::parse_cidr(&cidr)?;
if is_v6 { bail!("invalid address type (expected IPv4, got IPv6)"); }
interface.cidr = Some(cidr);
}
if let Some(cidr6) = cidr6 {
let (_, _, is_v6) = network::parse_cidr(&cidr6)?;
if !is_v6 { bail!("invalid address type (expected IPv6, got IPv4)"); }
interface.cidr6 = Some(cidr6);
}
if let Some(gateway) = gateway {
let is_v6 = gateway.contains(':');
if is_v6 { bail!("invalid address type (expected IPv4, got IPv6)"); }
check_duplicate_gateway_v4(&config, &iface)?;
interface.gateway = Some(gateway);
}
if let Some(gateway6) = gateway6 {
let is_v6 = gateway6.contains(':');
if !is_v6 { bail!("invalid address type (expected IPv6, got IPv4)"); }
check_duplicate_gateway_v6(&config, &iface)?;
interface.gateway6 = Some(gateway6);
}
match interface_type {
NetworkInterfaceType::Bridge => {
if let Some(ports) = bridge_ports {
let ports = split_interface_list(&ports)?;
interface.set_bridge_ports(ports)?;
}
if bridge_vlan_aware.is_some() { interface.bridge_vlan_aware = bridge_vlan_aware; }
}
NetworkInterfaceType::Bond => {
if bond_mode.is_some() { interface.bond_mode = bond_mode; }
if let Some(slaves) = slaves {
let slaves = split_interface_list(&slaves)?;
interface.set_bond_slaves(slaves)?;
}
}
_ => bail!("creating network interface type '{:?}' is not supported", interface_type),
}
if interface.cidr.is_some() || interface.gateway.is_some() {
interface.method = Some(NetworkConfigMethod::Static);
} else if interface.method.is_none() {
interface.method = Some(NetworkConfigMethod::Manual);
}
if interface.cidr6.is_some() || interface.gateway6.is_some() {
interface.method6 = Some(NetworkConfigMethod::Static);
} else if interface.method6.is_none() {
interface.method6 = Some(NetworkConfigMethod::Manual);
}
config.interfaces.insert(iface, interface);
network::save_config(&config)?;
Ok(())
}
#[api()]
#[derive(Serialize, Deserialize)]
#[allow(non_camel_case_types)]
/// Deletable property name
pub enum DeletableProperty {
/// Delete the IPv4 address property.
cidr,
/// Delete the IPv6 address property.
cidr6,
/// Delete the IPv4 gateway property.
gateway,
/// Delete the IPv6 gateway property.
gateway6,
/// Delete the whole IPv4 configuration entry.
method,
/// Delete the whole IPv6 configuration entry.
method6,
/// Delete IPv4 comments
comments,
/// Delete IPv6 comments
comments6,
/// Delete mtu.
mtu,
/// Delete autostart flag
autostart,
/// Delete bridge ports (set to 'none')
bridge_ports,
/// Delete bridge-vlan-aware flag
bridge_vlan_aware,
/// Delete bond-slaves (set to 'none')
slaves,
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
iface: {
schema: NETWORK_INTERFACE_NAME_SCHEMA,
},
"type": {
description: "Interface type. If specified, need to match the current type.",
type: NetworkInterfaceType,
optional: true,
},
autostart: {
description: "Autostart interface.",
type: bool,
optional: true,
},
method: {
type: NetworkConfigMethod,
optional: true,
},
method6: {
type: NetworkConfigMethod,
optional: true,
},
comments: {
description: "Comments (inet, may span multiple lines)",
type: String,
optional: true,
},
comments6: {
description: "Comments (inet5, may span multiple lines)",
type: String,
optional: true,
},
cidr: {
schema: CIDR_V4_SCHEMA,
optional: true,
},
cidr6: {
schema: CIDR_V6_SCHEMA,
optional: true,
},
gateway: {
schema: IP_V4_SCHEMA,
optional: true,
},
gateway6: {
schema: IP_V6_SCHEMA,
optional: true,
},
mtu: {
description: "Maximum Transmission Unit.",
optional: true,
minimum: 46,
maximum: 65535,
default: 1500,
},
bridge_ports: {
schema: NETWORK_INTERFACE_LIST_SCHEMA,
optional: true,
},
bridge_vlan_aware: {
description: "Enable bridge vlan support.",
type: bool,
optional: true,
},
bond_mode: {
type: LinuxBondMode,
optional: true,
},
slaves: {
schema: NETWORK_INTERFACE_LIST_SCHEMA,
optional: true,
},
delete: {
description: "List of properties to delete.",
type: Array,
optional: true,
items: {
type: DeletableProperty,
}
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "network", "interfaces", "{iface}"], PRIV_SYS_MODIFY, false),
},
)]
/// Update network interface config.
pub fn update_interface(
iface: String,
autostart: Option<bool>,
method: Option<NetworkConfigMethod>,
method6: Option<NetworkConfigMethod>,
comments: Option<String>,
comments6: Option<String>,
cidr: Option<String>,
gateway: Option<String>,
cidr6: Option<String>,
gateway6: Option<String>,
mtu: Option<u64>,
bridge_ports: Option<String>,
bridge_vlan_aware: Option<bool>,
bond_mode: Option<LinuxBondMode>,
slaves: Option<String>,
delete: Option<Vec<DeletableProperty>>,
digest: Option<String>,
param: Value,
) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut config, expected_digest) = network::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
if gateway.is_some() { check_duplicate_gateway_v4(&config, &iface)?; }
if gateway6.is_some() { check_duplicate_gateway_v6(&config, &iface)?; }
let interface = config.lookup_mut(&iface)?;
if let Some(interface_type) = param.get("type") {
let interface_type: NetworkInterfaceType = serde_json::from_value(interface_type.clone())?;
if interface_type != interface.interface_type {
bail!("got unexpected interface type ({:?} != {:?})", interface_type, interface.interface_type);
}
}
if let Some(delete) = delete {
for delete_prop in delete {
match delete_prop {
DeletableProperty::cidr => { interface.cidr = None; },
DeletableProperty::cidr6 => { interface.cidr6 = None; },
DeletableProperty::gateway => { interface.gateway = None; },
DeletableProperty::gateway6 => { interface.gateway6 = None; },
DeletableProperty::method => { interface.method = None; },
DeletableProperty::method6 => { interface.method6 = None; },
DeletableProperty::comments => { interface.comments = None; },
DeletableProperty::comments6 => { interface.comments6 = None; },
DeletableProperty::mtu => { interface.mtu = None; },
DeletableProperty::autostart => { interface.autostart = false; },
DeletableProperty::bridge_ports => { interface.set_bridge_ports(Vec::new())?; }
DeletableProperty::bridge_vlan_aware => { interface.bridge_vlan_aware = None; }
DeletableProperty::slaves => { interface.set_bond_slaves(Vec::new())?; }
}
}
}
if let Some(autostart) = autostart { interface.autostart = autostart; }
if method.is_some() { interface.method = method; }
if method6.is_some() { interface.method6 = method6; }
if mtu.is_some() { interface.mtu = mtu; }
if let Some(ports) = bridge_ports {
let ports = split_interface_list(&ports)?;
interface.set_bridge_ports(ports)?;
}
if bridge_vlan_aware.is_some() { interface.bridge_vlan_aware = bridge_vlan_aware; }
if let Some(slaves) = slaves {
let slaves = split_interface_list(&slaves)?;
interface.set_bond_slaves(slaves)?;
}
if bond_mode.is_some() { interface.bond_mode = bond_mode; }
if let Some(cidr) = cidr {
let (_, _, is_v6) = network::parse_cidr(&cidr)?;
if is_v6 { bail!("invalid address type (expected IPv4, got IPv6)"); }
interface.cidr = Some(cidr);
}
if let Some(cidr6) = cidr6 {
let (_, _, is_v6) = network::parse_cidr(&cidr6)?;
if !is_v6 { bail!("invalid address type (expected IPv6, got IPv4)"); }
interface.cidr6 = Some(cidr6);
}
if let Some(gateway) = gateway {
let is_v6 = gateway.contains(':');
if is_v6 { bail!("invalid address type (expected IPv4, got IPv6)"); }
interface.gateway = Some(gateway);
}
if let Some(gateway6) = gateway6 {
let is_v6 = gateway6.contains(':');
if !is_v6 { bail!("invalid address type (expected IPv6, got IPv4)"); }
interface.gateway6 = Some(gateway6);
}
if comments.is_some() { interface.comments = comments; }
if comments6.is_some() { interface.comments6 = comments6; }
if interface.cidr.is_some() || interface.gateway.is_some() {
interface.method = Some(NetworkConfigMethod::Static);
} else {
interface.method = Some(NetworkConfigMethod::Manual);
}
if interface.cidr6.is_some() || interface.gateway6.is_some() {
interface.method6 = Some(NetworkConfigMethod::Static);
} else {
interface.method6 = Some(NetworkConfigMethod::Manual);
}
network::save_config(&config)?;
Ok(())
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
iface: {
schema: NETWORK_INTERFACE_NAME_SCHEMA,
},
digest: {
optional: true,
schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "network", "interfaces", "{iface}"], PRIV_SYS_MODIFY, false),
},
)]
/// Remove network interface configuration.
pub fn delete_interface(iface: String, digest: Option<String>) -> Result<(), Error> {
let _lock = crate::tools::open_file_locked(network::NETWORK_LOCKFILE, std::time::Duration::new(10, 0))?;
let (mut config, expected_digest) = network::config()?;
if let Some(ref digest) = digest {
let digest = proxmox::tools::hex_to_digest(digest)?;
crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
}
let _interface = config.lookup(&iface)?; // check if interface exists
config.interfaces.remove(&iface);
network::save_config(&config)?;
Ok(())
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "network", "interfaces"], PRIV_SYS_MODIFY, false),
},
)]
/// Reload network configuration (requires ifupdown2).
pub async fn reload_network_config(
rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> {
network::assert_ifupdown2_installed()?;
let username = rpcenv.get_user().unwrap();
let upid_str = WorkerTask::spawn("srvreload", Some(String::from("networking")), &username.clone(), true, |_worker| async {
let _ = std::fs::rename(network::NETWORK_INTERFACES_NEW_FILENAME, network::NETWORK_INTERFACES_FILENAME);
network::network_reload()?;
Ok(())
})?;
Ok(upid_str)
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "network", "interfaces"], PRIV_SYS_MODIFY, false),
},
)]
/// Revert network configuration (rm /etc/network/interfaces.new).
pub fn revert_network_config() -> Result<(), Error> {
let _ = std::fs::remove_file(network::NETWORK_INTERFACES_NEW_FILENAME);
Ok(())
}
const ITEM_ROUTER: Router = Router::new()
.get(&API_METHOD_READ_INTERFACE)
.put(&API_METHOD_UPDATE_INTERFACE)
.delete(&API_METHOD_DELETE_INTERFACE);
pub const ROUTER: Router = Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&get_network_config),
&ObjectSchema::new(
"Read network configuration.",
&[ ("node", false, &NODE_SCHEMA) ],
)
)
);
.get(&API_METHOD_LIST_NETWORK_DEVICES)
.put(&API_METHOD_RELOAD_NETWORK_CONFIG)
.post(&API_METHOD_CREATE_INTERFACE)
.delete(&API_METHOD_REVERT_NETWORK_CONFIG)
.match_all("iface", &ITEM_ROUTER);

87
src/api2/node/rrd.rs Normal file
View File

@ -0,0 +1,87 @@
use anyhow::Error;
use serde_json::{Value, json};
use proxmox::api::{api, Router};
use crate::api2::types::*;
use crate::tools::epoch_now_f64;
use crate::rrd::{extract_cached_data, RRD_DATA_ENTRIES};
pub fn create_value_from_rrd(
basedir: &str,
list: &[&str],
timeframe: RRDTimeFrameResolution,
cf: RRDMode,
) -> Result<Value, Error> {
let mut result = Vec::new();
let now = epoch_now_f64()?;
for name in list {
let (start, reso, list) = match extract_cached_data(basedir, name, now, timeframe, cf) {
Some(result) => result,
None => continue,
};
let mut t = start;
for index in 0..RRD_DATA_ENTRIES {
if result.len() <= index {
if let Some(value) = list[index] {
result.push(json!({ "time": t, *name: value }));
} else {
result.push(json!({ "time": t }));
}
} else {
if let Some(value) = list[index] {
result[index][name] = value.into();
}
}
t += reso;
}
}
Ok(result.into())
}
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
timeframe: {
type: RRDTimeFrameResolution,
},
cf: {
type: RRDMode,
},
},
},
)]
/// Read node stats
fn get_node_stats(
timeframe: RRDTimeFrameResolution,
cf: RRDMode,
_param: Value,
) -> Result<Value, Error> {
create_value_from_rrd(
"host",
&[
"cpu", "iowait",
"memtotal", "memused",
"swaptotal", "swapused",
"netin", "netout",
"loadavg",
"total", "used",
"read_ios", "read_bytes",
"write_ios", "write_bytes",
"io_ticks",
],
timeframe,
cf,
)
}
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_GET_NODE_STATS);

View File

@ -1,15 +1,15 @@
use std::process::{Command, Stdio};
use failure::*;
use anyhow::{bail, Error};
use serde_json::{json, Value};
use proxmox::{sortable, identity, list_subdirs_api_method};
use proxmox::api::{ApiHandler, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{api, Router, Permission};
use proxmox::api::router::SubdirMap;
use proxmox::api::schema::*;
use crate::api2::types::*;
use crate::tools;
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
static SERVICE_NAME_LIST: [&str; 7] = [
"proxmox-backup",
@ -38,7 +38,7 @@ fn get_full_service_state(service: &str) -> Result<Value, Error> {
let real_service_name = real_service_name(service);
let mut child = Command::new("/bin/systemctl")
let mut child = Command::new("systemctl")
.args(&["show", real_service_name])
.stdout(Stdio::piped())
.spawn()?;
@ -91,11 +91,45 @@ fn json_service_state(service: &str, status: Value) -> Value {
Value::Null
}
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
},
},
returns: {
description: "Returns a list of systemd services.",
type: Array,
items: {
description: "Service details.",
properties: {
service: {
schema: SERVICE_ID_SCHEMA,
},
name: {
type: String,
description: "systemd service name.",
},
desc: {
type: String,
description: "systemd service description.",
},
state: {
type: String,
description: "systemd service 'SubState'.",
},
},
},
},
access: {
permission: &Permission::Privilege(&["system", "services"], PRIV_SYS_AUDIT, false),
},
)]
/// Service list.
fn list_services(
_param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
let mut list = vec![];
@ -115,21 +149,36 @@ fn list_services(
Ok(Value::from(list))
}
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
service: {
schema: SERVICE_ID_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_AUDIT, false),
},
)]
/// Read service properties.
fn get_service_state(
param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
service: String,
_param: Value,
) -> Result<Value, Error> {
let service = tools::required_string_param(&param, "service")?;
let service = service.as_str();
if !SERVICE_NAME_LIST.contains(&service) {
bail!("unknown service name '{}'", service);
}
let status = get_full_service_state(service)?;
let status = get_full_service_state(&service)?;
Ok(json_service_state(service, status))
Ok(json_service_state(&service, status))
}
fn run_service_command(service: &str, cmd: &str) -> Result<Value, Error> {
@ -147,7 +196,7 @@ fn run_service_command(service: &str, cmd: &str) -> Result<Value, Error> {
let real_service_name = real_service_name(service);
let status = Command::new("/bin/systemctl")
let status = Command::new("systemctl")
.args(&[cmd, real_service_name])
.status()?;
@ -158,61 +207,117 @@ fn run_service_command(service: &str, cmd: &str) -> Result<Value, Error> {
Ok(Value::Null)
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
service: {
schema: SERVICE_ID_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_MODIFY, false),
},
)]
/// Start service.
fn start_service(
param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
service: String,
_param: Value,
) -> Result<Value, Error> {
let service = tools::required_string_param(&param, "service")?;
log::info!("starting service {}", service);
run_service_command(service, "start")
run_service_command(&service, "start")
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
service: {
schema: SERVICE_ID_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_MODIFY, false),
},
)]
/// Stop service.
fn stop_service(
param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
service: String,
_param: Value,
) -> Result<Value, Error> {
let service = tools::required_string_param(&param, "service")?;
log::info!("stopping service {}", service);
log::info!("stoping service {}", service);
run_service_command(service, "stop")
run_service_command(&service, "stop")
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
service: {
schema: SERVICE_ID_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_MODIFY, false),
},
)]
/// Retart service.
fn restart_service(
param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
service: String,
_param: Value,
) -> Result<Value, Error> {
let service = tools::required_string_param(&param, "service")?;
log::info!("re-starting service {}", service);
if service == "proxmox-backup-proxy" {
if &service == "proxmox-backup-proxy" {
// special case, avoid aborting running tasks
run_service_command(service, "reload")
run_service_command(&service, "reload")
} else {
run_service_command(service, "restart")
run_service_command(&service, "restart")
}
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
service: {
schema: SERVICE_ID_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_MODIFY, false),
},
)]
/// Reload service.
fn reload_service(
param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
service: String,
_param: Value,
) -> Result<Value, Error> {
let service = tools::required_string_param(&param, "service")?;
log::info!("reloading service {}", service);
run_service_command(service, "reload")
run_service_command(&service, "reload")
}
@ -221,111 +326,33 @@ const SERVICE_ID_SCHEMA: Schema = StringSchema::new("Service ID.")
.schema();
#[sortable]
const SERVICE_SUBDIRS: SubdirMap = &[
const SERVICE_SUBDIRS: SubdirMap = &sorted!([
(
"reload", &Router::new()
.post(
&ApiMethod::new(
&ApiHandler::Sync(&reload_service),
&ObjectSchema::new(
"Reload service.",
&sorted!([
("node", false, &NODE_SCHEMA),
("service", false, &SERVICE_ID_SCHEMA),
]),
)
).protected(true)
)
.post(&API_METHOD_RELOAD_SERVICE)
),
(
"restart", &Router::new()
.post(
&ApiMethod::new(
&ApiHandler::Sync(&restart_service),
&ObjectSchema::new(
"Restart service.",
&sorted!([
("node", false, &NODE_SCHEMA),
("service", false, &SERVICE_ID_SCHEMA),
]),
)
).protected(true)
)
.post(&API_METHOD_RESTART_SERVICE)
),
(
"start", &Router::new()
.post(
&ApiMethod::new(
&ApiHandler::Sync(&start_service),
&ObjectSchema::new(
"Start service.",
&sorted!([
("node", false, &NODE_SCHEMA),
("service", false, &SERVICE_ID_SCHEMA),
]),
)
).protected(true)
)
.post(&API_METHOD_START_SERVICE)
),
(
"state", &Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&get_service_state),
&ObjectSchema::new(
"Read service properties.",
&sorted!([
("node", false, &NODE_SCHEMA),
("service", false, &SERVICE_ID_SCHEMA),
]),
)
)
)
.get(&API_METHOD_GET_SERVICE_STATE)
),
(
"stop", &Router::new()
.post(
&ApiMethod::new(
&ApiHandler::Sync(&stop_service),
&ObjectSchema::new(
"Stop service.",
&sorted!([
("node", false, &NODE_SCHEMA),
("service", false, &SERVICE_ID_SCHEMA),
]),
)
).protected(true)
)
.post(&API_METHOD_STOP_SERVICE)
),
];
]);
const SERVICE_ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SERVICE_SUBDIRS))
.subdirs(SERVICE_SUBDIRS);
#[sortable]
pub const ROUTER: Router = Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&list_services),
&ObjectSchema::new(
"Service list.",
&sorted!([ ("node", false, &NODE_SCHEMA) ]),
)
).returns(
&ArraySchema::new(
"Returns a list of systemd services.",
&ObjectSchema::new(
"Service details.",
&sorted!([
("service", false, &SERVICE_ID_SCHEMA),
("name", false, &StringSchema::new("systemd service name.").schema()),
("desc", false, &StringSchema::new("systemd service description.").schema()),
("state", false, &StringSchema::new("systemd service 'SubState'.").schema()),
]),
).schema()
).schema()
)
)
.get(&API_METHOD_LIST_SERVICES)
.match_all("service", &SERVICE_ROUTER);

View File

@ -1,12 +1,15 @@
use failure::*;
use std::process::Command;
use std::path::Path;
use anyhow::{Error, format_err, bail};
use serde_json::{json, Value};
use proxmox::sys::linux::procfs;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, SubdirMap};
use proxmox::list_subdirs_api_method;
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
use crate::api2::types::*;
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_POWER_MANAGEMENT};
#[api(
input: {
@ -44,7 +47,10 @@ use crate::api2::types::*;
optional: true,
},
}
}
},
access: {
permission: &Permission::Privilege(&["system", "status"], PRIV_SYS_AUDIT, false),
},
)]
/// Read node memory, CPU and (root) disk usage
fn get_usage(
@ -55,6 +61,7 @@ fn get_usage(
let meminfo: procfs::ProcFsMemInfo = procfs::read_meminfo()?;
let kstat: procfs::ProcFsStat = procfs::read_proc_stat()?;
let disk_usage = crate::tools::disks::disk_usage(Path::new("/"))?;
Ok(json!({
"memory": {
@ -63,15 +70,57 @@ fn get_usage(
"free": meminfo.memfree,
},
"cpu": kstat.cpu,
"root": {
"total": disk_usage.total,
"used": disk_usage.used,
"free": disk_usage.avail,
}
}))
}
pub const USAGE_ROUTER: Router = Router::new()
.get(&API_METHOD_GET_USAGE);
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
command: {
type: NodePowerCommand,
},
}
},
access: {
permission: &Permission::Privilege(&["system", "status"], PRIV_SYS_POWER_MANAGEMENT, false),
},
)]
/// Reboot or shutdown the node.
fn reboot_or_shutdown(command: NodePowerCommand) -> Result<(), Error> {
let systemctl_command = match command {
NodePowerCommand::Reboot => "reboot",
NodePowerCommand::Shutdown => "poweroff",
};
let output = Command::new("systemctl")
.arg(systemctl_command)
.output()
.map_err(|err| format_err!("failed to execute systemctl - {}", err))?;
if !output.status.success() {
match output.status.code() {
Some(code) => {
let msg = String::from_utf8(output.stderr)
.map(|m| if m.is_empty() { String::from("no error message") } else { m })
.unwrap_or_else(|_| String::from("non utf8 error message (suppressed)"));
bail!("diff failed with status code: {} - {}", code, msg);
}
None => bail!("systemctl terminated by signal"),
}
}
Ok(())
}
pub const SUBDIRS: SubdirMap = &[
("usage", &USAGE_ROUTER),
];
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);
.get(&API_METHOD_GET_USAGE)
.post(&API_METHOD_REBOOT_OR_SHUTDOWN);

View File

@ -1,11 +1,12 @@
use std::process::{Command, Stdio};
use failure::*;
use anyhow::{Error};
use serde_json::{json, Value};
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment, Permission};
use crate::api2::types::*;
use crate::config::acl::PRIV_SYS_AUDIT;
fn dump_journal(
start: Option<u64>,
@ -26,7 +27,7 @@ fn dump_journal(
let start = start.unwrap_or(0);
let mut count: u64 = 0;
let mut child = Command::new("/bin/journalctl")
let mut child = Command::new("journalctl")
.args(&args)
.stdout(Stdio::piped())
.spawn()?;
@ -122,12 +123,15 @@ fn dump_journal(
}
},
},
access: {
permission: &Permission::Privilege(&["system", "log"], PRIV_SYS_AUDIT, false),
},
)]
/// Read syslog entries.
fn get_syslog(
param: Value,
_info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
let (count, lines) = dump_journal(
@ -137,7 +141,7 @@ fn get_syslog(
param["until"].as_str(),
param["service"].as_str())?;
rpcenv.set_result_attrib("total", Value::from(count));
rpcenv["total"] = Value::from(count);
Ok(json!(lines))
}

View File

@ -1,26 +1,96 @@
use std::fs::File;
use std::io::{BufRead, BufReader};
use failure::*;
use anyhow::{Error};
use serde_json::{json, Value};
use proxmox::api::{api, ApiHandler, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{api, Router, RpcEnvironment, Permission, UserInformation};
use proxmox::api::router::SubdirMap;
use proxmox::api::schema::*;
use proxmox::{identity, list_subdirs_api_method, sortable};
use crate::tools;
use crate::api2::types::*;
use crate::server::{self, UPID};
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
use crate::config::cached_user_info::CachedUserInfo;
fn get_task_status(
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
upid: {
schema: UPID_SCHEMA,
},
},
},
returns: {
description: "Task status nformation.",
properties: {
node: {
schema: NODE_SCHEMA,
},
upid: {
schema: UPID_SCHEMA,
},
pid: {
type: i64,
description: "The Unix PID.",
},
pstart: {
type: u64,
description: "The Unix process start time from `/proc/pid/stat`",
},
starttime: {
type: i64,
description: "The task start time (Epoch)",
},
"type": {
type: String,
description: "Worker type (arbitrary ASCII string)",
},
id: {
type: String,
optional: true,
description: "Worker ID (arbitrary ASCII string)",
},
user: {
type: String,
description: "The user who started the task.",
},
status: {
type: String,
description: "'running' or 'stopped'",
},
exitstatus: {
type: String,
optional: true,
description: "'OK', 'Error: <msg>', or 'unkwown'.",
},
},
},
access: {
description: "Users can access there own tasks, or need Sys.Audit on /system/tasks.",
permission: &Permission::Anybody,
},
)]
/// Get task status.
async fn get_task_status(
param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
let upid = extract_upid(&param)?;
let username = rpcenv.get_user().unwrap();
if username != upid.username {
let user_info = CachedUserInfo::new()?;
user_info.check_privs(&username, &["system", "tasks"], PRIV_SYS_AUDIT, false)?;
}
let mut result = json!({
"upid": param["upid"],
"node": upid.node,
@ -32,7 +102,7 @@ fn get_task_status(
"user": upid.username,
});
if crate::server::worker_is_active(&upid) {
if crate::server::worker_is_active(&upid).await? {
result["status"] = Value::from("running");
} else {
let exitstatus = crate::server::upid_read_status(&upid).unwrap_or(String::from("unknown"));
@ -50,14 +120,54 @@ fn extract_upid(param: &Value) -> Result<UPID, Error> {
upid_str.parse::<UPID>()
}
fn read_task_log(
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
upid: {
schema: UPID_SCHEMA,
},
"test-status": {
type: bool,
optional: true,
description: "Test task status, and set result attribute \"active\" accordingly.",
},
start: {
type: u64,
optional: true,
description: "Start at this line.",
default: 0,
},
limit: {
type: u64,
optional: true,
description: "Only list this amount of lines.",
default: 50,
},
},
},
access: {
description: "Users can access there own tasks, or need Sys.Audit on /system/tasks.",
permission: &Permission::Anybody,
},
)]
/// Read task log.
async fn read_task_log(
param: Value,
_info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
let upid = extract_upid(&param)?;
let username = rpcenv.get_user().unwrap();
if username != upid.username {
let user_info = CachedUserInfo::new()?;
user_info.check_privs(&username, &["system", "tasks"], PRIV_SYS_AUDIT, false)?;
}
let test_status = param["test-status"].as_bool().unwrap_or(false);
let start = param["start"].as_u64().unwrap_or(0);
@ -89,28 +199,50 @@ fn read_task_log(
}
}
rpcenv.set_result_attrib("total", Value::from(count));
rpcenv["total"] = Value::from(count);
if test_status {
let active = crate::server::worker_is_active(&upid);
rpcenv.set_result_attrib("active", Value::from(active));
let active = crate::server::worker_is_active(&upid).await?;
rpcenv["active"] = Value::from(active);
}
Ok(json!(lines))
}
#[api(
protected: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
upid: {
schema: UPID_SCHEMA,
},
},
},
access: {
description: "Users can stop there own tasks, or need Sys.Modify on /system/tasks.",
permission: &Permission::Anybody,
},
)]
/// Try to stop a task.
fn stop_task(
param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
let upid = extract_upid(&param)?;
if crate::server::worker_is_active(&upid) {
server::abort_worker_async(upid);
let username = rpcenv.get_user().unwrap();
if username != upid.username {
let user_info = CachedUserInfo::new()?;
user_info.check_privs(&username, &["system", "tasks"], PRIV_SYS_MODIFY, false)?;
}
server::abort_worker_async(upid);
Ok(Value::Null)
}
@ -140,11 +272,13 @@ fn stop_task(
type: bool,
description: "Only list running tasks.",
optional: true,
default: false,
},
errors: {
type: bool,
description: "Only list erroneous tasks.",
optional:true,
default: false,
},
userfilter: {
optional:true,
@ -158,18 +292,26 @@ fn stop_task(
type: Array,
items: { type: TaskListItem },
},
access: {
description: "Users can only see there own tasks, unless the have Sys.Audit on /system/tasks.",
permission: &Permission::Anybody,
},
)]
/// List tasks.
pub fn list_tasks(
start: u64,
limit: u64,
errors: bool,
running: bool,
param: Value,
_info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment,
mut rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<TaskListItem>, Error> {
let start = param["start"].as_u64().unwrap_or(0);
let limit = param["limit"].as_u64().unwrap_or(50);
let errors = param["errors"].as_bool().unwrap_or(false);
let running = param["running"].as_bool().unwrap_or(false);
let username = rpcenv.get_user().unwrap();
let user_info = CachedUserInfo::new()?;
let user_privs = user_info.lookup_privs(&username, &["system", "tasks"]);
let list_all = (user_privs & PRIV_SYS_AUDIT) != 0;
let store = param["store"].as_str();
@ -181,19 +323,9 @@ pub fn list_tasks(
let mut count = 0;
for info in list.iter() {
let mut entry = TaskListItem {
upid: info.upid_str.clone(),
node: "localhost".to_string(),
pid: info.upid.pid as i64,
pstart: info.upid.pstart,
starttime: info.upid.starttime,
worker_type: info.upid.worker_type.clone(),
worker_id: info.upid.worker_id.clone(),
user: info.upid.username.clone(),
endtime: None,
status: None,
};
for info in list {
if !list_all && info.upid.username != username { continue; }
if let Some(username) = userfilter {
if !info.upid.username.contains(username) { continue; }
@ -223,9 +355,6 @@ pub fn list_tasks(
if errors && state.1 == "OK" {
continue;
}
entry.endtime = Some(state.0);
entry.status = Some(state.1.clone());
}
if (count as u64) < start {
@ -235,82 +364,31 @@ pub fn list_tasks(
count += 1;
}
if (result.len() as u64) < limit { result.push(entry); };
if (result.len() as u64) < limit { result.push(info.into()); };
}
rpcenv.set_result_attrib("total", Value::from(count));
rpcenv["total"] = Value::from(count);
Ok(result)
}
#[sortable]
const UPID_API_SUBDIRS: SubdirMap = &[
const UPID_API_SUBDIRS: SubdirMap = &sorted!([
(
"log", &Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&read_task_log),
&ObjectSchema::new(
"Read task log.",
&sorted!([
("node", false, &NODE_SCHEMA),
( "test-status",
true,
&BooleanSchema::new(
"Test task status, and set result attribute \"active\" accordingly."
).schema()
),
("upid", false, &UPID_SCHEMA),
("start", true, &IntegerSchema::new("Start at this line.")
.minimum(0)
.default(0)
.schema()
),
("limit", true, &IntegerSchema::new("Only list this amount of lines.")
.minimum(0)
.default(50)
.schema()
),
]),
)
)
)
.get(&API_METHOD_READ_TASK_LOG)
),
(
"status", &Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&get_task_status),
&ObjectSchema::new(
"Get task status.",
&sorted!([
("node", false, &NODE_SCHEMA),
("upid", false, &UPID_SCHEMA),
]),
)
)
)
.get(&API_METHOD_GET_TASK_STATUS)
)
];
]);
#[sortable]
pub const UPID_API_ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(UPID_API_SUBDIRS))
.delete(
&ApiMethod::new(
&ApiHandler::Sync(&stop_task),
&ObjectSchema::new(
"Try to stop a task.",
&sorted!([
("node", false, &NODE_SCHEMA),
("upid", false, &UPID_SCHEMA),
]),
)
).protected(true)
)
.delete(&API_METHOD_STOP_TASK)
.subdirs(&UPID_API_SUBDIRS);
#[sortable]
pub const ROUTER: Router = Router::new()
.get(&API_METHOD_LIST_TASKS)
.match_all("upid", &UPID_API_ROUTER);

View File

@ -1,14 +1,11 @@
use std::mem::{self, MaybeUninit};
use chrono::prelude::*;
use failure::*;
use anyhow::{bail, format_err, Error};
use serde_json::{json, Value};
use proxmox::{sortable, identity};
use proxmox::api::{ApiHandler, ApiMethod, Router, RpcEnvironment};
use proxmox::api::schema::*;
use proxmox::api::{api, Router, Permission};
use proxmox::tools::fs::{file_read_firstline, replace_file, CreateOptions};
use crate::config::acl::PRIV_SYS_MODIFY;
use crate::api2::types::*;
fn read_etc_localtime() -> Result<String, Error> {
@ -18,34 +15,48 @@ fn read_etc_localtime() -> Result<String, Error> {
}
// otherwise guess from the /etc/localtime symlink
let mut buf = MaybeUninit::<[u8; 64]>::uninit();
let len = unsafe {
libc::readlink(
"/etc/localtime".as_ptr() as *const _,
buf.as_mut_ptr() as *mut _,
mem::size_of_val(&buf),
)
};
if len <= 0 {
bail!("failed to guess timezone");
}
let len = len as usize;
let buf = unsafe {
(*buf.as_mut_ptr())[len] = 0;
buf.assume_init()
};
let link = std::str::from_utf8(&buf[..len])?;
let link = std::fs::read_link("/etc/localtime").
map_err(|err| format_err!("failed to guess timezone - {}", err))?;
let link = link.to_string_lossy();
match link.rfind("/zoneinfo/") {
Some(pos) => Ok(link[(pos + 10)..].to_string()),
None => Ok(link.to_string()),
}
}
fn get_time(
_param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
#[api(
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
},
},
returns: {
description: "Returns server time and timezone.",
properties: {
timezone: {
schema: TIME_ZONE_SCHEMA,
},
time: {
type: i64,
description: "Seconds since 1970-01-01 00:00:00 UTC.",
minimum: 1_297_163_644,
},
localtime: {
type: i64,
description: "Seconds since 1970-01-01 00:00:00 UTC. (local time)",
minimum: 1_297_163_644,
},
}
},
access: {
permission: &Permission::Anybody,
},
)]
/// Read server time and time zone settings.
fn get_time(_param: Value) -> Result<Value, Error> {
let datetime = Local::now();
let offset = datetime.offset();
let time = datetime.timestamp();
@ -58,13 +69,28 @@ fn get_time(
}))
}
#[api(
protected: true,
reload_timezone: true,
input: {
properties: {
node: {
schema: NODE_SCHEMA,
},
timezone: {
schema: TIME_ZONE_SCHEMA,
},
},
},
access: {
permission: &Permission::Privilege(&["system", "time"], PRIV_SYS_MODIFY, false),
},
)]
/// Set time zone
fn set_timezone(
param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
timezone: String,
_param: Value,
) -> Result<Value, Error> {
let timezone = crate::tools::required_string_param(&param, "timezone")?;
let path = std::path::PathBuf::from(format!("/usr/share/zoneinfo/{}", timezone));
if !path.exists() {
@ -81,45 +107,6 @@ fn set_timezone(
Ok(Value::Null)
}
#[sortable]
pub const ROUTER: Router = Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&get_time),
&ObjectSchema::new(
"Read server time and time zone settings.",
&sorted!([ ("node", false, &NODE_SCHEMA) ]),
)
).returns(
&ObjectSchema::new(
"Returns server time and timezone.",
&sorted!([
("timezone", false, &StringSchema::new("Time zone").schema()),
("time", false, &IntegerSchema::new("Seconds since 1970-01-01 00:00:00 UTC.")
.minimum(1_297_163_644)
.schema()
),
("localtime", false, &IntegerSchema::new("Seconds since 1970-01-01 00:00:00 UTC. (local time)")
.minimum(1_297_163_644)
.schema()
),
]),
).schema()
)
)
.put(
&ApiMethod::new(
&ApiHandler::Sync(&set_timezone),
&ObjectSchema::new(
"Set time zone.",
&sorted!([
("node", false, &NODE_SCHEMA),
("timezone", false, &StringSchema::new(
"Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.")
.schema()
),
]),
)
).protected(true).reload_timezone(true)
);
.get(&API_METHOD_GET_TIME)
.put(&API_METHOD_SET_TIMEZONE);

View File

@ -1,370 +1,65 @@
//! Sync datastore from remote server
use std::sync::{Arc};
use failure::*;
use serde_json::json;
use std::convert::TryFrom;
use std::sync::Arc;
use std::collections::HashMap;
use std::io::{Seek, SeekFrom};
use chrono::{Utc, TimeZone};
use anyhow::{format_err, Error};
use proxmox::api::api;
use proxmox::api::{ApiMethod, Router, RpcEnvironment};
use proxmox::api::{ApiMethod, Router, RpcEnvironment, Permission};
use crate::server::{WorkerTask};
use crate::backup::*;
use crate::client::*;
use crate::config::remote;
use crate::backup::DataStore;
use crate::client::{HttpClient, HttpClientOptions, BackupRepository, pull::pull_store};
use crate::api2::types::*;
// fixme: implement filters
// fixme: delete vanished groups
// Todo: correctly lock backup groups
async fn pull_index_chunks<I: IndexFile>(
_worker: &WorkerTask,
chunk_reader: &mut RemoteChunkReader,
target: Arc<DataStore>,
index: I,
) -> Result<(), Error> {
use crate::config::{
remote,
acl::{PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_PRUNE, PRIV_REMOTE_READ},
cached_user_info::CachedUserInfo,
};
for pos in 0..index.index_count() {
let digest = index.index_digest(pos).unwrap();
let chunk_exists = target.cond_touch_chunk(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(&digest)?;
target.insert_chunk(&chunk, &digest)?;
}
Ok(())
}
async fn download_manifest(
reader: &BackupReader,
filename: &std::path::Path,
) -> Result<std::fs::File, Error> {
let tmp_manifest_file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.open(&filename)?;
let mut tmp_manifest_file = reader.download(MANIFEST_BLOB_NAME, tmp_manifest_file).await?;
tmp_manifest_file.seek(SeekFrom::Start(0))?;
Ok(tmp_manifest_file)
}
async fn pull_single_archive(
worker: &WorkerTask,
reader: &BackupReader,
chunk_reader: &mut RemoteChunkReader,
tgt_store: Arc<DataStore>,
snapshot: &BackupDir,
archive_name: &str,
) -> Result<(), Error> {
let mut path = tgt_store.base_path();
path.push(snapshot.relative_path());
path.push(archive_name);
let mut tmp_path = path.clone();
tmp_path.set_extension("tmp");
worker.log(format!("sync archive {}", archive_name));
let tmpfile = std::fs::OpenOptions::new()
.write(true)
.create(true)
.read(true)
.open(&tmp_path)?;
let tmpfile = reader.download(archive_name, tmpfile).await?;
match archive_type(archive_name)? {
ArchiveType::DynamicIndex => {
let index = DynamicIndexReader::new(tmpfile)
.map_err(|err| format_err!("unable to read dynamic index {:?} - {}", tmp_path, err))?;
pull_index_chunks(worker, chunk_reader, tgt_store.clone(), index).await?;
}
ArchiveType::FixedIndex => {
let index = FixedIndexReader::new(tmpfile)
.map_err(|err| format_err!("unable to read fixed index '{:?}' - {}", tmp_path, err))?;
pull_index_chunks(worker, chunk_reader, tgt_store.clone(), index).await?;
}
ArchiveType::Blob => { /* nothing to do */ }
}
if let Err(err) = std::fs::rename(&tmp_path, &path) {
bail!("Atomic rename file {:?} failed - {}", path, err);
}
Ok(())
}
async fn pull_snapshot(
worker: &WorkerTask,
reader: Arc<BackupReader>,
tgt_store: Arc<DataStore>,
snapshot: &BackupDir,
) -> Result<(), Error> {
let mut manifest_name = tgt_store.base_path();
manifest_name.push(snapshot.relative_path());
manifest_name.push(MANIFEST_BLOB_NAME);
let mut tmp_manifest_name = manifest_name.clone();
tmp_manifest_name.set_extension("tmp");
let mut tmp_manifest_file = download_manifest(&reader, &tmp_manifest_name).await?;
let tmp_manifest_blob = DataBlob::load(&mut tmp_manifest_file)?;
tmp_manifest_blob.verify_crc()?;
if manifest_name.exists() {
let manifest_blob = proxmox::try_block!({
let mut manifest_file = std::fs::File::open(&manifest_name)
.map_err(|err| format_err!("unable to open local manifest {:?} - {}", manifest_name, err))?;
let manifest_blob = DataBlob::load(&mut manifest_file)?;
manifest_blob.verify_crc()?;
Ok(manifest_blob)
}).map_err(|err: Error| {
format_err!("unable to read local manifest {:?} - {}", manifest_name, err)
})?;
if manifest_blob.raw_data() == tmp_manifest_blob.raw_data() {
return Ok(()); // nothing changed
}
}
let manifest = BackupManifest::try_from(tmp_manifest_blob)?;
let mut chunk_reader = RemoteChunkReader::new(reader.clone(), None, HashMap::new());
for item in manifest.files() {
let mut path = tgt_store.base_path();
path.push(snapshot.relative_path());
path.push(&item.filename);
if path.exists() {
match archive_type(&item.filename)? {
ArchiveType::DynamicIndex => {
let index = DynamicIndexReader::open(&path)?;
let (csum, size) = index.compute_csum();
match manifest.verify_file(&item.filename, &csum, size) {
Ok(_) => continue,
Err(err) => {
worker.log(format!("detected changed file {:?} - {}", path, err));
}
}
}
ArchiveType::FixedIndex => {
let index = FixedIndexReader::open(&path)?;
let (csum, size) = index.compute_csum();
match manifest.verify_file(&item.filename, &csum, size) {
Ok(_) => continue,
Err(err) => {
worker.log(format!("detected changed file {:?} - {}", path, err));
}
}
}
ArchiveType::Blob => {
let mut tmpfile = std::fs::File::open(&path)?;
let (csum, size) = compute_file_csum(&mut tmpfile)?;
match manifest.verify_file(&item.filename, &csum, size) {
Ok(_) => continue,
Err(err) => {
worker.log(format!("detected changed file {:?} - {}", path, err));
}
}
}
}
}
pull_single_archive(
worker,
&reader,
&mut chunk_reader,
tgt_store.clone(),
snapshot,
&item.filename,
).await?;
}
if let Err(err) = std::fs::rename(&tmp_manifest_name, &manifest_name) {
bail!("Atomic rename file {:?} failed - {}", manifest_name, err);
}
// cleanup - remove stale files
tgt_store.cleanup_backup_dir(snapshot, &manifest)?;
Ok(())
}
pub async fn pull_snapshot_from(
worker: &WorkerTask,
reader: Arc<BackupReader>,
tgt_store: Arc<DataStore>,
snapshot: &BackupDir,
) -> Result<(), Error> {
let (_path, is_new) = tgt_store.create_backup_dir(&snapshot)?;
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(cleanup_err) = tgt_store.remove_backup_dir(&snapshot) {
worker.log(format!("cleanup error - {}", cleanup_err));
}
return Err(err);
}
} else {
worker.log(format!("re-sync snapshot {:?}", snapshot.relative_path()));
pull_snapshot(worker, reader, tgt_store.clone(), &snapshot).await?
}
Ok(())
}
pub async fn pull_group(
worker: &WorkerTask,
client: &HttpClient,
src_repo: &BackupRepository,
tgt_store: Arc<DataStore>,
group: &BackupGroup,
pub fn check_pull_privs(
username: &str,
store: &str,
remote: &str,
remote_store: &str,
delete: bool,
) -> Result<(), Error> {
let path = format!("api2/json/admin/datastore/{}/snapshots", src_repo.store());
let user_info = CachedUserInfo::new()?;
let args = json!({
"backup-type": group.backup_type(),
"backup-id": group.backup_id(),
});
let mut result = client.get(&path, Some(args)).await?;
let mut list: Vec<SnapshotListItem> = serde_json::from_value(result["data"].take())?;
list.sort_unstable_by(|a, b| a.backup_time.cmp(&b.backup_time));
let auth_info = client.login().await?;
let fingerprint = client.fingerprint();
let last_sync = tgt_store.last_successful_backup(group)?;
let mut remote_snapshots = std::collections::HashSet::new();
for item in list {
let backup_time = Utc.timestamp(item.backup_time, 0);
remote_snapshots.insert(backup_time);
if let Some(last_sync_time) = last_sync {
if last_sync_time > backup_time { continue; }
}
let options = HttpClientOptions::new()
.password(Some(auth_info.ticket.clone()))
.fingerprint(fingerprint.clone());
let new_client = HttpClient::new(src_repo.host(), src_repo.user(), options)?;
let reader = BackupReader::start(
new_client,
None,
src_repo.store(),
&item.backup_type,
&item.backup_id,
backup_time,
true,
).await?;
let snapshot = BackupDir::new(item.backup_type, item.backup_id, item.backup_time);
pull_snapshot_from(worker, reader, tgt_store.clone(), &snapshot).await?;
}
user_info.check_privs(username, &["datastore", store], PRIV_DATASTORE_BACKUP, false)?;
user_info.check_privs(username, &["remote", remote, remote_store], PRIV_REMOTE_READ, false)?;
if delete {
let local_list = group.list_backups(&tgt_store.base_path())?;
for info in local_list {
let backup_time = info.backup_dir.backup_time();
if remote_snapshots.contains(&backup_time) { continue; }
worker.log(format!("delete vanished snapshot {:?}", info.backup_dir.relative_path()));
tgt_store.remove_backup_dir(&info.backup_dir)?;
}
user_info.check_privs(username, &["datastore", store], PRIV_DATASTORE_PRUNE, false)?;
}
Ok(())
}
pub async fn pull_store(
worker: &WorkerTask,
client: &HttpClient,
src_repo: &BackupRepository,
tgt_store: Arc<DataStore>,
delete: bool,
) -> Result<(), Error> {
pub async fn get_pull_parameters(
store: &str,
remote: &str,
remote_store: &str,
) -> Result<(HttpClient, BackupRepository, Arc<DataStore>), Error> {
let path = format!("api2/json/admin/datastore/{}/groups", src_repo.store());
let tgt_store = DataStore::lookup_datastore(store)?;
let mut result = client.get(&path, None).await?;
let (remote_config, _digest) = remote::config()?;
let remote: remote::Remote = remote_config.lookup("remote", remote)?;
let mut list: Vec<GroupListItem> = serde_json::from_value(result["data"].take())?;
let options = HttpClientOptions::new()
.password(Some(remote.password.clone()))
.fingerprint(remote.fingerprint.clone());
list.sort_unstable_by(|a, b| {
let type_order = a.backup_type.cmp(&b.backup_type);
if type_order == std::cmp::Ordering::Equal {
a.backup_id.cmp(&b.backup_id)
} else {
type_order
}
});
let client = HttpClient::new(&remote.host, &remote.userid, 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 mut errors = false;
let src_repo = BackupRepository::new(Some(remote.userid), Some(remote.host), remote_store.to_string());
let mut new_groups = std::collections::HashSet::new();
for item in list {
let group = BackupGroup::new(&item.backup_type, &item.backup_id);
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;
// do not stop here, instead continue
}
new_groups.insert(group);
}
if delete {
let result: Result<(), Error> = proxmox::try_block!({
let local_groups = BackupGroup::list_groups(&tgt_store.base_path())?;
for local_group in local_groups {
if new_groups.contains(&local_group) { continue; }
worker.log(format!("delete vanished group '{}/{}'", local_group.backup_type(), local_group.backup_id()));
if let Err(err) = tgt_store.remove_backup_group(&local_group) {
worker.log(err.to_string());
errors = true;
}
}
Ok(())
});
if let Err(err) = result {
worker.log(format!("error during cleanup: {}", err));
errors = true;
};
}
if errors {
bail!("sync failed with some errors.");
}
Ok(())
Ok((client, src_repo, tgt_store))
}
#[api(
@ -379,54 +74,44 @@ pub async fn pull_store(
"remote-store": {
schema: DATASTORE_SCHEMA,
},
delete: {
description: "Delete vanished backups. This remove the local copy if the remote backup was deleted.",
type: Boolean,
"remove-vanished": {
schema: REMOVE_VANISHED_BACKUPS_SCHEMA,
optional: true,
default: true,
},
},
},
access: {
// Note: used parameters are no uri parameters, so we need to test inside function body
description: r###"The user needs Datastore.Backup privilege on '/datastore/{store}',
and needs to own the backup group. Remote.Read is required on '/remote/{remote}/{remote-store}'.
The delete flag additionally requires the Datastore.Prune privilege on '/datastore/{store}'.
"###,
permission: &Permission::Anybody,
},
)]
/// Sync store from other repository
async fn pull (
store: String,
remote: String,
remote_store: String,
delete: Option<bool>,
remove_vanished: Option<bool>,
_info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<String, Error> {
let username = rpcenv.get_user().unwrap();
let delete = remove_vanished.unwrap_or(true);
let delete = delete.unwrap_or(true);
check_pull_privs(&username, &store, &remote, &remote_store, delete)?;
let tgt_store = DataStore::lookup_datastore(&store)?;
let (remote_config, _digest) = remote::config()?;
let remote: remote::Remote = remote_config.lookup("remote", &remote)?;
let options = HttpClientOptions::new()
.password(Some(remote.password.clone()))
.fingerprint(remote.fingerprint.clone());
let client = HttpClient::new(&remote.host, &remote.userid, 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);
let (client, src_repo, tgt_store) = get_pull_parameters(&store, &remote, &remote_store).await?;
// fixme: set to_stdout to false?
let upid_str = WorkerTask::spawn("sync", Some(store.clone()), &username.clone(), true, move |worker| async move {
worker.log(format!("sync datastore '{}' start", store));
// explicit create shared lock to prevent GC on newly created chunks
let _shared_store_lock = tgt_store.try_shared_chunk_store_lock()?;
pull_store(&worker, &client, &src_repo, tgt_store.clone(), delete).await?;
pull_store(&worker, &client, &src_repo, tgt_store.clone(), delete, username).await?;
worker.log(format!("sync datastore '{}' end", store));

View File

@ -1,5 +1,5 @@
//use chrono::{Local, TimeZone};
use failure::*;
use anyhow::{bail, format_err, Error};
use futures::*;
use hyper::header::{self, HeaderValue, UPGRADE};
use hyper::http::request::Parts;
@ -7,7 +7,7 @@ use hyper::{Body, Response, StatusCode};
use serde_json::Value;
use proxmox::{sortable, identity};
use proxmox::api::{ApiResponseFuture, ApiHandler, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{ApiResponseFuture, ApiHandler, ApiMethod, Router, RpcEnvironment, Permission};
use proxmox::api::schema::*;
use proxmox::http_err;
@ -15,6 +15,9 @@ use crate::api2::types::*;
use crate::backup::*;
use crate::server::{WorkerTask, H2Service};
use crate::tools;
use crate::config::acl::PRIV_DATASTORE_READ;
use crate::config::cached_user_info::CachedUserInfo;
use crate::api2::helpers;
mod environment;
use environment::*;
@ -29,18 +32,16 @@ pub const API_METHOD_UPGRADE_BACKUP: ApiMethod = ApiMethod::new(
concat!("Upgraded to backup protocol ('", PROXMOX_BACKUP_READER_PROTOCOL_ID_V1!(), "')."),
&sorted!([
("store", false, &DATASTORE_SCHEMA),
("backup-type", false, &StringSchema::new("Backup type.")
.format(&ApiStringFormat::Enum(&["vm", "ct", "host"]))
.schema()
),
("backup-id", false, &StringSchema::new("Backup ID.").schema()),
("backup-time", false, &IntegerSchema::new("Backup time (Unix epoch.)")
.minimum(1_547_797_308)
.schema()
),
("backup-type", false, &BACKUP_TYPE_SCHEMA),
("backup-id", false, &BACKUP_ID_SCHEMA),
("backup-time", false, &BACKUP_TIME_SCHEMA),
("debug", true, &BooleanSchema::new("Enable verbose debug logging.").schema()),
]),
)
).access(
// Note: parameter 'store' is no uri parameter, so we need to test inside function body
Some("The user needs Datastore.Read privilege on /datastore/{store}."),
&Permission::Anybody
);
fn upgrade_to_backup_reader_protocol(
@ -54,7 +55,12 @@ fn upgrade_to_backup_reader_protocol(
async move {
let debug = param["debug"].as_bool().unwrap_or(false);
let username = rpcenv.get_user().unwrap();
let store = tools::required_string_param(&param, "store")?.to_owned();
let user_info = CachedUserInfo::new()?;
user_info.check_privs(&username, &["datastore", &store], PRIV_DATASTORE_READ, false)?;
let datastore = DataStore::lookup_datastore(&store)?;
let backup_type = tools::required_string_param(&param, "backup-type")?;
@ -75,7 +81,6 @@ fn upgrade_to_backup_reader_protocol(
bail!("unexpected http version '{:?}' (expected version < 2)", parts.version);
}
let username = rpcenv.get_user().unwrap();
let env_type = rpcenv.env_type();
let backup_dir = BackupDir::new(backup_type, backup_id, backup_time);
@ -127,7 +132,7 @@ fn upgrade_to_backup_reader_protocol(
Either::Right((Ok(res), _)) => Ok(res),
Either::Right((Err(err), _)) => Err(err),
})
.map_ok(move |_| env.log("reader finished sucessfully"))
.map_ok(move |_| env.log("reader finished successfully"))
})?;
let response = Response::builder()
@ -183,26 +188,9 @@ fn download_file(
path.push(env.backup_dir.relative_path());
path.push(&file_name);
let path2 = path.clone();
let path3 = path.clone();
env.log(format!("download {:?}", path.clone()));
let file = tokio::fs::File::open(path)
.map_err(move |err| http_err!(BAD_REQUEST, format!("open file {:?} failed: {}", path2, err)))
.await?;
env.log(format!("download {:?}", path3));
let payload = tokio_util::codec::FramedRead::new(file, tokio_util::codec::BytesCodec::new())
.map_ok(|bytes| hyper::body::Bytes::from(bytes.freeze()));
let body = Body::wrap_stream(payload);
// fixme: set other headers ?
Ok(Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "application/octet-stream")
.body(body)
.unwrap())
helpers::create_download_response(path).await
}.boxed()
}

View File

@ -1,8 +1,7 @@
//use failure::*;
//use anyhow::{bail, format_err, Error};
use std::sync::Arc;
use std::collections::HashMap;
use serde_json::Value;
use serde_json::{json, Value};
use proxmox::api::{RpcEnvironment, RpcEnvironmentType};
@ -16,7 +15,7 @@ use crate::server::formatter::*;
#[derive(Clone)]
pub struct ReaderEnvironment {
env_type: RpcEnvironmentType,
result_attributes: HashMap<String, Value>,
result_attributes: Value,
user: String,
pub debug: bool,
pub formatter: &'static OutputFormatter,
@ -37,7 +36,7 @@ impl ReaderEnvironment {
Self {
result_attributes: HashMap::new(),
result_attributes: json!({}),
env_type,
user,
worker,
@ -61,12 +60,12 @@ impl ReaderEnvironment {
impl RpcEnvironment for ReaderEnvironment {
fn set_result_attrib(&mut self, name: &str, value: Value) {
self.result_attributes.insert(name.into(), value);
fn result_attrib_mut(&mut self) -> &mut Value {
&mut self.result_attributes
}
fn get_result_attrib(&self, name: &str) -> Option<&Value> {
self.result_attributes.get(name)
fn result_attrib(&self) -> &Value {
&self.result_attributes
}
fn env_type(&self) -> RpcEnvironmentType {

226
src/api2/status.rs Normal file
View File

@ -0,0 +1,226 @@
use proxmox::list_subdirs_api_method;
use anyhow::{Error};
use serde_json::{json, Value};
use proxmox::api::{
api,
ApiMethod,
Permission,
Router,
RpcEnvironment,
SubdirMap,
UserInformation,
};
use crate::api2::types::{
DATASTORE_SCHEMA,
RRDMode,
RRDTimeFrameResolution,
TaskListItem
};
use crate::server;
use crate::backup::{DataStore};
use crate::config::datastore;
use crate::tools::epoch_now_f64;
use crate::tools::statistics::{linear_regression};
use crate::config::cached_user_info::CachedUserInfo;
use crate::config::acl::{
PRIV_SYS_AUDIT,
PRIV_DATASTORE_AUDIT,
PRIV_DATASTORE_BACKUP,
};
#[api(
returns: {
description: "Lists the Status of the Datastores.",
type: Array,
items: {
description: "Status of a Datastore",
type: Object,
properties: {
store: {
schema: DATASTORE_SCHEMA,
},
total: {
type: Integer,
description: "The Size of the underlying storage in bytes",
},
used: {
type: Integer,
description: "The used bytes of the underlying storage",
},
avail: {
type: Integer,
description: "The available bytes of the underlying storage",
},
history: {
type: Array,
description: "A list of usages of the past (last Month).",
items: {
type: Number,
description: "The usage of a time in the past. Either null or between 0.0 and 1.0.",
}
},
"estimated-full-date": {
type: Integer,
optional: true,
description: "Estimation of the UNIX epoch when the storage will be full.\
This is calculated via a simple Linear Regression (Least Squares)\
of RRD data of the last Month. Missing if there are not enough data points yet.\
If the estimate lies in the past, the usage is decreasing.",
},
},
},
},
)]
/// List Datastore usages and estimates
fn datastore_status(
_param: Value,
_info: &ApiMethod,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
let (config, _digest) = datastore::config()?;
let username = rpcenv.get_user().unwrap();
let user_info = CachedUserInfo::new()?;
let mut list = Vec::new();
for (store, (_, _)) in &config.sections {
let user_privs = user_info.lookup_privs(&username, &["datastore", &store]);
let allowed = (user_privs & (PRIV_DATASTORE_AUDIT| PRIV_DATASTORE_BACKUP)) != 0;
if !allowed {
continue;
}
let datastore = DataStore::lookup_datastore(&store)?;
let status = crate::tools::disks::disk_usage(&datastore.base_path())?;
let mut entry = json!({
"store": store,
"total": status.total,
"used": status.used,
"avail": status.avail,
});
let rrd_dir = format!("datastore/{}", store);
let now = epoch_now_f64()?;
let rrd_resolution = RRDTimeFrameResolution::Month;
let rrd_mode = RRDMode::Average;
let total_res = crate::rrd::extract_cached_data(
&rrd_dir,
"total",
now,
rrd_resolution,
rrd_mode,
);
let used_res = crate::rrd::extract_cached_data(
&rrd_dir,
"used",
now,
rrd_resolution,
rrd_mode,
);
match (total_res, used_res) {
(Some((start, reso, total_list)), Some((_, _, used_list))) => {
let mut usage_list: Vec<f64> = Vec::new();
let mut time_list: Vec<u64> = Vec::new();
let mut history = Vec::new();
for (idx, used) in used_list.iter().enumerate() {
let total = if idx < total_list.len() {
total_list[idx]
} else {
None
};
match (total, used) {
(Some(total), Some(used)) if total != 0.0 => {
time_list.push(start + (idx as u64)*reso);
let usage = used/total;
usage_list.push(usage);
history.push(json!(usage));
},
_ => {
history.push(json!(null))
}
}
}
entry["history"] = history.into();
// we skip the calculation for datastores with not enough data
if usage_list.len() >= 7 {
if let Some((a,b)) = linear_regression(&time_list, &usage_list) {
if b != 0.0 {
let estimate = (1.0 - a) / b;
entry["estimated-full-date"] = Value::from(estimate.floor() as u64);
}
}
}
},
_ => {},
}
list.push(entry);
}
Ok(list.into())
}
#[api(
input: {
properties: {
since: {
type: u64,
description: "Only list tasks since this UNIX epoch.",
optional: true,
},
},
},
returns: {
description: "A list of tasks.",
type: Array,
items: { type: TaskListItem },
},
access: {
description: "Users can only see there own tasks, unless the have Sys.Audit on /system/tasks.",
permission: &Permission::Anybody,
},
)]
/// List tasks.
pub fn list_tasks(
_param: Value,
rpcenv: &mut dyn RpcEnvironment,
) -> Result<Vec<TaskListItem>, Error> {
let username = rpcenv.get_user().unwrap();
let user_info = CachedUserInfo::new()?;
let user_privs = user_info.lookup_privs(&username, &["system", "tasks"]);
let list_all = (user_privs & PRIV_SYS_AUDIT) != 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 == username)
.collect();
Ok(list.into())
}
const SUBDIRS: SubdirMap = &[
("datastore-usage", &Router::new().get(&API_METHOD_DATASTORE_STATUS)),
("tasks", &Router::new().get(&API_METHOD_LIST_TASKS)),
];
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
.subdirs(SUBDIRS);

View File

@ -1,16 +1,39 @@
use failure::*;
use anyhow::{Error};
use serde_json::{json, Value};
use proxmox::api::{ApiHandler, ApiMethod, Router, RpcEnvironment};
use proxmox::api::schema::ObjectSchema;
use proxmox::api::{api, Router, Permission};
use crate::tools;
use crate::config::acl::PRIV_SYS_AUDIT;
fn get_subscription(
_param: Value,
_info: &ApiMethod,
_rpcenv: &mut dyn RpcEnvironment,
) -> Result<Value, Error> {
#[api(
returns: {
description: "Subscription status.",
properties: {
status: {
type: String,
description: "'NotFound', 'active' or 'inactive'."
},
message: {
type: String,
description: "Human readable problem description.",
},
serverid: {
type: String,
description: "The unique server ID.",
},
url: {
type: String,
description: "URL to Web Shop.",
},
},
},
access: {
permission: &Permission::Privilege(&[], PRIV_SYS_AUDIT, false),
},
)]
/// Read subscription info.
fn get_subscription(_param: Value) -> Result<Value, Error> {
let url = "https://www.proxmox.com/en/proxmox-backup-server/pricing";
Ok(json!({
@ -22,9 +45,4 @@ fn get_subscription(
}
pub const ROUTER: Router = Router::new()
.get(
&ApiMethod::new(
&ApiHandler::Sync(&get_subscription),
&ObjectSchema::new("Read subscription info.", &[])
)
);
.get(&API_METHOD_GET_SUBSCRIPTION);

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail};
use ::serde::{Deserialize, Serialize};
use proxmox::api::{api, schema::*};
@ -25,11 +25,24 @@ macro_rules! DNS_NAME { () => (concat!(r"(?:", DNS_LABEL!() , r"\.)*", DNS_LABEL
// slash is not allowed because it is used as pve API delimiter
// also see "man useradd"
macro_rules! USER_NAME_REGEX_STR { () => (r"(?:[^\s:/[:cntrl:]]+)") }
macro_rules! GROUP_NAME_REGEX_STR { () => (USER_NAME_REGEX_STR!()) }
macro_rules! USER_ID_REGEX_STR { () => (concat!(USER_NAME_REGEX_STR!(), r"@", PROXMOX_SAFE_ID_REGEX_STR!())) }
#[macro_export]
macro_rules! PROXMOX_SAFE_ID_REGEX_STR { () => (r"(?:[A-Za-z0-9_][A-Za-z0-9._\-]*)") }
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})$")) }
const_regex!{
pub IP_FORMAT_REGEX = IPRE!();
pub IP_V4_REGEX = concat!(r"^", IPV4RE!(), r"$");
pub IP_V6_REGEX = concat!(r"^", IPV6RE!(), r"$");
pub IP_REGEX = concat!(r"^", IPRE!(), r"$");
pub CIDR_V4_REGEX = concat!(r"^", CIDR_V4_REGEX_STR!(), r"$");
pub CIDR_V6_REGEX = concat!(r"^", CIDR_V6_REGEX_STR!(), r"$");
pub CIDR_REGEX = concat!(r"^(?:", CIDR_V4_REGEX_STR!(), "|", CIDR_V6_REGEX_STR!(), r")$");
pub SHA256_HEX_REGEX = r"^[a-f0-9]{64}$"; // fixme: define in common_regex ?
pub SYSTEMD_DATETIME_REGEX = r"^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}(:\d{2})?)?$"; // fixme: define in common_regex ?
@ -52,16 +65,30 @@ const_regex!{
pub DNS_NAME_OR_IP_REGEX = concat!(r"^", DNS_NAME!(), "|", IPRE!(), r"$");
pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_NAME_REGEX_STR!(), r"@", PROXMOX_SAFE_ID_REGEX_STR!(), r"$");
pub PROXMOX_USER_ID_REGEX = concat!(r"^", USER_ID_REGEX_STR!(), r"$");
pub BACKUP_REPO_URL_REGEX = concat!(r"^^(?:(?:(", USER_ID_REGEX_STR!(), ")@)?(", DNS_NAME!(), "|", IPRE!() ,"):)?(", PROXMOX_SAFE_ID_REGEX_STR!(), r")$");
pub PROXMOX_GROUP_ID_REGEX = concat!(r"^", GROUP_NAME_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}$";
pub ACL_PATH_REGEX = concat!(r"^(?:/|", r"(?:/", PROXMOX_SAFE_ID_REGEX_STR!(), ")+", r")$");
pub BLOCKDEVICE_NAME_REGEX = r"^(:?(:?h|s|x?v)d[a-z]+)|(:?nvme\d+n\d+)$";
}
pub const SYSTEMD_DATETIME_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&SYSTEMD_DATETIME_REGEX);
pub const IP_V4_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&IP_V4_REGEX);
pub const IP_V6_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&IP_V6_REGEX);
pub const IP_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&IP_FORMAT_REGEX);
ApiStringFormat::Pattern(&IP_REGEX);
pub const PVE_CONFIG_DIGEST_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&SHA256_HEX_REGEX);
@ -87,10 +114,41 @@ pub const DNS_NAME_OR_IP_FORMAT: ApiStringFormat =
pub const PROXMOX_USER_ID_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_USER_ID_REGEX);
pub const PROXMOX_GROUP_ID_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_GROUP_ID_REGEX);
pub const PASSWORD_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PASSWORD_REGEX);
pub const ACL_PATH_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&ACL_PATH_REGEX);
pub const NETWORK_INTERFACE_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&PROXMOX_SAFE_ID_REGEX);
pub const CIDR_V4_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&CIDR_V4_REGEX);
pub const CIDR_V6_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&CIDR_V6_REGEX);
pub const CIDR_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&CIDR_REGEX);
pub const BLOCKDEVICE_NAME_FORMAT: ApiStringFormat =
ApiStringFormat::Pattern(&BLOCKDEVICE_NAME_REGEX);
pub const PASSWORD_SCHEMA: Schema = StringSchema::new("Password.")
.format(&PASSWORD_FORMAT)
.min_length(1)
.max_length(1024)
.schema();
pub const PBS_PASSWORD_SCHEMA: Schema = StringSchema::new("User Password.")
.format(&PASSWORD_FORMAT)
.min_length(5)
.max_length(64)
.schema();
pub const CERT_FINGERPRINT_SHA256_SCHEMA: Schema = StringSchema::new(
"X509 certificate fingerprint (sha256)."
@ -142,6 +200,68 @@ pub const THIRD_DNS_SERVER_SCHEMA: Schema =
.format(&IP_FORMAT)
.schema();
pub const IP_V4_SCHEMA: Schema =
StringSchema::new("IPv4 address.")
.format(&IP_V4_FORMAT)
.max_length(15)
.schema();
pub const IP_V6_SCHEMA: Schema =
StringSchema::new("IPv6 address.")
.format(&IP_V6_FORMAT)
.max_length(39)
.schema();
pub const IP_SCHEMA: Schema =
StringSchema::new("IP (IPv4 or IPv6) address.")
.format(&IP_FORMAT)
.max_length(39)
.schema();
pub const CIDR_V4_SCHEMA: Schema =
StringSchema::new("IPv4 address with netmask (CIDR notation).")
.format(&CIDR_V4_FORMAT)
.max_length(18)
.schema();
pub const CIDR_V6_SCHEMA: Schema =
StringSchema::new("IPv6 address with netmask (CIDR notation).")
.format(&CIDR_V6_FORMAT)
.max_length(43)
.schema();
pub const CIDR_SCHEMA: Schema =
StringSchema::new("IP address (IPv4 or IPv6) with netmask (CIDR notation).")
.format(&CIDR_FORMAT)
.max_length(43)
.schema();
pub const TIME_ZONE_SCHEMA: Schema = StringSchema::new(
"Time zone. The file '/usr/share/zoneinfo/zone.tab' contains the list of valid names.")
.format(&SINGLE_LINE_COMMENT_FORMAT)
.min_length(2)
.max_length(64)
.schema();
pub const ACL_PATH_SCHEMA: Schema = StringSchema::new(
"Access control path.")
.format(&ACL_PATH_FORMAT)
.min_length(1)
.max_length(128)
.schema();
pub const ACL_PROPAGATE_SCHEMA: Schema = BooleanSchema::new(
"Allow to propagate (inherit) permissions.")
.default(true)
.schema();
pub const ACL_UGID_TYPE_SCHEMA: Schema = StringSchema::new(
"Type of 'ugid' property.")
.format(&ApiStringFormat::Enum(&[
EnumEntry::new("user", "User"),
EnumEntry::new("group", "Group")]))
.schema();
pub const BACKUP_ARCHIVE_NAME_SCHEMA: Schema =
StringSchema::new("Backup archive name.")
.format(&PROXMOX_SAFE_ID_FORMAT)
@ -149,7 +269,10 @@ pub const BACKUP_ARCHIVE_NAME_SCHEMA: Schema =
pub const BACKUP_TYPE_SCHEMA: Schema =
StringSchema::new("Backup type.")
.format(&ApiStringFormat::Enum(&["vm", "ct", "host"]))
.format(&ApiStringFormat::Enum(&[
EnumEntry::new("vm", "Virtual Machine Backup"),
EnumEntry::new("ct", "Container Backup"),
EnumEntry::new("host", "Host Backup")]))
.schema();
pub const BACKUP_ID_SCHEMA: Schema =
@ -172,12 +295,38 @@ pub const DATASTORE_SCHEMA: Schema = StringSchema::new("Datastore name.")
.max_length(32)
.schema();
pub const SYNC_SCHEDULE_SCHEMA: Schema = StringSchema::new(
"Run sync job at specified schedule.")
.format(&ApiStringFormat::VerifyFn(crate::tools::systemd::time::verify_calendar_event))
.schema();
pub const GC_SCHEDULE_SCHEMA: Schema = StringSchema::new(
"Run garbage collection job at specified schedule.")
.format(&ApiStringFormat::VerifyFn(crate::tools::systemd::time::verify_calendar_event))
.schema();
pub const PRUNE_SCHEDULE_SCHEMA: Schema = StringSchema::new(
"Run prune job at specified schedule.")
.format(&ApiStringFormat::VerifyFn(crate::tools::systemd::time::verify_calendar_event))
.schema();
pub const REMOTE_ID_SCHEMA: Schema = StringSchema::new("Remote ID.")
.format(&PROXMOX_SAFE_ID_FORMAT)
.min_length(3)
.max_length(32)
.schema();
pub const JOB_ID_SCHEMA: Schema = StringSchema::new("Job ID.")
.format(&PROXMOX_SAFE_ID_FORMAT)
.min_length(3)
.max_length(32)
.schema();
pub const REMOVE_VANISHED_BACKUPS_SCHEMA: Schema = BooleanSchema::new(
"Delete vanished backups. This remove the local copy if the remote backup was deleted.")
.default(true)
.schema();
pub const SINGLE_LINE_COMMENT_SCHEMA: Schema = StringSchema::new("Comment (single line).")
.format(&SINGLE_LINE_COMMENT_FORMAT)
.schema();
@ -202,6 +351,17 @@ pub const PROXMOX_USER_ID_SCHEMA: Schema = StringSchema::new("User ID")
.max_length(64)
.schema();
pub const PROXMOX_GROUP_ID_SCHEMA: Schema = StringSchema::new("Group ID")
.format(&PROXMOX_GROUP_ID_FORMAT)
.min_length(3)
.max_length(64)
.schema();
pub const BLOCKDEVICE_NAME_SCHEMA: Schema = StringSchema::new("Block device name (/sys/block/<name>).")
.format(&BLOCKDEVICE_NAME_FORMAT)
.min_length(3)
.max_length(64)
.schema();
// Complex type definitions
@ -237,6 +397,9 @@ pub struct GroupListItem {
pub backup_count: u64,
/// List of contained archive files.
pub files: Vec<String>,
/// The owner of group
#[serde(skip_serializing_if="Option::is_none")]
pub owner: Option<String>,
}
#[api(
@ -265,12 +428,69 @@ pub struct SnapshotListItem {
pub backup_id: String,
pub backup_time: i64,
/// List of contained archive files.
pub files: Vec<String>,
pub files: Vec<BackupContent>,
/// Overall snapshot size (sum of all archive sizes).
#[serde(skip_serializing_if="Option::is_none")]
pub size: Option<u64>,
/// The owner of the snapshots group
#[serde(skip_serializing_if="Option::is_none")]
pub owner: Option<String>,
}
#[api(
properties: {
"backup-type": {
schema: BACKUP_TYPE_SCHEMA,
},
"backup-id": {
schema: BACKUP_ID_SCHEMA,
},
"backup-time": {
schema: BACKUP_TIME_SCHEMA,
},
},
)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all="kebab-case")]
/// Prune result.
pub struct PruneListItem {
pub backup_type: String, // enum
pub backup_id: String,
pub backup_time: i64,
/// Keep snapshot
pub keep: bool,
}
pub const PRUNE_SCHEMA_KEEP_DAILY: Schema = IntegerSchema::new(
"Number of daily backups to keep.")
.minimum(1)
.schema();
pub const PRUNE_SCHEMA_KEEP_HOURLY: Schema = IntegerSchema::new(
"Number of hourly backups to keep.")
.minimum(1)
.schema();
pub const PRUNE_SCHEMA_KEEP_LAST: Schema = IntegerSchema::new(
"Number of backups to keep.")
.minimum(1)
.schema();
pub const PRUNE_SCHEMA_KEEP_MONTHLY: Schema = IntegerSchema::new(
"Number of monthly backups to keep.")
.minimum(1)
.schema();
pub const PRUNE_SCHEMA_KEEP_WEEKLY: Schema = IntegerSchema::new(
"Number of weekly backups to keep.")
.minimum(1)
.schema();
pub const PRUNE_SCHEMA_KEEP_YEARLY: Schema = IntegerSchema::new(
"Number of yearly backups to keep.")
.minimum(1)
.schema();
#[api(
properties: {
"filename": {
@ -283,6 +503,9 @@ pub struct SnapshotListItem {
/// Basic information about archive files inside a backup snapshot.
pub struct BackupContent {
pub filename: String,
/// Info if file is encrypted (or empty if we do not have that info)
#[serde(skip_serializing_if="Option::is_none")]
pub encrypted: Option<bool>,
/// Archive size (from backup manifest).
#[serde(skip_serializing_if="Option::is_none")]
pub size: Option<u64>,
@ -313,6 +536,10 @@ pub struct GarbageCollectionStatus {
pub removed_bytes: u64,
/// Number of removed chunks.
pub removed_chunks: usize,
/// Sum of pending bytes (pending removal - kept for safety).
pub pending_bytes: u64,
/// Number of pending chunks (pending removal - kept for safety).
pub pending_chunks: usize,
}
impl Default for GarbageCollectionStatus {
@ -325,6 +552,8 @@ impl Default for GarbageCollectionStatus {
disk_chunks: 0,
removed_bytes: 0,
removed_chunks: 0,
pending_bytes: 0,
pending_chunks: 0,
}
}
}
@ -373,10 +602,245 @@ pub struct TaskListItem {
pub status: Option<String>,
}
impl From<crate::server::TaskListInfo> for TaskListItem {
fn from(info: crate::server::TaskListInfo) -> Self {
let (endtime, status) = info
.state
.map_or_else(|| (None, None), |(a,b)| (Some(a), Some(b)));
TaskListItem {
upid: info.upid_str,
node: "localhost".to_string(),
pid: info.upid.pid as i64,
pstart: info.upid.pstart,
starttime: info.upid.starttime,
worker_type: info.upid.worker_type,
worker_id: info.upid.worker_id,
user: info.upid.username,
endtime,
status,
}
}
}
#[api()]
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// Node Power command type.
pub enum NodePowerCommand {
/// Restart the server
Reboot,
/// Shutdown the server
Shutdown,
}
#[api()]
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// Interface configuration method
pub enum NetworkConfigMethod {
/// Configuration is done manually using other tools
Manual,
/// Define interfaces with statically allocated addresses.
Static,
/// Obtain an address via DHCP
DHCP,
/// Define the loopback interface.
Loopback,
}
#[api()]
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[allow(non_camel_case_types)]
#[repr(u8)]
/// Linux Bond Mode
pub enum LinuxBondMode {
/// Round-robin policy
balance_rr = 0,
/// Active-backup policy
active_backup = 1,
/// XOR policy
balance_xor = 2,
/// Broadcast policy
broadcast = 3,
/// IEEE 802.3ad Dynamic link aggregation
//#[serde(rename = "802.3ad")]
ieee802_3ad = 4,
/// Adaptive transmit load balancing
balance_tlb = 5,
/// Adaptive load balancing
balance_alb = 6,
}
#[api()]
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
/// Network interface type
pub enum NetworkInterfaceType {
/// Loopback
Loopback,
/// Physical Ethernet device
Eth,
/// Linux Bridge
Bridge,
/// Linux Bond
Bond,
/// Linux VLAN (eth.10)
Vlan,
/// Interface Alias (eth:1)
Alias,
/// Unknown interface type
Unknown,
}
pub const NETWORK_INTERFACE_NAME_SCHEMA: Schema = StringSchema::new("Network interface name.")
.format(&NETWORK_INTERFACE_FORMAT)
.min_length(1)
.max_length(libc::IFNAMSIZ-1)
.schema();
pub const NETWORK_INTERFACE_ARRAY_SCHEMA: Schema = ArraySchema::new(
"Network interface list.", &NETWORK_INTERFACE_NAME_SCHEMA)
.schema();
pub const NETWORK_INTERFACE_LIST_SCHEMA: Schema = StringSchema::new(
"A list of network devices, comma separated.")
.format(&ApiStringFormat::PropertyString(&NETWORK_INTERFACE_ARRAY_SCHEMA))
.schema();
#[api(
properties: {
name: {
schema: NETWORK_INTERFACE_NAME_SCHEMA,
},
"type": {
type: NetworkInterfaceType,
},
method: {
type: NetworkConfigMethod,
optional: true,
},
method6: {
type: NetworkConfigMethod,
optional: true,
},
cidr: {
schema: CIDR_V4_SCHEMA,
optional: true,
},
cidr6: {
schema: CIDR_V6_SCHEMA,
optional: true,
},
gateway: {
schema: IP_V4_SCHEMA,
optional: true,
},
gateway6: {
schema: IP_V6_SCHEMA,
optional: true,
},
options: {
description: "Option list (inet)",
type: Array,
items: {
description: "Optional attribute line.",
type: String,
},
},
options6: {
description: "Option list (inet6)",
type: Array,
items: {
description: "Optional attribute line.",
type: String,
},
},
comments: {
description: "Comments (inet, may span multiple lines)",
type: String,
optional: true,
},
comments6: {
description: "Comments (inet6, may span multiple lines)",
type: String,
optional: true,
},
bridge_ports: {
schema: NETWORK_INTERFACE_ARRAY_SCHEMA,
optional: true,
},
slaves: {
schema: NETWORK_INTERFACE_ARRAY_SCHEMA,
optional: true,
},
bond_mode: {
type: LinuxBondMode,
optional: true,
}
}
)]
#[derive(Debug, Serialize, Deserialize)]
/// Network Interface configuration
pub struct Interface {
/// Autostart interface
#[serde(rename = "autostart")]
pub autostart: bool,
/// Interface is active (UP)
pub active: bool,
/// Interface name
pub name: String,
/// Interface type
#[serde(rename = "type")]
pub interface_type: NetworkInterfaceType,
#[serde(skip_serializing_if="Option::is_none")]
pub method: Option<NetworkConfigMethod>,
#[serde(skip_serializing_if="Option::is_none")]
pub method6: Option<NetworkConfigMethod>,
#[serde(skip_serializing_if="Option::is_none")]
/// IPv4 address with netmask
pub cidr: Option<String>,
#[serde(skip_serializing_if="Option::is_none")]
/// IPv4 gateway
pub gateway: Option<String>,
#[serde(skip_serializing_if="Option::is_none")]
/// IPv6 address with netmask
pub cidr6: Option<String>,
#[serde(skip_serializing_if="Option::is_none")]
/// IPv6 gateway
pub gateway6: Option<String>,
#[serde(skip_serializing_if="Vec::is_empty")]
pub options: Vec<String>,
#[serde(skip_serializing_if="Vec::is_empty")]
pub options6: Vec<String>,
#[serde(skip_serializing_if="Option::is_none")]
pub comments: Option<String>,
#[serde(skip_serializing_if="Option::is_none")]
pub comments6: Option<String>,
#[serde(skip_serializing_if="Option::is_none")]
/// Maximum Transmission Unit
pub mtu: Option<u64>,
#[serde(skip_serializing_if="Option::is_none")]
pub bridge_ports: Option<Vec<String>>,
/// Enable bridge vlan support.
#[serde(skip_serializing_if="Option::is_none")]
pub bridge_vlan_aware: Option<bool>,
#[serde(skip_serializing_if="Option::is_none")]
pub slaves: Option<Vec<String>>,
#[serde(skip_serializing_if="Option::is_none")]
pub bond_mode: Option<LinuxBondMode>,
}
// Regression tests
#[test]
fn test_cert_fingerprint_schema() -> Result<(), Error> {
fn test_cert_fingerprint_schema() -> Result<(), anyhow::Error> {
let schema = CERT_FINGERPRINT_SHA256_SCHEMA;
@ -391,7 +855,7 @@ fn test_cert_fingerprint_schema() -> Result<(), Error> {
for fingerprint in invalid_fingerprints.iter() {
if let Ok(_) = parse_simple_value(fingerprint, &schema) {
bail!("test fingerprint '{}' failed - got Ok() while expection an error.", fingerprint);
bail!("test fingerprint '{}' failed - got Ok() while exception an error.", fingerprint);
}
}
@ -417,7 +881,7 @@ fn test_cert_fingerprint_schema() -> Result<(), Error> {
}
#[test]
fn test_proxmox_user_id_schema() -> Result<(), Error> {
fn test_proxmox_user_id_schema() -> Result<(), anyhow::Error> {
let schema = PROXMOX_USER_ID_SCHEMA;
@ -435,7 +899,7 @@ fn test_proxmox_user_id_schema() -> Result<(), Error> {
for name in invalid_user_ids.iter() {
if let Ok(_) = parse_simple_value(name, &schema) {
bail!("test userid '{}' failed - got Ok() while expection an error.", name);
bail!("test userid '{}' failed - got Ok() while exception an error.", name);
}
}
@ -462,3 +926,31 @@ fn test_proxmox_user_id_schema() -> Result<(), Error> {
Ok(())
}
#[api()]
#[derive(Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum RRDMode {
/// Maximum
Max,
/// Average
Average,
}
#[api()]
#[repr(u64)]
#[derive(Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RRDTimeFrameResolution {
/// 1 min => last 70 minutes
Hour = 60,
/// 30 min => last 35 hours
Day = 60*30,
/// 3 hours => about 8 days
Week = 60*180,
/// 12 hours => last 35 days
Month = 60*720,
/// 1 week => last 490 days
Year = 60*10080,
}

View File

@ -1,7 +1,7 @@
use failure::*;
use anyhow::{Error};
use serde_json::{json, Value};
use proxmox::api::{ApiHandler, ApiMethod, Router, RpcEnvironment};
use proxmox::api::{ApiHandler, ApiMethod, Router, RpcEnvironment, Permission};
use proxmox::api::schema::ObjectSchema;
pub const PROXMOX_PKG_VERSION: &str =
@ -31,6 +31,6 @@ pub const ROUTER: Router = Router::new()
&ApiMethod::new(
&ApiHandler::Sync(&get_version),
&ObjectSchema::new("Proxmox Backup Server API version.", &[])
)
).access(None, &Permission::Anybody)
);

148
src/auth.rs Normal file
View File

@ -0,0 +1,148 @@
//! Proxmox Backup Server Authentication
//!
//! This library contains helper to authenticate users.
use std::process::{Command, Stdio};
use std::io::Write;
use std::ffi::{CString, CStr};
use base64;
use anyhow::{bail, format_err, Error};
use serde_json::json;
pub trait ProxmoxAuthenticator {
fn authenticate_user(&self, username: &str, password: &str) -> Result<(), Error>;
fn store_password(&self, username: &str, password: &str) -> Result<(), Error>;
}
pub struct PAM();
impl ProxmoxAuthenticator for PAM {
fn authenticate_user(&self, username: &str, password: &str) -> Result<(), Error> {
let mut auth = pam::Authenticator::with_password("proxmox-backup-auth").unwrap();
auth.get_handler().set_credentials(username, password);
auth.authenticate()?;
return Ok(());
}
fn store_password(&self, username: &str, password: &str) -> Result<(), Error> {
let mut child = Command::new("passwd")
.arg(username)
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.or_else(|err| Err(format_err!("unable to set password for '{}' - execute passwd failed: {}", username, err)))?;
// Note: passwd reads password twice from stdin (for verify)
writeln!(child.stdin.as_mut().unwrap(), "{}\n{}", password, password)?;
let output = child.wait_with_output()
.or_else(|err| Err(format_err!("unable to set password for '{}' - wait failed: {}", username, err)))?;
if !output.status.success() {
bail!("unable to set password for '{}' - {}", username, String::from_utf8_lossy(&output.stderr));
}
Ok(())
}
}
pub struct PBS();
pub fn crypt(password: &[u8], salt: &str) -> Result<String, Error> {
#[link(name="crypt")]
extern "C" {
#[link_name = "crypt"]
fn __crypt(key: *const libc::c_char, salt: *const libc::c_char) -> * mut libc::c_char;
}
let salt = CString::new(salt)?;
let password = CString::new(password)?;
let res = unsafe {
CStr::from_ptr(
__crypt(
password.as_c_str().as_ptr(),
salt.as_c_str().as_ptr()
)
)
};
Ok(String::from(res.to_str()?))
}
pub fn encrypt_pw(password: &str) -> Result<String, Error> {
let salt = proxmox::sys::linux::random_data(8)?;
let salt = format!("$5${}$", base64::encode_config(&salt, base64::CRYPT));
crypt(password.as_bytes(), &salt)
}
pub fn verify_crypt_pw(password: &str, enc_password: &str) -> Result<(), Error> {
let verify = crypt(password.as_bytes(), enc_password)?;
if &verify != enc_password {
bail!("invalid credentials");
}
Ok(())
}
const SHADOW_CONFIG_FILENAME: &str = "/etc/proxmox-backup/shadow.json";
impl ProxmoxAuthenticator for PBS {
fn authenticate_user(&self, username: &str, password: &str) -> Result<(), Error> {
let data = proxmox::tools::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?;
match data[username].as_str() {
None => bail!("no password set"),
Some(enc_password) => verify_crypt_pw(password, enc_password)?,
}
Ok(())
}
fn store_password(&self, username: &str, password: &str) -> Result<(), Error> {
let enc_password = encrypt_pw(password)?;
let mut data = proxmox::tools::fs::file_get_json(SHADOW_CONFIG_FILENAME, Some(json!({})))?;
data[username] = enc_password.into();
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0600);
let options = proxmox::tools::fs::CreateOptions::new()
.perm(mode)
.owner(nix::unistd::ROOT)
.group(nix::unistd::Gid::from_raw(0));
let data = serde_json::to_vec_pretty(&data)?;
proxmox::tools::fs::replace_file(SHADOW_CONFIG_FILENAME, &data, options)?;
Ok(())
}
}
pub fn parse_userid(userid: &str) -> Result<(String, String), Error> {
let data: Vec<&str> = userid.rsplitn(2, '@').collect();
if data.len() != 2 {
bail!("userid '{}' has no realm", userid);
}
Ok((data[1].to_owned(), data[0].to_owned()))
}
/// Lookup the autenticator for the specified realm
pub fn lookup_authenticator(realm: &str) -> Result<Box<dyn ProxmoxAuthenticator>, Error> {
match realm {
"pam" => Ok(Box::new(PAM())),
"pbs" => Ok(Box::new(PBS())),
_ => bail!("unknown realm '{}'", realm),
}
}
/// Authenticate users
pub fn authenticate_user(userid: &str, password: &str) -> Result<(), Error> {
let (username, realm) = parse_userid(userid)?;
lookup_authenticator(&realm)?
.authenticate_user(&username, password)
}

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, format_err, Error};
use lazy_static::lazy_static;
use openssl::rsa::{Rsa};
@ -10,6 +10,8 @@ use std::path::PathBuf;
use proxmox::tools::fs::{file_get_contents, replace_file, CreateOptions};
use proxmox::try_block;
use crate::tools::epoch_now_u64;
fn compute_csrf_secret_digest(
timestamp: i64,
secret: &[u8],
@ -29,8 +31,7 @@ pub fn assemble_csrf_prevention_token(
username: &str,
) -> String {
let epoch = std::time::SystemTime::now().duration_since(
std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64;
let epoch = epoch_now_u64().unwrap() as i64;
let digest = compute_csrf_secret_digest(epoch, secret, username);
@ -67,8 +68,7 @@ pub fn verify_csrf_prevention_token(
bail!("invalid signature.");
}
let now = std::time::SystemTime::now().duration_since(
std::time::SystemTime::UNIX_EPOCH)?.as_secs() as i64;
let now = epoch_now_u64()? as i64;
let age = now - ttime;
if age < min_age {

View File

@ -103,7 +103,7 @@
//!
//! Not sure if this is better. TODO
use failure::*;
use anyhow::{bail, Error};
// Note: .pcat1 => Proxmox Catalog Format version 1
pub const CATALOG_NAME: &str = "catalog.pcat1.didx";
@ -198,5 +198,11 @@ pub use prune::*;
mod datastore;
pub use datastore::*;
mod verify;
pub use verify::*;
mod catalog_shell;
pub use catalog_shell::*;
mod async_index_reader;
pub use async_index_reader::*;

View File

@ -0,0 +1,128 @@
use std::future::Future;
use std::task::{Poll, Context};
use std::pin::Pin;
use anyhow::Error;
use futures::future::FutureExt;
use futures::ready;
use tokio::io::AsyncRead;
use proxmox::sys::error::io_err_other;
use proxmox::io_format_err;
use super::IndexFile;
use super::read_chunk::AsyncReadChunk;
enum AsyncIndexReaderState<S> {
NoData,
WaitForData(Pin<Box<dyn Future<Output = Result<(S, Vec<u8>), Error>> + Send + 'static>>),
HaveData(usize),
}
pub struct AsyncIndexReader<S, I: IndexFile> {
store: Option<S>,
index: I,
read_buffer: Vec<u8>,
current_chunk_idx: usize,
current_chunk_digest: [u8; 32],
state: AsyncIndexReaderState<S>,
}
// ok because the only public interfaces operates on &mut Self
unsafe impl<S: Sync, I: IndexFile + Sync> Sync for AsyncIndexReader<S, I> {}
impl<S: AsyncReadChunk, I: IndexFile> AsyncIndexReader<S, I> {
pub fn new(index: I, store: S) -> Self {
Self {
store: Some(store),
index,
read_buffer: Vec::with_capacity(1024 * 1024),
current_chunk_idx: 0,
current_chunk_digest: [0u8; 32],
state: AsyncIndexReaderState::NoData,
}
}
}
impl<S, I> AsyncRead for AsyncIndexReader<S, I>
where
S: AsyncReadChunk + Unpin + Sync + 'static,
I: IndexFile + Unpin,
{
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context,
buf: &mut [u8],
) -> Poll<tokio::io::Result<usize>> {
let this = Pin::get_mut(self);
loop {
match &mut this.state {
AsyncIndexReaderState::NoData => {
if this.current_chunk_idx >= this.index.index_count() {
return Poll::Ready(Ok(0));
}
let digest = this
.index
.index_digest(this.current_chunk_idx)
.ok_or(io_format_err!("could not get digest"))?
.clone();
if digest == this.current_chunk_digest {
this.state = AsyncIndexReaderState::HaveData(0);
continue;
}
this.current_chunk_digest = digest;
let store = match this.store.take() {
Some(store) => store,
None => {
return Poll::Ready(Err(io_format_err!("could not find store")));
}
};
let future = async move {
store.read_chunk(&digest)
.await
.map(move |x| (store, x))
};
this.state = AsyncIndexReaderState::WaitForData(future.boxed());
}
AsyncIndexReaderState::WaitForData(ref mut future) => {
match ready!(future.as_mut().poll(cx)) {
Ok((store, mut chunk_data)) => {
this.read_buffer.clear();
this.read_buffer.append(&mut chunk_data);
this.state = AsyncIndexReaderState::HaveData(0);
this.store = Some(store);
}
Err(err) => {
return Poll::Ready(Err(io_err_other(err)));
}
};
}
AsyncIndexReaderState::HaveData(offset) => {
let offset = *offset;
let len = this.read_buffer.len();
let n = if len - offset < buf.len() {
len - offset
} else {
buf.len()
};
buf[0..n].copy_from_slice(&this.read_buffer[offset..(offset + n)]);
if offset + n == len {
this.state = AsyncIndexReaderState::NoData;
this.current_chunk_idx += 1;
} else {
this.state = AsyncIndexReaderState::HaveData(offset + n);
}
return Poll::Ready(Ok(n));
}
}
}
}
}

View File

@ -1,6 +1,6 @@
use crate::tools;
use failure::*;
use anyhow::{bail, format_err, Error};
use regex::Regex;
use std::os::unix::io::RawFd;
@ -59,17 +59,6 @@ impl BackupGroup {
&self.backup_id
}
pub fn parse(path: &str) -> Result<Self, Error> {
let cap = GROUP_PATH_REGEX.captures(path)
.ok_or_else(|| format_err!("unable to parse backup group path '{}'", path))?;
Ok(Self {
backup_type: cap.get(1).unwrap().as_str().to_owned(),
backup_id: cap.get(2).unwrap().as_str().to_owned(),
})
}
pub fn group_path(&self) -> PathBuf {
let mut relative_path = PathBuf::new();
@ -152,6 +141,31 @@ impl BackupGroup {
}
}
impl std::fmt::Display for BackupGroup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let backup_type = self.backup_type();
let id = self.backup_id();
write!(f, "{}/{}", backup_type, id)
}
}
impl std::str::FromStr for BackupGroup {
type Err = Error;
/// Parse a backup group path
///
/// This parses strings like `vm/100".
fn from_str(path: &str) -> Result<Self, Self::Err> {
let cap = GROUP_PATH_REGEX.captures(path)
.ok_or_else(|| format_err!("unable to parse backup group path '{}'", path))?;
Ok(Self {
backup_type: cap.get(1).unwrap().as_str().to_owned(),
backup_id: cap.get(2).unwrap().as_str().to_owned(),
})
}
}
/// Uniquely identify a Backup (relative to data store)
///
/// We also call this a backup snaphost.
@ -188,16 +202,6 @@ impl BackupDir {
self.backup_time
}
pub fn parse(path: &str) -> Result<Self, Error> {
let cap = SNAPSHOT_PATH_REGEX.captures(path)
.ok_or_else(|| format_err!("unable to parse backup snapshot path '{}'", path))?;
let group = BackupGroup::new(cap.get(1).unwrap().as_str(), cap.get(2).unwrap().as_str());
let backup_time = cap.get(3).unwrap().as_str().parse::<DateTime<Utc>>()?;
Ok(BackupDir::from((group, backup_time.timestamp())))
}
pub fn relative_path(&self) -> PathBuf {
let mut relative_path = self.group.group_path();
@ -212,6 +216,31 @@ impl BackupDir {
}
}
impl std::str::FromStr for BackupDir {
type Err = Error;
/// Parse a snapshot path
///
/// This parses strings like `host/elsa/2020-06-15T05:18:33Z".
fn from_str(path: &str) -> Result<Self, Self::Err> {
let cap = SNAPSHOT_PATH_REGEX.captures(path)
.ok_or_else(|| format_err!("unable to parse backup snapshot path '{}'", path))?;
let group = BackupGroup::new(cap.get(1).unwrap().as_str(), cap.get(2).unwrap().as_str());
let backup_time = cap.get(3).unwrap().as_str().parse::<DateTime<Utc>>()?;
Ok(BackupDir::from((group, backup_time.timestamp())))
}
}
impl std::fmt::Display for BackupDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let backup_type = self.group.backup_type();
let id = self.group.backup_id();
let time = Self::backup_time_to_string(self.backup_time);
write!(f, "{}/{}/{}", backup_type, id, time)
}
}
impl From<(BackupGroup, i64)> for BackupDir {
fn from((group, timestamp): (BackupGroup, i64)) -> Self {
Self { group, backup_time: Utc.timestamp(timestamp, 0) }

View File

@ -1,23 +1,21 @@
use failure::*;
use std::fmt;
use std::ffi::{CStr, CString, OsStr};
use std::os::unix::ffi::OsStrExt;
use std::io::{Read, Write, Seek, SeekFrom};
use std::convert::TryFrom;
use std::ffi::{CStr, CString, OsStr};
use std::fmt;
use std::io::{Read, Write, Seek, SeekFrom};
use std::os::unix::ffi::OsStrExt;
use anyhow::{bail, format_err, Error};
use chrono::offset::{TimeZone, Local};
use pathpatterns::{MatchList, MatchType};
use proxmox::tools::io::ReadExt;
use proxmox::sys::error::io_err_other;
use crate::pxar::catalog::BackupCatalogWriter;
use crate::pxar::{MatchPattern, MatchPatternSlice, MatchType};
use crate::backup::file_formats::PROXMOX_CATALOG_FILE_MAGIC_1_0;
use crate::tools::runtime::block_on;
use crate::pxar::catalog::BackupCatalogWriter;
#[repr(u8)]
#[derive(Copy,Clone,PartialEq)]
enum CatalogEntryType {
pub(crate) enum CatalogEntryType {
Directory = b'd',
File = b'f',
Symlink = b'l',
@ -46,6 +44,21 @@ impl TryFrom<u8> for CatalogEntryType {
}
}
impl From<&DirEntryAttribute> for CatalogEntryType {
fn from(value: &DirEntryAttribute) -> Self {
match value {
DirEntryAttribute::Directory { .. } => CatalogEntryType::Directory,
DirEntryAttribute::File { .. } => CatalogEntryType::File,
DirEntryAttribute::Symlink => CatalogEntryType::Symlink,
DirEntryAttribute::Hardlink => CatalogEntryType::Hardlink,
DirEntryAttribute::BlockDevice => CatalogEntryType::BlockDevice,
DirEntryAttribute::CharDevice => CatalogEntryType::CharDevice,
DirEntryAttribute::Fifo => CatalogEntryType::Fifo,
DirEntryAttribute::Socket => CatalogEntryType::Socket,
}
}
}
impl fmt::Display for CatalogEntryType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", char::from(*self as u8))
@ -63,7 +76,7 @@ pub struct DirEntry {
}
/// Used to specific additional attributes inside DirEntry
#[derive(Clone, PartialEq)]
#[derive(Clone, Debug, PartialEq)]
pub enum DirEntryAttribute {
Directory { start: u64 },
File { size: u64, mtime: u64 },
@ -106,6 +119,23 @@ impl DirEntry {
}
}
/// Get file mode bits for this entry to be used with the `MatchList` api.
pub fn get_file_mode(&self) -> Option<u32> {
Some(
match self.attr {
DirEntryAttribute::Directory { .. } => pxar::mode::IFDIR,
DirEntryAttribute::File { .. } => pxar::mode::IFREG,
DirEntryAttribute::Symlink => pxar::mode::IFLNK,
DirEntryAttribute::Hardlink => return None,
DirEntryAttribute::BlockDevice => pxar::mode::IFBLK,
DirEntryAttribute::CharDevice => pxar::mode::IFCHR,
DirEntryAttribute::Fifo => pxar::mode::IFIFO,
DirEntryAttribute::Socket => pxar::mode::IFSOCK,
}
as u32
)
}
/// Check if DirEntry is a directory
pub fn is_directory(&self) -> bool {
match self.attr {
@ -383,32 +413,6 @@ impl <W: Write> BackupCatalogWriter for CatalogWriter<W> {
}
}
// fixme: move to somehere else?
/// Implement Write to tokio mpsc channel Sender
pub struct SenderWriter(tokio::sync::mpsc::Sender<Result<Vec<u8>, Error>>);
impl SenderWriter {
pub fn new(sender: tokio::sync::mpsc::Sender<Result<Vec<u8>, Error>>) -> Self {
Self(sender)
}
}
impl Write for SenderWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize, std::io::Error> {
block_on(async move {
self.0
.send(Ok(buf.to_vec()))
.await
.map_err(io_err_other)
.and(Ok(buf.len()))
})
}
fn flush(&mut self) -> Result<(), std::io::Error> {
Ok(())
}
}
/// Read Catalog files
pub struct CatalogReader<R> {
reader: R,
@ -476,7 +480,7 @@ impl <R: Read + Seek> CatalogReader<R> {
&mut self,
parent: &DirEntry,
filename: &[u8],
) -> Result<DirEntry, Error> {
) -> Result<Option<DirEntry>, Error> {
let start = match parent.attr {
DirEntryAttribute::Directory { start } => start,
@ -496,10 +500,7 @@ impl <R: Read + Seek> CatalogReader<R> {
Ok(false) // stop parsing
})?;
match item {
None => bail!("no such file"),
Some(entry) => Ok(entry),
}
Ok(item)
}
/// Read the raw directory info block from current reader position.
@ -532,7 +533,10 @@ impl <R: Read + Seek> CatalogReader<R> {
self.dump_dir(&path, pos)?;
}
CatalogEntryType::File => {
let dt = Local.timestamp(mtime as i64, 0);
let dt = Local
.timestamp_opt(mtime as i64, 0)
.single() // chrono docs say timestamp_opt can only be None or Single!
.unwrap_or_else(|| Local.timestamp(0, 0));
println!(
"{} {:?} {} {}",
@ -555,38 +559,30 @@ impl <R: Read + Seek> CatalogReader<R> {
/// provided callback on them.
pub fn find(
&mut self,
mut entry: &mut Vec<DirEntry>,
pattern: &[MatchPatternSlice],
callback: &Box<fn(&[DirEntry])>,
parent: &DirEntry,
file_path: &mut Vec<u8>,
match_list: &impl MatchList, //&[MatchEntry],
callback: &mut dyn FnMut(&[u8]) -> Result<(), Error>,
) -> Result<(), Error> {
let parent = entry.last().unwrap();
if !parent.is_directory() {
return Ok(())
}
let file_len = file_path.len();
for e in self.read_dir(parent)? {
match MatchPatternSlice::match_filename_include(
&CString::new(e.name.clone())?,
e.is_directory(),
pattern,
)? {
(MatchType::Positive, _) => {
entry.push(e);
callback(&entry);
let pattern = MatchPattern::from_line(b"**/*").unwrap().unwrap();
let child_pattern = vec![pattern.as_slice()];
self.find(&mut entry, &child_pattern, callback)?;
entry.pop();
}
(MatchType::PartialPositive, child_pattern)
| (MatchType::PartialNegative, child_pattern) => {
entry.push(e);
self.find(&mut entry, &child_pattern, callback)?;
entry.pop();
}
_ => {}
let is_dir = e.is_directory();
file_path.truncate(file_len);
if !e.name.starts_with(b"/") {
file_path.reserve(e.name.len() + 1);
file_path.push(b'/');
}
file_path.extend(&e.name);
match match_list.matches(&file_path, e.get_file_mode()) {
Some(MatchType::Exclude) => continue,
Some(MatchType::Include) => callback(&file_path)?,
None => (),
}
if is_dir {
self.find(&e, file_path, match_list, callback)?;
}
}
file_path.truncate(file_len);
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use std::sync::Arc;
use std::io::Read;

View File

@ -1,7 +1,7 @@
use std::sync::Arc;
use std::io::Write;
use failure::*;
use anyhow::{Error};
use super::CryptConfig;
use crate::tools::borrow::Tied;

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, format_err, Error};
use std::path::{Path, PathBuf};
use std::io::Write;
@ -157,8 +157,8 @@ impl ChunkStore {
let (chunk_path, _digest_str) = self.chunk_path(digest);
const UTIME_NOW: i64 = ((1 << 30) - 1);
const UTIME_OMIT: i64 = ((1 << 30) - 2);
const UTIME_NOW: i64 = (1 << 30) - 1;
const UTIME_OMIT: i64 = (1 << 30) - 2;
let times: [libc::timespec; 2] = [
libc::timespec { tv_sec: 0, tv_nsec: UTIME_NOW },
@ -289,9 +289,9 @@ impl ChunkStore {
pub fn sweep_unused_chunks(
&self,
oldest_writer: Option<i64>,
oldest_writer: i64,
status: &mut GarbageCollectionStatus,
worker: Arc<WorkerTask>,
worker: &WorkerTask,
) -> Result<(), Error> {
use nix::sys::stat::fstatat;
@ -299,10 +299,8 @@ impl ChunkStore {
let mut min_atime = now - 3600*24; // at least 24h (see mount option relatime)
if let Some(stamp) = oldest_writer {
if stamp < min_atime {
min_atime = stamp;
}
if oldest_writer < min_atime {
min_atime = oldest_writer;
}
min_atime -= 300; // add 5 mins gap for safety
@ -316,6 +314,7 @@ impl ChunkStore {
worker.log(format!("percentage done: {}, chunk count: {}", percentage, chunk_count));
}
worker.fail_on_abort()?;
tools::fail_on_shutdown()?;
let (dirfd, entry) = match entry {
@ -338,10 +337,9 @@ impl ChunkStore {
let lock = self.mutex.lock();
if let Ok(stat) = fstatat(dirfd, filename, nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW) {
let age = now - stat.st_atime;
//println!("FOUND {} {:?}", age/(3600*24), filename);
if stat.st_atime < min_atime {
println!("UNLINK {} {:?}", age/(3600*24), filename);
//let age = now - stat.st_atime;
//println!("UNLINK {} {:?}", age/(3600*24), filename);
let res = unsafe { libc::unlinkat(dirfd, filename.as_ptr(), 0) };
if res != 0 {
let err = nix::Error::last();
@ -354,9 +352,14 @@ impl ChunkStore {
}
status.removed_chunks += 1;
status.removed_bytes += stat.st_size as u64;
} else {
status.disk_chunks += 1;
status.disk_bytes += stat.st_size as u64;
} else {
if stat.st_atime < oldest_writer {
status.pending_chunks += 1;
status.pending_bytes += stat.st_size as u64;
} else {
status.disk_chunks += 1;
status.disk_bytes += stat.st_size as u64;
}
}
}
drop(lock);
@ -426,6 +429,10 @@ impl ChunkStore {
full_path
}
pub fn name(&self) -> &str {
&self.name
}
pub fn base_path(&self) -> PathBuf {
self.base.clone()
}

View File

@ -2,7 +2,7 @@ use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::BytesMut;
use failure::*;
use anyhow::{Error};
use futures::ready;
use futures::stream::{Stream, TryStream};

View File

@ -6,7 +6,7 @@
//! See the Wikipedia Artikel for [Authenticated
//! encryption](https://en.wikipedia.org/wiki/Authenticated_encryption)
//! for a short introduction.
use failure::*;
use anyhow::{bail, Error};
use openssl::pkcs5::pbkdf2_hmac;
use openssl::hash::MessageDigest;
use openssl::symm::{decrypt_aead, Cipher, Crypter, Mode};

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, Error};
use std::sync::Arc;
use std::io::{Read, BufRead};

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use std::sync::Arc;
use std::io::Write;

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, Error};
use std::convert::TryInto;
use proxmox::tools::io::{ReadExt, WriteExt};
@ -167,7 +167,7 @@ impl DataBlob {
}
/// Decode blob data
pub fn decode(self, config: Option<&CryptConfig>) -> Result<Vec<u8>, Error> {
pub fn decode(&self, config: Option<&CryptConfig>) -> Result<Vec<u8>, Error> {
let magic = self.magic();
@ -311,7 +311,9 @@ impl DataBlob {
/// Verify digest and data length for unencrypted chunks.
///
/// To do that, we need to decompress data first. Please note that
/// this is noth possible for encrypted chunks.
/// this is not possible for encrypted chunks. This function simply return Ok
/// for encrypted chunks.
/// Note: This does not call verify_crc
pub fn verify_unencrypted(
&self,
expected_chunk_size: usize,
@ -320,22 +322,18 @@ impl DataBlob {
let magic = self.magic();
let verify_raw_data = |data: &[u8]| {
if expected_chunk_size != data.len() {
bail!("detected chunk with wrong length ({} != {})", expected_chunk_size, data.len());
}
let digest = openssl::sha::sha256(data);
if &digest != expected_digest {
bail!("detected chunk with wrong digest.");
}
Ok(())
};
if magic == &ENCR_COMPR_BLOB_MAGIC_1_0 || magic == &ENCRYPTED_BLOB_MAGIC_1_0 {
return Ok(());
}
if magic == &COMPRESSED_BLOB_MAGIC_1_0 {
let data = zstd::block::decompress(&self.raw_data[12..], 16*1024*1024)?;
verify_raw_data(&data)?;
} else if magic == &UNCOMPRESSED_BLOB_MAGIC_1_0 {
verify_raw_data(&self.raw_data[12..])?;
let data = self.decode(None)?;
if expected_chunk_size != data.len() {
bail!("detected chunk with wrong length ({} != {})", expected_chunk_size, data.len());
}
let digest = openssl::sha::sha256(&data);
if &digest != expected_digest {
bail!("detected chunk with wrong digest.");
}
Ok(())

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, Error};
use std::sync::Arc;
use std::io::{Read, BufReader};
use proxmox::tools::io::ReadExt;
@ -19,6 +19,10 @@ pub struct DataBlobReader<R: Read> {
state: BlobReaderState<R>,
}
// zstd_safe::DCtx is not sync but we are, since
// the only public interface is on mutable reference
unsafe impl<R: Read> Sync for DataBlobReader<R> {}
impl <R: Read> DataBlobReader<R> {
pub fn new(mut reader: R, config: Option<Arc<CryptConfig>>) -> Result<Self, Error> {

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use std::sync::Arc;
use std::io::{Write, Seek, SeekFrom};
use proxmox::tools::io::WriteExt;

View File

@ -1,9 +1,10 @@
use std::collections::{HashSet, HashMap};
use std::io;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::convert::TryFrom;
use failure::*;
use anyhow::{bail, format_err, Error};
use lazy_static::lazy_static;
use chrono::{DateTime, Utc};
@ -11,7 +12,7 @@ use super::backup_info::{BackupGroup, BackupDir};
use super::chunk_store::ChunkStore;
use super::dynamic_index::{DynamicIndexReader, DynamicIndexWriter};
use super::fixed_index::{FixedIndexReader, FixedIndexWriter};
use super::manifest::{MANIFEST_BLOB_NAME, BackupManifest};
use super::manifest::{MANIFEST_BLOB_NAME, CLIENT_LOG_BLOB_NAME, BackupManifest};
use super::index::*;
use super::{DataBlob, ArchiveType, archive_type};
use crate::config::datastore;
@ -134,6 +135,10 @@ impl DataStore {
Ok(out)
}
pub fn name(&self) -> &str {
self.chunk_store.name()
}
pub fn base_path(&self) -> PathBuf {
self.chunk_store.base_path()
}
@ -149,6 +154,7 @@ impl DataStore {
let mut wanted_files = HashSet::new();
wanted_files.insert(MANIFEST_BLOB_NAME.to_string());
wanted_files.insert(CLIENT_LOG_BLOB_NAME.to_string());
manifest.files().iter().for_each(|item| { wanted_files.insert(item.filename.clone()); });
for item in tools::fs::read_subdir(libc::AT_FDCWD, &full_path)? {
@ -236,18 +242,80 @@ impl DataStore {
}
}
pub fn create_backup_dir(&self, backup_dir: &BackupDir) -> Result<(PathBuf, bool), io::Error> {
/// Returns the backup owner.
///
/// The backup owner is the user who first created the backup group.
pub fn get_owner(&self, backup_group: &BackupGroup) -> Result<String, Error> {
let mut full_path = self.base_path();
full_path.push(backup_group.group_path());
full_path.push("owner");
let owner = proxmox::tools::fs::file_read_firstline(full_path)?;
Ok(owner.trim_end().to_string()) // remove trailing newline
}
/// Set the backup owner.
pub fn set_owner(&self, backup_group: &BackupGroup, userid: &str, force: bool) -> Result<(), Error> {
let mut path = self.base_path();
path.push(backup_group.group_path());
path.push("owner");
let mut open_options = std::fs::OpenOptions::new();
open_options.write(true);
open_options.truncate(true);
if force {
open_options.create(true);
} else {
open_options.create_new(true);
}
let mut file = open_options.open(&path)
.map_err(|err| format_err!("unable to create owner file {:?} - {}", path, err))?;
write!(file, "{}\n", userid)
.map_err(|err| format_err!("unable to write owner file {:?} - {}", path, err))?;
Ok(())
}
/// Create a backup group if it does not already exists.
///
/// And set the owner to 'userid'. If the group already exists, it returns the
/// current owner (instead of setting the owner).
pub fn create_backup_group(&self, backup_group: &BackupGroup, userid: &str) -> Result<String, Error> {
// create intermediate path first:
let mut full_path = self.base_path();
full_path.push(backup_dir.group().group_path());
let base_path = self.base_path();
let mut full_path = base_path.clone();
full_path.push(backup_group.backup_type());
std::fs::create_dir_all(&full_path)?;
full_path.push(backup_group.backup_id());
// create the last component now
match std::fs::create_dir(&full_path) {
Ok(_) => {
self.set_owner(backup_group, userid, false)?;
let owner = self.get_owner(backup_group)?; // just to be sure
Ok(owner)
}
Err(ref err) if err.kind() == io::ErrorKind::AlreadyExists => {
let owner = self.get_owner(backup_group)?; // just to be sure
Ok(owner)
}
Err(err) => bail!("unable to create backup group {:?} - {}", full_path, err),
}
}
/// Creates a new backup snapshot inside a BackupGroup
///
/// The BackupGroup directory needs to exist.
pub fn create_backup_dir(&self, backup_dir: &BackupDir) -> Result<(PathBuf, bool), io::Error> {
let relative_path = backup_dir.relative_path();
let mut full_path = self.base_path();
full_path.push(&relative_path);
// create the last component now
match std::fs::create_dir(&full_path) {
Ok(_) => Ok((relative_path, true)),
Err(ref e) if e.kind() == io::ErrorKind::AlreadyExists => Ok((relative_path, false)),
@ -290,12 +358,14 @@ impl DataStore {
index: I,
file_name: &Path, // only used for error reporting
status: &mut GarbageCollectionStatus,
worker: &WorkerTask,
) -> Result<(), Error> {
status.index_file_count += 1;
status.index_data_bytes += index.index_bytes();
for pos in 0..index.index_count() {
worker.fail_on_abort()?;
tools::fail_on_shutdown()?;
let digest = index.index_digest(pos).unwrap();
if let Err(err) = self.chunk_store.touch_chunk(digest) {
@ -306,21 +376,22 @@ impl DataStore {
Ok(())
}
fn mark_used_chunks(&self, status: &mut GarbageCollectionStatus) -> Result<(), Error> {
fn mark_used_chunks(&self, status: &mut GarbageCollectionStatus, worker: &WorkerTask) -> Result<(), Error> {
let image_list = self.list_images()?;
for path in image_list {
worker.fail_on_abort()?;
tools::fail_on_shutdown()?;
if let Ok(archive_type) = archive_type(&path) {
if archive_type == ArchiveType::FixedIndex {
let index = self.open_fixed_reader(&path)?;
self.index_mark_used_chunks(index, &path, status)?;
self.index_mark_used_chunks(index, &path, status, worker)?;
} else if archive_type == ArchiveType::DynamicIndex {
let index = self.open_dynamic_reader(&path)?;
self.index_mark_used_chunks(index, &path, status)?;
self.index_mark_used_chunks(index, &path, status, worker)?;
}
}
}
@ -332,26 +403,36 @@ impl DataStore {
self.last_gc_status.lock().unwrap().clone()
}
pub fn garbage_collection(&self, worker: Arc<WorkerTask>) -> Result<(), Error> {
pub fn garbage_collection_running(&self) -> bool {
if let Ok(_) = self.gc_mutex.try_lock() { false } else { true }
}
pub fn garbage_collection(&self, worker: &WorkerTask) -> Result<(), Error> {
if let Ok(ref mut _mutex) = self.gc_mutex.try_lock() {
let _exclusive_lock = self.chunk_store.try_exclusive_lock()?;
let oldest_writer = self.chunk_store.oldest_writer();
let now = unsafe { libc::time(std::ptr::null_mut()) };
let oldest_writer = self.chunk_store.oldest_writer().unwrap_or(now);
let mut gc_status = GarbageCollectionStatus::default();
gc_status.upid = Some(worker.to_string());
worker.log("Start GC phase1 (mark used chunks)");
self.mark_used_chunks(&mut gc_status)?;
self.mark_used_chunks(&mut gc_status, &worker)?;
worker.log("Start GC phase2 (sweep unused chunks)");
self.chunk_store.sweep_unused_chunks(oldest_writer, &mut gc_status, worker.clone())?;
self.chunk_store.sweep_unused_chunks(oldest_writer, &mut gc_status, &worker)?;
worker.log(&format!("Removed bytes: {}", gc_status.removed_bytes));
worker.log(&format!("Removed chunks: {}", gc_status.removed_chunks));
if gc_status.pending_bytes > 0 {
worker.log(&format!("Pending removals: {} bytes ({} chunks)", gc_status.pending_bytes, gc_status.pending_chunks));
}
worker.log(&format!("Original data bytes: {}", gc_status.index_data_bytes));
if gc_status.index_data_bytes > 0 {
@ -394,4 +475,28 @@ impl DataStore {
) -> Result<(bool, u64), Error> {
self.chunk_store.insert_chunk(chunk, digest)
}
pub fn verify_stored_chunk(&self, digest: &[u8; 32], expected_chunk_size: u64) -> Result<(), Error> {
let blob = self.chunk_store.read_chunk(digest)?;
blob.verify_crc()?;
blob.verify_unencrypted(expected_chunk_size as usize, digest)?;
Ok(())
}
pub fn load_blob(&self, backup_dir: &BackupDir, filename: &str) -> Result<(DataBlob, u64), Error> {
let mut path = self.base_path();
path.push(backup_dir.relative_path());
path.push(filename);
let raw_data = proxmox::tools::fs::file_get_contents(&path)?;
let raw_size = raw_data.len() as u64;
let blob = DataBlob::from_raw(raw_data)?;
Ok((blob, raw_size))
}
pub fn load_manifest(&self, backup_dir: &BackupDir) -> Result<(BackupManifest, u64), Error> {
let (blob, raw_size) = self.load_blob(backup_dir, MANIFEST_BLOB_NAME)?;
let manifest = BackupManifest::try_from(blob)?;
Ok((manifest, raw_size))
}
}

View File

@ -1,23 +1,28 @@
use std::convert::TryInto;
use std::fs::File;
use std::io::{BufWriter, Seek, SeekFrom, Write};
use std::io::{self, BufWriter, Seek, SeekFrom, Write};
use std::ops::Range;
use std::os::unix::io::AsRawFd;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::{Arc, Mutex};
use std::task::Context;
use std::pin::Pin;
use failure::*;
use anyhow::{bail, format_err, Error};
use proxmox::tools::io::ReadExt;
use proxmox::tools::uuid::Uuid;
use proxmox::tools::vec;
use proxmox::tools::mmap::Mmap;
use pxar::accessor::{MaybeReady, ReadAt, ReadAtOperation};
use super::chunk_stat::ChunkStat;
use super::chunk_store::ChunkStore;
use super::index::ChunkReadInfo;
use super::read_chunk::ReadChunk;
use super::Chunker;
use super::IndexFile;
use super::{DataBlob, DataChunkBuilder};
use crate::tools;
use crate::tools::{self, epoch_now_u64};
/// Header format definition for dynamic index files (`.dixd`)
#[repr(C)]
@ -36,34 +41,34 @@ proxmox::static_assert_size!(DynamicIndexHeader, 4096);
// pub data: DynamicIndexHeaderData,
// }
#[derive(Clone, Debug)]
#[repr(C)]
pub struct DynamicEntry {
end_le: u64,
digest: [u8; 32],
}
impl DynamicEntry {
#[inline]
pub fn end(&self) -> u64 {
u64::from_le(self.end_le)
}
}
pub struct DynamicIndexReader {
_file: File,
pub size: usize,
index: *const u8,
index_entries: usize,
index: Mmap<DynamicEntry>,
pub uuid: [u8; 16],
pub ctime: u64,
pub index_csum: [u8; 32],
}
// `index` is mmap()ed which cannot be thread-local so should be sendable
// FIXME: Introduce an mmap wrapper type for this?
unsafe impl Send for DynamicIndexReader {}
unsafe impl Sync for DynamicIndexReader {}
impl Drop for DynamicIndexReader {
fn drop(&mut self) {
if let Err(err) = self.unmap() {
eprintln!("Unable to unmap dynamic index - {}", err);
}
}
}
impl DynamicIndexReader {
pub fn open(path: &Path) -> Result<Self, Error> {
File::open(path)
.map_err(Error::from)
.and_then(|file| Self::new(file))
.and_then(Self::new)
.map_err(|err| format_err!("Unable to open dynamic index {:?} - {}", path, err))
}
@ -74,6 +79,7 @@ impl DynamicIndexReader {
bail!("unable to get shared lock - {}", err);
}
// FIXME: This is NOT OUR job! Check the callers of this method and remove this!
file.seek(SeekFrom::Start(0))?;
let header_size = std::mem::size_of::<DynamicIndexHeader>();
@ -93,123 +99,49 @@ impl DynamicIndexReader {
let size = stat.st_size as usize;
let index_size = size - header_size;
if (index_size % 40) != 0 {
let index_count = index_size / 40;
if index_count * 40 != index_size {
bail!("got unexpected file size");
}
let data = unsafe {
nix::sys::mman::mmap(
std::ptr::null_mut(),
index_size,
let index = unsafe {
Mmap::map_fd(
rawfd,
header_size as u64,
index_count,
nix::sys::mman::ProtFlags::PROT_READ,
nix::sys::mman::MapFlags::MAP_PRIVATE,
rawfd,
header_size as i64,
)
}? as *const u8;
)?
};
Ok(Self {
_file: file,
size,
index: data,
index_entries: index_size / 40,
index,
ctime,
uuid: header.uuid,
index_csum: header.index_csum,
})
}
fn unmap(&mut self) -> Result<(), Error> {
if self.index == std::ptr::null_mut() {
return Ok(());
}
if let Err(err) = unsafe {
nix::sys::mman::munmap(self.index as *mut std::ffi::c_void, self.index_entries * 40)
} {
bail!("unmap dynamic index failed - {}", err);
}
self.index = std::ptr::null_mut();
Ok(())
}
#[allow(clippy::cast_ptr_alignment)]
pub fn chunk_info(&self, pos: usize) -> Result<(u64, u64, [u8; 32]), Error> {
if pos >= self.index_entries {
bail!("chunk index out of range");
}
let start = if pos == 0 {
0
} else {
unsafe { *(self.index.add((pos - 1) * 40) as *const u64) }
};
let end = unsafe { *(self.index.add(pos * 40) as *const u64) };
let mut digest = std::mem::MaybeUninit::<[u8; 32]>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(
self.index.add(pos * 40 + 8),
(*digest.as_mut_ptr()).as_mut_ptr(),
32,
);
}
Ok((start, end, unsafe { digest.assume_init() }))
}
#[inline]
#[allow(clippy::cast_ptr_alignment)]
fn chunk_end(&self, pos: usize) -> u64 {
if pos >= self.index_entries {
if pos >= self.index.len() {
panic!("chunk index out of range");
}
unsafe { *(self.index.add(pos * 40) as *const u64) }
self.index[pos].end()
}
#[inline]
fn chunk_digest(&self, pos: usize) -> &[u8; 32] {
if pos >= self.index_entries {
if pos >= self.index.len() {
panic!("chunk index out of range");
}
let slice = unsafe { std::slice::from_raw_parts(self.index.add(pos * 40 + 8), 32) };
slice.try_into().unwrap()
&self.index[pos].digest
}
/// Compute checksum and data size
pub fn compute_csum(&self) -> ([u8; 32], u64) {
let mut csum = openssl::sha::Sha256::new();
let mut chunk_end = 0;
for pos in 0..self.index_entries {
chunk_end = self.chunk_end(pos);
let digest = self.chunk_digest(pos);
csum.update(&chunk_end.to_le_bytes());
csum.update(digest);
}
let csum = csum.finish();
(csum, chunk_end)
}
/*
pub fn dump_pxar(&self, mut writer: Box<dyn Write>) -> Result<(), Error> {
for pos in 0..self.index_entries {
let _end = self.chunk_end(pos);
let digest = self.chunk_digest(pos);
//println!("Dump {:08x}", end );
let chunk = self.store.read_chunk(digest)?;
// fimxe: handle encrypted chunks
let data = chunk.decode(None)?;
writer.write_all(&data)?;
}
Ok(())
}
*/
// TODO: can we use std::slice::binary_search with Mmap now?
fn binary_search(
&self,
start_idx: usize,
@ -238,11 +170,11 @@ impl DynamicIndexReader {
impl IndexFile for DynamicIndexReader {
fn index_count(&self) -> usize {
self.index_entries
self.index.len()
}
fn index_digest(&self, pos: usize) -> Option<&[u8; 32]> {
if pos >= self.index_entries {
if pos >= self.index.len() {
None
} else {
Some(unsafe { std::mem::transmute(self.chunk_digest(pos).as_ptr()) })
@ -250,12 +182,59 @@ impl IndexFile for DynamicIndexReader {
}
fn index_bytes(&self) -> u64 {
if self.index_entries == 0 {
if self.index.is_empty() {
0
} else {
self.chunk_end((self.index_entries - 1) as usize)
self.chunk_end(self.index.len() - 1)
}
}
fn compute_csum(&self) -> ([u8; 32], u64) {
let mut csum = openssl::sha::Sha256::new();
let mut chunk_end = 0;
for pos in 0..self.index_count() {
let info = self.chunk_info(pos).unwrap();
chunk_end = info.range.end;
csum.update(&chunk_end.to_le_bytes());
csum.update(&info.digest);
}
let csum = csum.finish();
(csum, chunk_end)
}
#[allow(clippy::cast_ptr_alignment)]
fn chunk_info(&self, pos: usize) -> Option<ChunkReadInfo> {
if pos >= self.index.len() {
return None;
}
let start = if pos == 0 { 0 } else { self.index[pos - 1].end() };
let end = self.index[pos].end();
Some(ChunkReadInfo {
range: start..end,
digest: self.index[pos].digest.clone(),
})
}
}
struct CachedChunk {
range: Range<u64>,
data: Vec<u8>,
}
impl CachedChunk {
/// Perform sanity checks on the range and data size:
pub fn new(range: Range<u64>, data: Vec<u8>) -> Result<Self, Error> {
if data.len() as u64 != range.end - range.start {
bail!(
"read chunk with wrong size ({} != {})",
data.len(),
range.end - range.start,
);
}
Ok(Self { range, data })
}
}
pub struct BufferedDynamicReader<S> {
@ -266,7 +245,7 @@ pub struct BufferedDynamicReader<S> {
buffered_chunk_idx: usize,
buffered_chunk_start: u64,
read_offset: u64,
lru_cache: crate::tools::lru_cache::LruCache<usize, (u64, u64, Vec<u8>)>,
lru_cache: crate::tools::lru_cache::LruCache<usize, CachedChunk>,
}
struct ChunkCacher<'a, S> {
@ -274,16 +253,21 @@ struct ChunkCacher<'a, S> {
index: &'a DynamicIndexReader,
}
impl<'a, S: ReadChunk> crate::tools::lru_cache::Cacher<usize, (u64, u64, Vec<u8>)> for ChunkCacher<'a, S> {
fn fetch(&mut self, index: usize) -> Result<Option<(u64, u64, Vec<u8>)>, failure::Error> {
let (start, end, digest) = self.index.chunk_info(index)?;
self.store.read_chunk(&digest).and_then(|data| Ok(Some((start, end, data))))
impl<'a, S: ReadChunk> crate::tools::lru_cache::Cacher<usize, CachedChunk> for ChunkCacher<'a, S> {
fn fetch(&mut self, index: usize) -> Result<Option<CachedChunk>, Error> {
let info = match self.index.chunk_info(index) {
Some(info) => info,
None => bail!("chunk index out of range"),
};
let range = info.range;
let data = self.store.read_chunk(&info.digest)?;
CachedChunk::new(range, data).map(Some)
}
}
impl<S: ReadChunk> BufferedDynamicReader<S> {
pub fn new(index: DynamicIndexReader, store: S) -> Self {
let archive_size = index.chunk_end(index.index_entries - 1);
let archive_size = index.index_bytes();
Self {
store,
index,
@ -301,7 +285,8 @@ impl<S: ReadChunk> BufferedDynamicReader<S> {
}
fn buffer_chunk(&mut self, idx: usize) -> Result<(), Error> {
let (start, end, data) = self.lru_cache.access(
//let (start, end, data) = self.lru_cache.access(
let cached_chunk = self.lru_cache.access(
idx,
&mut ChunkCacher {
store: &mut self.store,
@ -309,21 +294,13 @@ impl<S: ReadChunk> BufferedDynamicReader<S> {
},
)?.ok_or_else(|| format_err!("chunk not found by cacher"))?;
if (*end - *start) != data.len() as u64 {
bail!(
"read chunk with wrong size ({} != {}",
(*end - *start),
data.len()
);
}
// fixme: avoid copy
self.read_buffer.clear();
self.read_buffer.extend_from_slice(&data);
self.read_buffer.extend_from_slice(&cached_chunk.data);
self.buffered_chunk_idx = idx;
self.buffered_chunk_start = *start;
self.buffered_chunk_start = cached_chunk.range.start;
//println!("BUFFER {} {}", self.buffered_chunk_start, end);
Ok(())
}
@ -340,7 +317,7 @@ impl<S: ReadChunk> crate::tools::BufferedRead for BufferedDynamicReader<S> {
// optimization for sequential read
if buffer_len > 0
&& ((self.buffered_chunk_idx + 1) < index.index_entries)
&& ((self.buffered_chunk_idx + 1) < index.index.len())
&& (offset >= (self.buffered_chunk_start + (self.read_buffer.len() as u64)))
{
let next_idx = self.buffered_chunk_idx + 1;
@ -356,7 +333,7 @@ impl<S: ReadChunk> crate::tools::BufferedRead for BufferedDynamicReader<S> {
|| (offset < self.buffered_chunk_start)
|| (offset >= (self.buffered_chunk_start + (self.read_buffer.len() as u64)))
{
let end_idx = index.index_entries - 1;
let end_idx = index.index.len() - 1;
let end = index.chunk_end(end_idx);
let idx = index.binary_search(0, 0, end_idx, end, offset)?;
self.buffer_chunk(idx)?;
@ -383,9 +360,7 @@ impl<S: ReadChunk> std::io::Read for BufferedDynamicReader<S> {
data.len()
};
unsafe {
std::ptr::copy_nonoverlapping(data.as_ptr(), buf.as_mut_ptr(), n);
}
buf[0..n].copy_from_slice(&data[0..n]);
self.read_offset += n as u64;
@ -417,6 +392,49 @@ impl<S: ReadChunk> std::io::Seek for BufferedDynamicReader<S> {
}
}
/// This is a workaround until we have cleaned up the chunk/reader/... infrastructure for better
/// async use!
///
/// Ideally BufferedDynamicReader gets replaced so the LruCache maps to `BroadcastFuture<Chunk>`,
/// so that we can properly access it from multiple threads simultaneously while not issuing
/// duplicate simultaneous reads over http.
#[derive(Clone)]
pub struct LocalDynamicReadAt<R: ReadChunk> {
inner: Arc<Mutex<BufferedDynamicReader<R>>>,
}
impl<R: ReadChunk> LocalDynamicReadAt<R> {
pub fn new(inner: BufferedDynamicReader<R>) -> Self {
Self {
inner: Arc::new(Mutex::new(inner)),
}
}
}
impl<R: ReadChunk> ReadAt for LocalDynamicReadAt<R> {
fn start_read_at<'a>(
self: Pin<&'a Self>,
_cx: &mut Context,
buf: &'a mut [u8],
offset: u64,
) -> MaybeReady<io::Result<usize>, ReadAtOperation<'a>> {
use std::io::Read;
MaybeReady::Ready(tokio::task::block_in_place(move || {
let mut reader = self.inner.lock().unwrap();
reader.seek(SeekFrom::Start(offset))?;
Ok(reader.read(buf)?)
}))
}
fn poll_complete<'a>(
self: Pin<&'a Self>,
_op: ReadAtOperation<'a>,
) -> MaybeReady<io::Result<usize>, ReadAtOperation<'a>> {
panic!("LocalDynamicReadAt::start_read_at returned Pending");
}
}
/// Create dynamic index files (`.dixd`)
pub struct DynamicIndexWriter {
store: Arc<ChunkStore>,
@ -460,9 +478,7 @@ impl DynamicIndexWriter {
panic!("got unexpected header size");
}
let ctime = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)?
.as_secs();
let ctime = epoch_now_u64()?;
let uuid = Uuid::generate();

View File

@ -1,11 +1,10 @@
use failure::*;
use std::convert::TryInto;
use anyhow::{bail, format_err, Error};
use std::io::{Seek, SeekFrom};
use super::chunk_stat::*;
use super::chunk_store::*;
use super::IndexFile;
use crate::tools;
use super::{IndexFile, ChunkReadInfo};
use crate::tools::{self, epoch_now_u64};
use chrono::{Local, TimeZone};
use std::fs::File;
@ -147,38 +146,6 @@ impl FixedIndexReader {
Ok(())
}
pub fn chunk_info(&self, pos: usize) -> Result<(u64, u64, [u8; 32]), Error> {
if pos >= self.index_length {
bail!("chunk index out of range");
}
let start = (pos * self.chunk_size) as u64;
let mut end = start + self.chunk_size as u64;
if end > self.size {
end = self.size;
}
let mut digest = std::mem::MaybeUninit::<[u8; 32]>::uninit();
unsafe {
std::ptr::copy_nonoverlapping(
self.index.add(pos * 32),
(*digest.as_mut_ptr()).as_mut_ptr(),
32,
);
}
Ok((start, end, unsafe { digest.assume_init() }))
}
#[inline]
fn chunk_digest(&self, pos: usize) -> &[u8; 32] {
if pos >= self.index_length {
panic!("chunk index out of range");
}
let slice = unsafe { std::slice::from_raw_parts(self.index.add(pos * 32), 32) };
slice.try_into().unwrap()
}
#[inline]
fn chunk_end(&self, pos: usize) -> u64 {
if pos >= self.index_length {
@ -193,20 +160,6 @@ impl FixedIndexReader {
}
}
/// Compute checksum and data size
pub fn compute_csum(&self) -> ([u8; 32], u64) {
let mut csum = openssl::sha::Sha256::new();
let mut chunk_end = 0;
for pos in 0..self.index_length {
chunk_end = ((pos + 1) * self.chunk_size) as u64;
let digest = self.chunk_digest(pos);
csum.update(digest);
}
let csum = csum.finish();
(csum, chunk_end)
}
pub fn print_info(&self) {
println!("Size: {}", self.size);
println!("ChunkSize: {}", self.chunk_size);
@ -234,6 +187,38 @@ impl IndexFile for FixedIndexReader {
fn index_bytes(&self) -> u64 {
self.size
}
fn chunk_info(&self, pos: usize) -> Option<ChunkReadInfo> {
if pos >= self.index_length {
return None;
}
let start = (pos * self.chunk_size) as u64;
let mut end = start + self.chunk_size as u64;
if end > self.size {
end = self.size;
}
let digest = self.index_digest(pos).unwrap();
Some(ChunkReadInfo {
range: start..end,
digest: *digest,
})
}
fn compute_csum(&self) -> ([u8; 32], u64) {
let mut csum = openssl::sha::Sha256::new();
let mut chunk_end = 0;
for pos in 0..self.index_count() {
let info = self.chunk_info(pos).unwrap();
chunk_end = info.range.end;
csum.update(&info.digest);
}
let csum = csum.finish();
(csum, chunk_end)
}
}
pub struct FixedIndexWriter {
@ -290,9 +275,7 @@ impl FixedIndexWriter {
panic!("got unexpected header size");
}
let ctime = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)?
.as_secs();
let ctime = epoch_now_u64()?;
let uuid = Uuid::generate();
@ -469,6 +452,18 @@ impl FixedIndexWriter {
Ok(())
}
pub fn clone_data_from(&mut self, reader: &FixedIndexReader) -> Result<(), Error> {
if self.index_length != reader.index_count() {
bail!("clone_data_from failed - index sizes not equal");
}
for i in 0..self.index_length {
self.add_digest(i, reader.index_digest(i).unwrap())?;
}
Ok(())
}
}
pub struct BufferedFixedReader<S> {
@ -501,18 +496,17 @@ impl<S: ReadChunk> BufferedFixedReader<S> {
fn buffer_chunk(&mut self, idx: usize) -> Result<(), Error> {
let index = &self.index;
let (start, end, digest) = index.chunk_info(idx)?;
let info = match index.chunk_info(idx) {
Some(info) => info,
None => bail!("chunk index out of range"),
};
// fixme: avoid copy
let data = self.store.read_chunk(&digest)?;
if (end - start) != data.len() as u64 {
bail!(
"read chunk with wrong size ({} != {}",
(end - start),
data.len()
);
let data = self.store.read_chunk(&info.digest)?;
let size = info.range.end - info.range.start;
if size != data.len() as u64 {
bail!("read chunk with wrong size ({} != {}", size, data.len());
}
self.read_buffer.clear();
@ -520,8 +514,7 @@ impl<S: ReadChunk> BufferedFixedReader<S> {
self.buffered_chunk_idx = idx;
self.buffered_chunk_start = start as u64;
//println!("BUFFER {} {}", self.buffered_chunk_start, end);
self.buffered_chunk_start = info.range.start as u64;
Ok(())
}
}

View File

@ -1,10 +1,17 @@
use std::collections::HashMap;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::ops::Range;
use bytes::{Bytes, BytesMut};
use failure::*;
use futures::*;
pub struct ChunkReadInfo {
pub range: Range<u64>,
pub digest: [u8; 32],
}
impl ChunkReadInfo {
#[inline]
pub fn size(&self) -> u64 {
self.range.end - self.range.start
}
}
/// Trait to get digest list from index files
///
@ -13,6 +20,10 @@ pub trait IndexFile {
fn index_count(&self) -> usize;
fn index_digest(&self, pos: usize) -> Option<&[u8; 32]>;
fn index_bytes(&self) -> u64;
fn chunk_info(&self, pos: usize) -> Option<ChunkReadInfo>;
/// Compute index checksum and size
fn compute_csum(&self) -> ([u8; 32], u64);
/// Returns most often used chunks
fn find_most_used_chunks(&self, max: usize) -> HashMap<[u8; 32], usize> {
@ -46,111 +57,3 @@ pub trait IndexFile {
map
}
}
/// Encode digest list from an `IndexFile` into a binary stream
///
/// The reader simply returns a birary stream of 32 byte digest values.
pub struct DigestListEncoder {
index: Box<dyn IndexFile + Send + Sync>,
pos: usize,
count: usize,
}
impl DigestListEncoder {
pub fn new(index: Box<dyn IndexFile + Send + Sync>) -> Self {
let count = index.index_count();
Self { index, pos: 0, count }
}
}
impl std::io::Read for DigestListEncoder {
fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
if buf.len() < 32 {
panic!("read buffer too small");
}
if self.pos < self.count {
let mut written = 0;
loop {
let digest = self.index.index_digest(self.pos).unwrap();
buf[written..(written + 32)].copy_from_slice(digest);
self.pos += 1;
written += 32;
if self.pos >= self.count {
break;
}
if (written + 32) >= buf.len() {
break;
}
}
Ok(written)
} else {
Ok(0)
}
}
}
/// Decodes a Stream<Item=Bytes> into Stream<Item=<[u8;32]>
///
/// The reader simply returns a birary stream of 32 byte digest values.
pub struct DigestListDecoder<S: Unpin> {
input: S,
buffer: BytesMut,
}
impl<S: Unpin> DigestListDecoder<S> {
pub fn new(input: S) -> Self {
Self { input, buffer: BytesMut::new() }
}
}
impl<S: Unpin> Unpin for DigestListDecoder<S> {}
impl<S: Unpin, E> Stream for DigestListDecoder<S>
where
S: Stream<Item=Result<Bytes, E>>,
E: Into<Error>,
{
type Item = Result<[u8; 32], Error>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
if this.buffer.len() >= 32 {
let left = this.buffer.split_to(32);
let mut digest = std::mem::MaybeUninit::<[u8; 32]>::uninit();
unsafe {
(*digest.as_mut_ptr()).copy_from_slice(&left[..]);
return Poll::Ready(Some(Ok(digest.assume_init())));
}
}
match Pin::new(&mut this.input).poll_next(cx) {
Poll::Pending => {
return Poll::Pending;
}
Poll::Ready(Some(Err(err))) => {
return Poll::Ready(Some(Err(err.into())));
}
Poll::Ready(Some(Ok(data))) => {
this.buffer.extend_from_slice(&data);
// continue
}
Poll::Ready(None) => {
let rest = this.buffer.len();
if rest == 0 {
return Poll::Ready(None);
}
return Poll::Ready(Some(Err(format_err!(
"got small digest ({} != 32).",
rest,
))));
}
}
}
}
}

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, format_err, Error};
use serde::{Deserialize, Serialize};
use chrono::{Local, TimeZone, DateTime};

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, format_err, Error};
use std::convert::TryFrom;
use std::path::Path;
@ -7,9 +7,11 @@ use serde_json::{json, Value};
use crate::backup::BackupDir;
pub const MANIFEST_BLOB_NAME: &str = "index.json.blob";
pub const CLIENT_LOG_BLOB_NAME: &str = "client.log.blob";
pub struct FileInfo {
pub filename: String,
pub encrypted: Option<bool>,
pub size: u64,
pub csum: [u8; 32],
}
@ -47,9 +49,9 @@ impl BackupManifest {
Self { files: Vec::new(), snapshot }
}
pub fn add_file(&mut self, filename: String, size: u64, csum: [u8; 32]) -> Result<(), Error> {
pub fn add_file(&mut self, filename: String, size: u64, csum: [u8; 32], encrypted: Option<bool>) -> Result<(), Error> {
let _archive_type = archive_type(&filename)?; // check type
self.files.push(FileInfo { filename, size, csum });
self.files.push(FileInfo { filename, size, csum, encrypted });
Ok(())
}
@ -72,7 +74,7 @@ impl BackupManifest {
let info = self.lookup_file_info(name)?;
if size != info.size {
bail!("wrong size for file '{}' ({} != {}", name, info.size, size);
bail!("wrong size for file '{}' ({} != {})", name, info.size, size);
}
if csum != &info.csum {
@ -89,11 +91,18 @@ impl BackupManifest {
"backup-time": self.snapshot.backup_time().timestamp(),
"files": self.files.iter()
.fold(Vec::new(), |mut acc, info| {
acc.push(json!({
let mut value = json!({
"filename": info.filename,
"encrypted": info.encrypted,
"size": info.size,
"csum": proxmox::tools::digest_to_hex(&info.csum),
}));
});
if let Some(encrypted) = info.encrypted {
value["encrypted"] = encrypted.into();
}
acc.push(value);
acc
})
})
@ -133,7 +142,8 @@ impl TryFrom<Value> for BackupManifest {
let csum = required_string_property(item, "csum")?;
let csum = proxmox::tools::hex_to_digest(csum)?;
let size = required_integer_property(item, "size")? as u64;
manifest.add_file(filename, size, csum)?;
let encrypted = item["encrypted"].as_bool();
manifest.add_file(filename, size, csum, encrypted)?;
}
if manifest.files().is_empty() {

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;

View File

@ -1,38 +1,39 @@
use failure::*;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use super::datastore::*;
use super::crypt_config::*;
use super::data_blob::*;
use anyhow::Error;
use super::crypt_config::CryptConfig;
use super::data_blob::DataBlob;
use super::datastore::DataStore;
/// The ReadChunk trait allows reading backup data chunks (local or remote)
pub trait ReadChunk {
/// Returns the encoded chunk data
fn read_raw_chunk(&mut self, digest:&[u8; 32]) -> Result<DataBlob, Error>;
fn read_raw_chunk(&self, digest: &[u8; 32]) -> Result<DataBlob, Error>;
/// Returns the decoded chunk data
fn read_chunk(&mut self, digest:&[u8; 32]) -> Result<Vec<u8>, Error>;
fn read_chunk(&self, digest: &[u8; 32]) -> Result<Vec<u8>, Error>;
}
#[derive(Clone)]
pub struct LocalChunkReader {
store: Arc<DataStore>,
crypt_config: Option<Arc<CryptConfig>>,
}
impl LocalChunkReader {
pub fn new(store: Arc<DataStore>, crypt_config: Option<Arc<CryptConfig>>) -> Self {
Self { store, crypt_config }
Self {
store,
crypt_config,
}
}
}
impl ReadChunk for LocalChunkReader {
fn read_raw_chunk(&mut self, digest:&[u8; 32]) -> Result<DataBlob, Error> {
let digest_str = proxmox::tools::digest_to_hex(digest);
println!("READ CHUNK {}", digest_str);
fn read_raw_chunk(&self, digest: &[u8; 32]) -> Result<DataBlob, Error> {
let (path, _) = self.store.chunk_path(digest);
let raw_data = proxmox::tools::fs::file_get_contents(&path)?;
let chunk = DataBlob::from_raw(raw_data)?;
@ -41,13 +42,59 @@ impl ReadChunk for LocalChunkReader {
Ok(chunk)
}
fn read_chunk(&mut self, digest:&[u8; 32]) -> Result<Vec<u8>, Error> {
let chunk = self.read_raw_chunk(digest)?;
fn read_chunk(&self, digest: &[u8; 32]) -> Result<Vec<u8>, Error> {
let chunk = ReadChunk::read_raw_chunk(self, digest)?;
let raw_data = chunk.decode(self.crypt_config.as_ref().map(Arc::as_ref))?;
let raw_data = chunk.decode(self.crypt_config.as_ref().map(Arc::as_ref))?;
// fixme: verify digest?
Ok(raw_data)
}
}
pub trait AsyncReadChunk: Send {
/// Returns the encoded chunk data
fn read_raw_chunk<'a>(
&'a self,
digest: &'a [u8; 32],
) -> Pin<Box<dyn Future<Output = Result<DataBlob, Error>> + Send + 'a>>;
/// Returns the decoded chunk data
fn read_chunk<'a>(
&'a self,
digest: &'a [u8; 32],
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, Error>> + Send + 'a>>;
}
impl AsyncReadChunk for LocalChunkReader {
fn read_raw_chunk<'a>(
&'a self,
digest: &'a [u8; 32],
) -> Pin<Box<dyn Future<Output = Result<DataBlob, Error>> + Send + 'a>> {
Box::pin(async move{
let (path, _) = self.store.chunk_path(digest);
let raw_data = tokio::fs::read(&path).await?;
let chunk = DataBlob::from_raw(raw_data)?;
chunk.verify_crc()?;
Ok(chunk)
})
}
fn read_chunk<'a>(
&'a self,
digest: &'a [u8; 32],
) -> Pin<Box<dyn Future<Output = Result<Vec<u8>, Error>> + Send + 'a>> {
Box::pin(async move {
let chunk = AsyncReadChunk::read_raw_chunk(self, digest).await?;
let raw_data = chunk.decode(self.crypt_config.as_ref().map(Arc::as_ref))?;
// fixme: verify digest?
Ok(raw_data)
})
}
}

196
src/backup/verify.rs Normal file
View File

@ -0,0 +1,196 @@
use anyhow::{bail, Error};
use crate::server::WorkerTask;
use super::{
DataStore, BackupGroup, BackupDir, BackupInfo, IndexFile,
ENCR_COMPR_BLOB_MAGIC_1_0, ENCRYPTED_BLOB_MAGIC_1_0,
FileInfo, ArchiveType, archive_type,
};
fn verify_blob(datastore: &DataStore, backup_dir: &BackupDir, info: &FileInfo) -> Result<(), Error> {
let (blob, raw_size) = datastore.load_blob(backup_dir, &info.filename)?;
let csum = openssl::sha::sha256(blob.raw_data());
if raw_size != info.size {
bail!("wrong size ({} != {})", info.size, raw_size);
}
if csum != info.csum {
bail!("wrong index checksum");
}
blob.verify_crc()?;
let magic = blob.magic();
if magic == &ENCR_COMPR_BLOB_MAGIC_1_0 || magic == &ENCRYPTED_BLOB_MAGIC_1_0 {
return Ok(());
}
blob.decode(None)?;
Ok(())
}
fn verify_index_chunks(
datastore: &DataStore,
index: Box<dyn IndexFile>,
worker: &WorkerTask,
) -> Result<(), Error> {
for pos in 0..index.index_count() {
worker.fail_on_abort()?;
let info = index.chunk_info(pos).unwrap();
let size = info.range.end - info.range.start;
datastore.verify_stored_chunk(&info.digest, size)?;
}
Ok(())
}
fn verify_fixed_index(datastore: &DataStore, backup_dir: &BackupDir, info: &FileInfo, worker: &WorkerTask) -> Result<(), Error> {
let mut path = backup_dir.relative_path();
path.push(&info.filename);
let index = datastore.open_fixed_reader(&path)?;
let (csum, size) = index.compute_csum();
if size != info.size {
bail!("wrong size ({} != {})", info.size, size);
}
if csum != info.csum {
bail!("wrong index checksum");
}
verify_index_chunks(datastore, Box::new(index), worker)
}
fn verify_dynamic_index(datastore: &DataStore, backup_dir: &BackupDir, info: &FileInfo, worker: &WorkerTask) -> Result<(), Error> {
let mut path = backup_dir.relative_path();
path.push(&info.filename);
let index = datastore.open_dynamic_reader(&path)?;
let (csum, size) = index.compute_csum();
if size != info.size {
bail!("wrong size ({} != {})", info.size, size);
}
if csum != info.csum {
bail!("wrong index checksum");
}
verify_index_chunks(datastore, Box::new(index), worker)
}
/// Verify a single backup snapshot
///
/// This checks all archives inside a backup snapshot.
/// Errors are logged to the worker log.
///
/// Returns
/// - Ok(true) if verify is successful
/// - Ok(false) if there were verification errors
/// - Err(_) if task was aborted
pub fn verify_backup_dir(datastore: &DataStore, backup_dir: &BackupDir, worker: &WorkerTask) -> Result<bool, Error> {
let manifest = match datastore.load_manifest(&backup_dir) {
Ok((manifest, _)) => manifest,
Err(err) => {
worker.log(format!("verify {}:{} - manifest load error: {}", datastore.name(), backup_dir, err));
return Ok(false);
}
};
worker.log(format!("verify {}:{}", datastore.name(), backup_dir));
let mut error_count = 0;
for info in manifest.files() {
let result = proxmox::try_block!({
worker.log(format!(" check {}", info.filename));
match archive_type(&info.filename)? {
ArchiveType::FixedIndex => verify_fixed_index(&datastore, &backup_dir, info, worker),
ArchiveType::DynamicIndex => verify_dynamic_index(&datastore, &backup_dir, info, worker),
ArchiveType::Blob => verify_blob(&datastore, &backup_dir, info),
}
});
worker.fail_on_abort()?;
if let Err(err) = result {
worker.log(format!("verify {}:{}/{} failed: {}", datastore.name(), backup_dir, info.filename, err));
error_count += 1;
}
}
Ok(error_count == 0)
}
/// Verify all backups inside a backup group
///
/// Errors are logged to the worker log.
///
/// Returns
/// - Ok(true) if verify is successful
/// - Ok(false) if there were verification errors
/// - Err(_) if task was aborted
pub fn verify_backup_group(datastore: &DataStore, group: &BackupGroup, worker: &WorkerTask) -> Result<bool, Error> {
let mut list = match group.list_backups(&datastore.base_path()) {
Ok(list) => list,
Err(err) => {
worker.log(format!("verify group {}:{} - unable to list backups: {}", datastore.name(), group, err));
return Ok(false);
}
};
worker.log(format!("verify group {}:{}", datastore.name(), group));
let mut error_count = 0;
BackupInfo::sort_list(&mut list, false); // newest first
for info in list {
if !verify_backup_dir(datastore, &info.backup_dir, worker)? {
error_count += 1;
}
}
Ok(error_count == 0)
}
/// Verify all backups inside a datastore
///
/// Errors are logged to the worker log.
///
/// Returns
/// - Ok(true) if verify is successful
/// - Ok(false) if there were verification errors
/// - Err(_) if task was aborted
pub fn verify_all_backups(datastore: &DataStore, worker: &WorkerTask) -> Result<bool, Error> {
let list = match BackupGroup::list_groups(&datastore.base_path()) {
Ok(list) => list,
Err(err) => {
worker.log(format!("verify datastore {} - unable to list backups: {}", datastore.name(), err));
return Ok(false);
}
};
worker.log(format!("verify datastore {}", datastore.name()));
let mut error_count = 0;
for group in list {
if !verify_backup_group(datastore, &group, worker)? {
error_count += 1;
}
}
Ok(error_count == 0)
}

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use proxmox::api::format::*;

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use proxmox::api::format::*;
use proxmox::api::cli::*;

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{Error};
use proxmox::api::format::dump_api;

View File

@ -1,4 +1,4 @@
use failure::*;
use anyhow::{bail, Error};
use futures::*;
use proxmox::try_block;
@ -14,6 +14,8 @@ use proxmox_backup::config;
use proxmox_backup::buildcfg;
fn main() {
proxmox_backup::tools::setup_safe_path_env();
if let Err(err) = proxmox_backup::tools::runtime::main(run()) {
eprintln!("Error: {}", err);
std::process::exit(-1);
@ -34,6 +36,8 @@ async fn run() -> Result<(), Error> {
config::update_self_signed_cert(false)?;
proxmox_backup::rrd::create_rrdb_dir()?;
if let Err(err) = generate_auth_key() {
bail!("unable to generate auth key - {}", err);
}
@ -45,8 +49,8 @@ async fn run() -> Result<(), Error> {
let _ = csrf_secret(); // load with lazy_static
let config = server::ApiConfig::new(
buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PRIVILEGED);
buildcfg::JS_DIR, &proxmox_backup::api2::ROUTER, RpcEnvironmentType::PRIVILEGED)?;
let rest_server = RestServer::new(config);
// http server future:
@ -86,6 +90,6 @@ async fn run() -> Result<(), Error> {
proxmox_backup::server::last_worker_future().await?;
log::info!("done - exit server");
Ok(())
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More