Compare commits
39 Commits
Author | SHA1 | Date | |
---|---|---|---|
399e48a1ed | |||
7ae571e7cb | |||
4264c5023b | |||
82b7adf90b | |||
71c4a3138f | |||
52991f239f | |||
3435f5491b | |||
aafe8609e5 | |||
a8d69fcf05 | |||
1e68497c03 | |||
74fc844787 | |||
4cda7603c4 | |||
11e1e27a42 | |||
4ea831bfa1 | |||
c1d7d708d4 | |||
3fa2b983c1 | |||
a1e9c05738 | |||
934deeff2d | |||
c162df60c8 | |||
98161fddb5 | |||
be614c625f | |||
87c4cb7419 | |||
93bb51fe7e | |||
713b66b6ed | |||
77bd2a469c | |||
97af919530 | |||
c91602316b | |||
a13573c24a | |||
02543a5c7f | |||
42b68f72e6 | |||
664d8a2765 | |||
e6263c2662 | |||
ae197dda23 | |||
4c116bafb8 | |||
df30017ff8 | |||
3f3ae19d63 | |||
72dc68323c | |||
593f917742 | |||
639419b049 |
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "proxmox-backup"
|
name = "proxmox-backup"
|
||||||
version = "0.8.10"
|
version = "0.8.12"
|
||||||
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
|
authors = ["Dietmar Maurer <dietmar@proxmox.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3"
|
license = "AGPL-3"
|
||||||
@ -43,7 +43,7 @@ proxmox = { version = "0.3.3", features = [ "sortable-macro", "api-macro", "webs
|
|||||||
#proxmox = { git = "ssh://gitolite3@proxdev.maurer-it.com/rust/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] }
|
#proxmox = { git = "ssh://gitolite3@proxdev.maurer-it.com/rust/proxmox", version = "0.1.2", features = [ "sortable-macro", "api-macro" ] }
|
||||||
#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
#proxmox = { path = "../proxmox/proxmox", features = [ "sortable-macro", "api-macro", "websocket" ] }
|
||||||
proxmox-fuse = "0.1.0"
|
proxmox-fuse = "0.1.0"
|
||||||
pxar = { version = "0.3.0", features = [ "tokio-io", "futures-io" ] }
|
pxar = { version = "0.4.0", features = [ "tokio-io", "futures-io" ] }
|
||||||
#pxar = { path = "../pxar", features = [ "tokio-io", "futures-io" ] }
|
#pxar = { path = "../pxar", features = [ "tokio-io", "futures-io" ] }
|
||||||
regex = "1.2"
|
regex = "1.2"
|
||||||
rustyline = "6"
|
rustyline = "6"
|
||||||
|
28
debian/changelog
vendored
28
debian/changelog
vendored
@ -1,3 +1,31 @@
|
|||||||
|
rust-proxmox-backup (0.8.12-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* verify: speedup - only verify chunks once
|
||||||
|
|
||||||
|
* verify: sort backup groups
|
||||||
|
|
||||||
|
* bump pxar dep to 0.4.0
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Tue, 25 Aug 2020 08:55:52 +0200
|
||||||
|
|
||||||
|
rust-proxmox-backup (0.8.11-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* improve sync jobs, allow to stop them and better logging
|
||||||
|
|
||||||
|
* fix #2926: make network interfaces parser more flexible
|
||||||
|
|
||||||
|
* fix #2904: zpool status: parse also those vdevs without READ/ẀRITE/...
|
||||||
|
statistics
|
||||||
|
|
||||||
|
* api2/node/services: turn service api calls into workers
|
||||||
|
|
||||||
|
* docs: add sections describing ACL related commands and describing
|
||||||
|
benchmarking
|
||||||
|
|
||||||
|
* docs: general grammar, wording and typo improvements
|
||||||
|
|
||||||
|
-- Proxmox Support Team <support@proxmox.com> Wed, 19 Aug 2020 19:20:03 +0200
|
||||||
|
|
||||||
rust-proxmox-backup (0.8.10-1) unstable; urgency=medium
|
rust-proxmox-backup (0.8.10-1) unstable; urgency=medium
|
||||||
|
|
||||||
* ui: acl: add improved permission selector
|
* ui: acl: add improved permission selector
|
||||||
|
7
debian/postinst
vendored
7
debian/postinst
vendored
@ -14,6 +14,13 @@ case "$1" in
|
|||||||
_dh_action=start
|
_dh_action=start
|
||||||
fi
|
fi
|
||||||
deb-systemd-invoke $_dh_action proxmox-backup.service proxmox-backup-proxy.service >/dev/null || true
|
deb-systemd-invoke $_dh_action proxmox-backup.service proxmox-backup-proxy.service >/dev/null || true
|
||||||
|
|
||||||
|
if test -n "$2"; then
|
||||||
|
if dpkg --compare-versions "$2" 'le' '0.8.10-1'; then
|
||||||
|
echo "Fixing up termproxy user id in task log..."
|
||||||
|
flock -w 30 /var/log/proxmox-backup/tasks/active.lock sed -i 's/:termproxy::root: /:termproxy::root@pam: /' /var/log/proxmox-backup/tasks/active
|
||||||
|
fi
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
||||||
abort-upgrade|abort-remove|abort-deconfigure)
|
abort-upgrade|abort-remove|abort-deconfigure)
|
||||||
|
@ -344,10 +344,10 @@ following roles exist:
|
|||||||
Disable Access - nothing is allowed.
|
Disable Access - nothing is allowed.
|
||||||
|
|
||||||
**Admin**
|
**Admin**
|
||||||
The Administrator can do anything.
|
Can do anything.
|
||||||
|
|
||||||
**Audit**
|
**Audit**
|
||||||
An Auditor can view things, but is not allowed to change settings.
|
Can view things, but is not allowed to change settings.
|
||||||
|
|
||||||
**DatastoreAdmin**
|
**DatastoreAdmin**
|
||||||
Can do anything on datastores.
|
Can do anything on datastores.
|
||||||
@ -356,10 +356,10 @@ following roles exist:
|
|||||||
Can view datastore settings and list content. But
|
Can view datastore settings and list content. But
|
||||||
is not allowed to read the actual data.
|
is not allowed to read the actual data.
|
||||||
|
|
||||||
**DataStoreReader**
|
**DatastoreReader**
|
||||||
Can Inspect datastore content and can do restores.
|
Can Inspect datastore content and can do restores.
|
||||||
|
|
||||||
**DataStoreBackup**
|
**DatastoreBackup**
|
||||||
Can backup and restore owned backups.
|
Can backup and restore owned backups.
|
||||||
|
|
||||||
**DatastorePowerUser**
|
**DatastorePowerUser**
|
||||||
@ -374,6 +374,35 @@ following roles exist:
|
|||||||
**RemoteSyncOperator**
|
**RemoteSyncOperator**
|
||||||
Is allowed to read data from a remote.
|
Is allowed to read data from a remote.
|
||||||
|
|
||||||
|
You can use the ``acl`` subcommand to manage and monitor user permissions. For
|
||||||
|
example, the command below will add the user ``john@pbs`` as a
|
||||||
|
**DatastoreAdmin** for the data store ``store1``, located at ``/backup/disk1/store1``:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# proxmox-backup-manager acl update /datastore/store1 DatastoreAdmin --userid john@pbs
|
||||||
|
|
||||||
|
You can monitor the roles of each user using the following command:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# proxmox-backup-manager acl list
|
||||||
|
┌──────────┬──────────────────┬───────────┬────────────────┐
|
||||||
|
│ ugid │ path │ propagate │ roleid │
|
||||||
|
╞══════════╪══════════════════╪═══════════╪════════════════╡
|
||||||
|
│ john@pbs │ /datastore/disk1 │ 1 │ DatastoreAdmin │
|
||||||
|
└──────────┴──────────────────┴───────────┴────────────────┘
|
||||||
|
|
||||||
|
A single user can be assigned multiple permission sets for different data stores.
|
||||||
|
|
||||||
|
.. Note::
|
||||||
|
Naming convention is important here. For data stores on the host,
|
||||||
|
you must use the convention ``/datastore/{storename}``. For example, to set
|
||||||
|
permissions for a data store mounted at ``/mnt/backup/disk4/store2``, you would use
|
||||||
|
``/datastore/store2`` for the path. For remote stores, use the convention
|
||||||
|
``/remote/{remote}/{storename}``, where ``{remote}`` signifies the name of the
|
||||||
|
remote (see `Remote` below) and ``{storename}`` is the name of the data store on
|
||||||
|
the remote.
|
||||||
|
|
||||||
:term:`Remote`
|
:term:`Remote`
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
@ -543,7 +572,9 @@ This will prompt you for a password and then uploads a file archive named
|
|||||||
|
|
||||||
The ``--repository`` option can get 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
|
commands. You can avoid having to enter this value by setting the
|
||||||
environment variable ``PBS_REPOSITORY``.
|
environment variable ``PBS_REPOSITORY``. Note that if you would like this to remain set
|
||||||
|
over multiple sessions, you should instead add the below line to your
|
||||||
|
``.bashrc`` file.
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
@ -578,7 +609,7 @@ Excluding files/folders from a backup
|
|||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Sometimes it is desired to exclude certain files or folders from a backup archive.
|
Sometimes it is desired to exclude certain files or folders from a backup archive.
|
||||||
To tell the Proxmox backup client when and how to ignore files and directories,
|
To tell the Proxmox Backup client when and how to ignore files and directories,
|
||||||
place a text file called ``.pxarexclude`` in the filesystem hierarchy.
|
place a text file called ``.pxarexclude`` in the filesystem hierarchy.
|
||||||
Whenever the backup client encounters such a file in a directory, it interprets
|
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
|
each line as glob match patterns for files and directories that are to be excluded
|
||||||
@ -775,7 +806,9 @@ To set up a master key:
|
|||||||
backed up. It can happen, for example, that you back up an entire system, using
|
backed up. It can happen, for example, that you back up an entire system, using
|
||||||
a key on that system. If the system then becomes inaccessable for any reason
|
a key on that system. If the system then becomes inaccessable for any reason
|
||||||
and needs to be restored, this will not be possible as the encryption key will be
|
and needs to be restored, this will not be possible as the encryption key will be
|
||||||
lost along with the broken system.
|
lost along with the broken system. In preparation for the worst case scenario,
|
||||||
|
you should consider keeping a paper copy of this key locked away in
|
||||||
|
a safe place.
|
||||||
|
|
||||||
Restoring Data
|
Restoring Data
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
@ -818,7 +851,7 @@ backup.
|
|||||||
|
|
||||||
# proxmox-backup-client restore host/elsa/2019-12-03T09:35:01Z root.pxar /target/path/
|
# proxmox-backup-client restore host/elsa/2019-12-03T09:35:01Z root.pxar /target/path/
|
||||||
|
|
||||||
To get the contents of any archive, you can restore the ``ìndex.json`` file in the
|
To get the contents of any archive, you can restore the ``index.json`` file in the
|
||||||
repository to the target path '-'. This will dump the contents to the standard output.
|
repository to the target path '-'. This will dump the contents to the standard output.
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
@ -900,8 +933,8 @@ file archive as a read-only filesystem to a mountpoint on your host.
|
|||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
# proxmox-backup-client mount host/backup-client/2020-01-29T11:29:22Z root.pxar /mnt
|
# proxmox-backup-client mount host/backup-client/2020-01-29T11:29:22Z root.pxar /mnt/mountpoint
|
||||||
# ls /mnt
|
# ls /mnt/mountpoint
|
||||||
bin dev home lib32 libx32 media opt root sbin sys usr
|
bin dev home lib32 libx32 media opt root sbin sys usr
|
||||||
boot etc lib lib64 lost+found mnt proc run srv tmp var
|
boot etc lib lib64 lost+found mnt proc run srv tmp var
|
||||||
|
|
||||||
@ -916,7 +949,7 @@ To unmount the filesystem use the ``umount`` command on the mountpoint:
|
|||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
# umount /mnt
|
# umount /mnt/mountpoint
|
||||||
|
|
||||||
Login and Logout
|
Login and Logout
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
@ -959,8 +992,8 @@ command:
|
|||||||
snapshot. They will be inaccessible and unrecoverable.
|
snapshot. They will be inaccessible and unrecoverable.
|
||||||
|
|
||||||
|
|
||||||
The manual removal is sometimes required, but normally the prune
|
Although manual removal is sometimes required, the ``prune``
|
||||||
command is used to systematically delete older backups. Prune lets
|
command is normally used to systematically delete older backups. Prune lets
|
||||||
you specify which backup snapshots you want to keep. The
|
you specify which backup snapshots you want to keep. The
|
||||||
following retention options are available:
|
following retention options are available:
|
||||||
|
|
||||||
@ -1080,6 +1113,38 @@ unused data blocks are removed.
|
|||||||
|
|
||||||
.. todo:: howto run garbage-collection at regular intervalls (cron)
|
.. todo:: howto run garbage-collection at regular intervalls (cron)
|
||||||
|
|
||||||
|
Benchmarking
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
The backup client also comes with a benchmarking tool. This tool measures
|
||||||
|
various metrics relating to compression and encryption speeds. You can run a
|
||||||
|
benchmark using the ``benchmark`` subcommand of ``proxmox-backup-client``:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
# proxmox-backup-client benchmark
|
||||||
|
Uploaded 656 chunks in 5 seconds.
|
||||||
|
Time per request: 7659 microseconds.
|
||||||
|
TLS speed: 547.60 MB/s
|
||||||
|
SHA256 speed: 585.76 MB/s
|
||||||
|
Compression speed: 1923.96 MB/s
|
||||||
|
Decompress speed: 7885.24 MB/s
|
||||||
|
AES256/GCM speed: 3974.03 MB/s
|
||||||
|
┌───────────────────────────────────┬─────────────────────┐
|
||||||
|
│ Name │ Value │
|
||||||
|
╞═══════════════════════════════════╪═════════════════════╡
|
||||||
|
│ TLS (maximal backup upload speed) │ 547.60 MB/s (93%) │
|
||||||
|
├───────────────────────────────────┼─────────────────────┤
|
||||||
|
│ SHA256 checksum computation speed │ 585.76 MB/s (28%) │
|
||||||
|
├───────────────────────────────────┼─────────────────────┤
|
||||||
|
│ ZStd level 1 compression speed │ 1923.96 MB/s (89%) │
|
||||||
|
├───────────────────────────────────┼─────────────────────┤
|
||||||
|
│ ZStd level 1 decompression speed │ 7885.24 MB/s (98%) │
|
||||||
|
├───────────────────────────────────┼─────────────────────┤
|
||||||
|
│ AES256 GCM encryption speed │ 3974.03 MB/s (104%) │
|
||||||
|
└───────────────────────────────────┴─────────────────────┘
|
||||||
|
|
||||||
|
You can also pass the ``--output-format`` parameter to output stats in ``json``,
|
||||||
|
rather than the default table format.
|
||||||
|
|
||||||
.. _pve-integration:
|
.. _pve-integration:
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
.. _Proxmox: https://www.proxmox.com
|
.. _Proxmox: https://www.proxmox.com
|
||||||
.. _Proxmox Community Forum: https://forum.proxmox.com
|
.. _Proxmox Community Forum: https://forum.proxmox.com
|
||||||
.. _Proxmox Virtual Environment: https://www.proxmox.com/proxmox-ve
|
.. _Proxmox Virtual Environment: https://www.proxmox.com/proxmox-ve
|
||||||
.. _Proxmox Backup: https://pbs.proxmox.com/wiki/index.php/Main_Page // FIXME
|
// FIXME
|
||||||
|
.. _Proxmox Backup: https://pbs.proxmox.com/wiki/index.php/Main_Page
|
||||||
.. _PBS Development List: https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
|
.. _PBS Development List: https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
|
||||||
.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html
|
.. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html
|
||||||
.. _Rust: https://www.rust-lang.org/
|
.. _Rust: https://www.rust-lang.org/
|
||||||
|
@ -19,9 +19,9 @@ for various management tasks such as disk management.
|
|||||||
The disk image (ISO file) provided by Proxmox includes a complete Debian system
|
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.
|
("buster" for version 1.x) as well as all necessary packages for the `Proxmox Backup`_ server.
|
||||||
|
|
||||||
The installer will guide you through the setup process and allows
|
The installer will guide you through the setup process and allow
|
||||||
you to partition the local disk(s), apply basic system configurations
|
you to partition the local disk(s), apply basic system configurations
|
||||||
(e.g. timezone, language, network), and installs all required packages.
|
(e.g. timezone, language, network), and install all required packages.
|
||||||
The provided ISO will get you started in just a few minutes, and is the
|
The provided ISO will get you started in just a few minutes, and is the
|
||||||
recommended method for new and existing users.
|
recommended method for new and existing users.
|
||||||
|
|
||||||
@ -36,11 +36,11 @@ It includes the following:
|
|||||||
|
|
||||||
* The `Proxmox Backup`_ server installer, which partitions the local
|
* The `Proxmox Backup`_ server installer, which partitions the local
|
||||||
disk(s) with ext4, ext3, xfs or ZFS, and installs the operating
|
disk(s) with ext4, ext3, xfs or ZFS, and installs the operating
|
||||||
system.
|
system
|
||||||
|
|
||||||
* Complete operating system (Debian Linux, 64-bit)
|
* Complete operating system (Debian Linux, 64-bit)
|
||||||
|
|
||||||
* Our Linux kernel with ZFS support.
|
* Our Linux kernel with ZFS support
|
||||||
|
|
||||||
* Complete tool-set to administer backups and all necessary resources
|
* Complete tool-set to administer backups and all necessary resources
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ Install `Proxmox Backup`_ server on Debian
|
|||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Proxmox ships as a set of Debian packages which can be installed on top of a
|
Proxmox ships as a set of Debian packages which can be installed on top of a
|
||||||
standard Debian installation. After configuring the
|
standard Debian installation. After configuring the
|
||||||
:ref:`sysadmin_package_repositories`, you need to run:
|
:ref:`sysadmin_package_repositories`, you need to run:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
@ -76,12 +76,11 @@ does, please use the following:
|
|||||||
This will install all required packages, the Proxmox kernel with ZFS_
|
This will install all required packages, the Proxmox kernel with ZFS_
|
||||||
support, and a set of common and useful packages.
|
support, and a set of common and useful packages.
|
||||||
|
|
||||||
Installing `Proxmox Backup`_ on top of an existing Debian_ installation looks easy, but
|
.. caution:: Installing `Proxmox Backup`_ on top of an existing Debian_
|
||||||
it presumes that the base system and local storage has been set up correctly.
|
installation looks easy, but it assumes that the base system and local
|
||||||
|
storage have been set up correctly. In general this is not trivial, especially
|
||||||
In general this is not trivial, especially when LVM_ or ZFS_ is used.
|
when LVM_ or ZFS_ is used. The network configuration is completely up to you
|
||||||
|
as well.
|
||||||
The network configuration is completely up to you as well.
|
|
||||||
|
|
||||||
.. note:: You can access the webinterface of the Proxmox Backup Server with
|
.. note:: You can access the webinterface of the Proxmox Backup Server with
|
||||||
your web browser, using HTTPS on port 8007. For example at
|
your web browser, using HTTPS on port 8007. For example at
|
||||||
@ -103,9 +102,9 @@ After configuring the
|
|||||||
server to store backups. Should the hypervisor server fail, you can
|
server to store backups. Should the hypervisor server fail, you can
|
||||||
still access the backups.
|
still access the backups.
|
||||||
|
|
||||||
.. note:: You can access the webinterface of the Proxmox Backup Server with
|
.. note::
|
||||||
your web browser, using HTTPS on port 8007. For example at
|
You can access the webinterface of the Proxmox Backup Server with your web
|
||||||
``https://<ip-or-dns-name>:8007``
|
browser, using HTTPS on port 8007. For example at ``https://<ip-or-dns-name>:8007``
|
||||||
|
|
||||||
Client installation
|
Client installation
|
||||||
-------------------
|
-------------------
|
||||||
|
@ -22,7 +22,7 @@ Architecture
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
Proxmox Backup Server uses a `client-server model`_. The server stores the
|
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
|
backup data and provides an API to create and manage data stores. With the
|
||||||
API, it's also possible to manage disks and other server-side resources.
|
API, it's also possible to manage disks and other server-side resources.
|
||||||
|
|
||||||
The backup client uses this API to access the backed up data. With the command
|
The backup client uses this API to access the backed up data. With the command
|
||||||
@ -143,6 +143,7 @@ Mailing Lists
|
|||||||
|
|
||||||
Proxmox Backup Server is fully open-source and contributions are welcome! Here
|
Proxmox Backup Server is fully open-source and contributions are welcome! Here
|
||||||
is the primary communication channel for developers:
|
is the primary communication channel for developers:
|
||||||
|
|
||||||
:Mailing list for developers: `PBS Development List`_
|
:Mailing list for developers: `PBS Development List`_
|
||||||
|
|
||||||
Bug Tracker
|
Bug Tracker
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
Debian Package Repositories
|
Debian Package Repositories
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
All Debian based systems use APT_ as package management tool. The list of
|
All Debian based systems use APT_ as a package management tool. The lists of
|
||||||
repositories is defined in ``/etc/apt/sources.list`` and ``.list`` files found
|
repositories are defined in ``/etc/apt/sources.list`` and the ``.list`` files found
|
||||||
in the ``/etc/apt/sources.d/`` directory. Updates can be installed directly
|
in the ``/etc/apt/sources.d/`` directory. Updates can be installed directly
|
||||||
with the ``apt`` command line tool, or via the GUI.
|
with the ``apt`` command line tool, or via the GUI.
|
||||||
|
|
||||||
@ -26,11 +26,10 @@ update``.
|
|||||||
|
|
||||||
.. FIXME for 7.0: change security update suite to bullseye-security
|
.. FIXME for 7.0: change security update suite to bullseye-security
|
||||||
|
|
||||||
In addition, you need a package repositories from Proxmox to get the backup
|
In addition, you need a package repository from Proxmox to get Proxmox Backup updates.
|
||||||
server updates.
|
|
||||||
|
|
||||||
During the Proxmox Backup beta phase only one repository (pbstest) will be
|
During the Proxmox Backup beta phase, only one repository (pbstest) will be
|
||||||
available. Once released, a Enterprise repository for production use and a
|
available. Once released, an Enterprise repository for production use and a
|
||||||
no-subscription repository will be provided.
|
no-subscription repository will be provided.
|
||||||
|
|
||||||
SecureApt
|
SecureApt
|
||||||
@ -39,8 +38,8 @@ SecureApt
|
|||||||
The `Release` files in the repositories are signed with GnuPG. APT is using
|
The `Release` files in the repositories are signed with GnuPG. APT is using
|
||||||
these signatures to verify that all packages are from a trusted source.
|
these signatures to verify that all packages are from a trusted source.
|
||||||
|
|
||||||
If you install Proxmox Backup Server from an official ISO image, the key for
|
If you install Proxmox Backup Server from an official ISO image, the
|
||||||
verification is already installed.
|
verification key is already installed.
|
||||||
|
|
||||||
If you install Proxmox Backup Server on top of Debian, download and install the
|
If you install Proxmox Backup Server on top of Debian, download and install the
|
||||||
key with the following commands:
|
key with the following commands:
|
||||||
@ -136,17 +135,17 @@ During the public beta, there is a repository called ``pbstest``. This one
|
|||||||
contains the latest packages and is heavily used by developers to test new
|
contains the latest packages and is heavily used by developers to test new
|
||||||
features.
|
features.
|
||||||
|
|
||||||
.. .. warning:: the ``pbstest`` repository should (as the name implies)
|
.. .. warning:: the ``pbstest`` repository should (as the name implies)
|
||||||
only be used to test new features or bug fixes.
|
only be used to test new features or bug fixes.
|
||||||
|
|
||||||
You can configure this using ``/etc/apt/sources.list`` by adding the following
|
You can access this repository by adding the following line to
|
||||||
line:
|
``/etc/apt/sources.list``:
|
||||||
|
|
||||||
.. code-block:: sources.list
|
.. code-block:: sources.list
|
||||||
:caption: sources.list entry for ``pbstest``
|
:caption: sources.list entry for ``pbstest``
|
||||||
|
|
||||||
deb http://download.proxmox.com/debian/pbs buster pbstest
|
deb http://download.proxmox.com/debian/pbs buster pbstest
|
||||||
|
|
||||||
If you installed Proxmox Backup Server from the official beta ISO you should
|
If you installed Proxmox Backup Server from the official beta ISO, you should
|
||||||
have this repository already configured in
|
have this repository already configured in
|
||||||
``/etc/apt/sources.list.d/pbstest-beta.list``
|
``/etc/apt/sources.list.d/pbstest-beta.list``
|
||||||
|
@ -9,7 +9,7 @@ which caters to a similar use-case.
|
|||||||
The ``.pxar`` format is adapted to fulfill the specific needs of the Proxmox
|
The ``.pxar`` format is adapted to fulfill the specific needs of the Proxmox
|
||||||
Backup Server, for example, efficient storage of hardlinks.
|
Backup Server, for example, efficient storage of hardlinks.
|
||||||
The format is designed to reduce storage space needed on the server by achieving
|
The format is designed to reduce storage space needed on the server by achieving
|
||||||
a high level of de-duplication.
|
a high level of deduplication.
|
||||||
|
|
||||||
Creating an Archive
|
Creating an Archive
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
@ -29,7 +29,7 @@ This will create a new archive called ``archive.pxar`` with the contents of the
|
|||||||
|
|
||||||
By default, ``pxar`` will skip certain mountpoints and will 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
|
boundaries. This design decision is based on the primary use case of creating
|
||||||
archives for backups. It is sensible to not back up the contents of certain
|
archives for backups. It makes sense to not back up the contents of certain
|
||||||
temporary or system specific files.
|
temporary or system specific files.
|
||||||
To alter this behavior and follow device boundaries, use the
|
To alter this behavior and follow device boundaries, use the
|
||||||
``--all-file-systems`` flag.
|
``--all-file-systems`` flag.
|
||||||
@ -66,7 +66,7 @@ All the glob patterns are relative to the ``source`` directory.
|
|||||||
previous ones. Permutations of the same patterns lead to different results.
|
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
|
``pxar`` will store the list of glob match patterns passed as parameters via the
|
||||||
command line in a file called ``.pxarexclude-cli`` and stores it at the root of
|
command line, in a file called ``.pxarexclude-cli`` at the root of
|
||||||
the archive.
|
the archive.
|
||||||
If a file with this name is already present in the source folder during 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
|
creation, this file is not included in the archive and the file containing the
|
||||||
@ -85,23 +85,23 @@ The behavior is the same as described in :ref:`creating-backups`.
|
|||||||
Extracting an Archive
|
Extracting an Archive
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
An existing archive ``archive.pxar`` is extracted to a ``target`` directory
|
An existing archive, ``archive.pxar``, is extracted to a ``target`` directory
|
||||||
with the following command:
|
with the following command:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
# pxar extract archive.pxar --target target
|
# pxar extract archive.pxar /path/to/target
|
||||||
|
|
||||||
If no target is provided, the content of the archive is extracted to the current
|
If no target is provided, the content of the archive is extracted to the current
|
||||||
working directory.
|
working directory.
|
||||||
|
|
||||||
In order to restore only parts of an archive, 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
|
it is possible to pass the corresponding glob match patterns as additional
|
||||||
parameters or use the patterns stored in a file:
|
parameters or to use the patterns stored in a file:
|
||||||
|
|
||||||
.. code-block:: console
|
.. code-block:: console
|
||||||
|
|
||||||
# pxar extract etc.pxar '**/*.conf' --target /restore/target/etc
|
# pxar extract etc.pxar /restore/target/etc --pattern '**/*.conf'
|
||||||
|
|
||||||
The above example restores all ``.conf`` files encountered in any of the
|
The above example restores all ``.conf`` files encountered in any of the
|
||||||
sub-folders in the archive ``etc.pxar`` to the target ``/restore/target/etc``.
|
sub-folders in the archive ``etc.pxar`` to the target ``/restore/target/etc``.
|
||||||
|
@ -7,8 +7,7 @@ use proxmox::api::router::{Router, SubdirMap};
|
|||||||
use proxmox::{sortable, identity};
|
use proxmox::{sortable, identity};
|
||||||
use proxmox::{http_err, list_subdirs_api_method};
|
use proxmox::{http_err, list_subdirs_api_method};
|
||||||
|
|
||||||
use crate::tools;
|
use crate::tools::ticket::{self, Empty, Ticket};
|
||||||
use crate::tools::ticket::*;
|
|
||||||
use crate::auth_helpers::*;
|
use crate::auth_helpers::*;
|
||||||
use crate::api2::types::*;
|
use crate::api2::types::*;
|
||||||
|
|
||||||
@ -35,27 +34,31 @@ fn authenticate_user(
|
|||||||
bail!("user account disabled or expired.");
|
bail!("user account disabled or expired.");
|
||||||
}
|
}
|
||||||
|
|
||||||
let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
|
|
||||||
|
|
||||||
if password.starts_with("PBS:") {
|
if password.starts_with("PBS:") {
|
||||||
if let Ok((_age, Some(ticket_username))) = tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", password, None, -300, ticket_lifetime) {
|
if let Ok(ticket_userid) = Ticket::<Userid>::parse(password)
|
||||||
if *userid == ticket_username {
|
.and_then(|ticket| ticket.verify(public_auth_key(), "PBS", None))
|
||||||
|
{
|
||||||
|
if *userid == ticket_userid {
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
} else {
|
|
||||||
bail!("ticket login failed - wrong userid");
|
|
||||||
}
|
}
|
||||||
|
bail!("ticket login failed - wrong userid");
|
||||||
}
|
}
|
||||||
} else if password.starts_with("PBSTERM:") {
|
} else if password.starts_with("PBSTERM:") {
|
||||||
if path.is_none() || privs.is_none() || port.is_none() {
|
if path.is_none() || privs.is_none() || port.is_none() {
|
||||||
bail!("cannot check termnal ticket without path, priv and port");
|
bail!("cannot check termnal ticket without path, priv and port");
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = path.unwrap();
|
let path = path.ok_or_else(|| format_err!("missing path for termproxy ticket"))?;
|
||||||
let privilege_name = privs.unwrap();
|
let privilege_name = privs
|
||||||
let port = port.unwrap();
|
.ok_or_else(|| format_err!("missing privilege name for termproxy ticket"))?;
|
||||||
|
let port = port.ok_or_else(|| format_err!("missing port for termproxy ticket"))?;
|
||||||
|
|
||||||
if let Ok((_age, _data)) =
|
if let Ok(Empty) = Ticket::parse(password)
|
||||||
tools::ticket::verify_term_ticket(public_auth_key(), &userid, &path, port, password)
|
.and_then(|ticket| ticket.verify(
|
||||||
|
public_auth_key(),
|
||||||
|
ticket::TERM_PREFIX,
|
||||||
|
Some(&ticket::term_aad(userid, &path, port)),
|
||||||
|
))
|
||||||
{
|
{
|
||||||
for (name, privilege) in PRIVILEGES {
|
for (name, privilege) in PRIVILEGES {
|
||||||
if *name == privilege_name {
|
if *name == privilege_name {
|
||||||
@ -138,7 +141,7 @@ fn create_ticket(
|
|||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
match authenticate_user(&username, &password, path, privs, port) {
|
match authenticate_user(&username, &password, path, privs, port) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some(&username), None)?;
|
let ticket = Ticket::new("PBS", &username)?.sign(private_auth_key(), None)?;
|
||||||
|
|
||||||
let token = assemble_csrf_prevention_token(csrf_secret(), &username);
|
let token = assemble_csrf_prevention_token(csrf_secret(), &username);
|
||||||
|
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
use std::collections::HashMap;
|
use anyhow::{format_err, Error};
|
||||||
|
|
||||||
use anyhow::{Error};
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment};
|
use proxmox::api::{api, ApiMethod, Router, RpcEnvironment};
|
||||||
@ -8,9 +6,10 @@ use proxmox::api::router::SubdirMap;
|
|||||||
use proxmox::{list_subdirs_api_method, sortable};
|
use proxmox::{list_subdirs_api_method, sortable};
|
||||||
|
|
||||||
use crate::api2::types::*;
|
use crate::api2::types::*;
|
||||||
use crate::api2::pull::{get_pull_parameters};
|
use crate::api2::pull::do_sync_job;
|
||||||
use crate::config::sync::{self, SyncJobStatus, SyncJobConfig};
|
use crate::config::sync::{self, SyncJobStatus, SyncJobConfig};
|
||||||
use crate::server::{self, TaskListInfo, WorkerTask};
|
use crate::server::UPID;
|
||||||
|
use crate::config::jobstate::{Job, JobState};
|
||||||
use crate::tools::systemd::time::{
|
use crate::tools::systemd::time::{
|
||||||
parse_calendar_event, compute_next_event};
|
parse_calendar_event, compute_next_event};
|
||||||
|
|
||||||
@ -34,33 +33,26 @@ pub fn list_sync_jobs(
|
|||||||
|
|
||||||
let mut list: Vec<SyncJobStatus> = config.convert_to_typed_array("sync")?;
|
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 {
|
for job in &mut list {
|
||||||
let mut last = 0;
|
let last_state = JobState::load("syncjob", &job.id)
|
||||||
if let Some(task) = last_tasks.get(&job.id) {
|
.map_err(|err| format_err!("could not open statefile for {}: {}", &job.id, err))?;
|
||||||
job.last_run_upid = Some(task.upid_str.clone());
|
let (upid, endtime, state, starttime) = match last_state {
|
||||||
if let Some((endtime, status)) = &task.state {
|
JobState::Created { time } => (None, None, None, time),
|
||||||
job.last_run_state = Some(String::from(status));
|
JobState::Started { upid } => {
|
||||||
job.last_run_endtime = Some(*endtime);
|
let parsed_upid: UPID = upid.parse()?;
|
||||||
last = *endtime;
|
(Some(upid), None, None, parsed_upid.starttime)
|
||||||
}
|
},
|
||||||
}
|
JobState::Finished { upid, state } => {
|
||||||
|
let parsed_upid: UPID = upid.parse()?;
|
||||||
|
(Some(upid), Some(state.endtime()), Some(state.to_string()), parsed_upid.starttime)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
job.last_run_upid = upid;
|
||||||
|
job.last_run_state = state;
|
||||||
|
job.last_run_endtime = endtime;
|
||||||
|
|
||||||
|
let last = job.last_run_endtime.unwrap_or_else(|| starttime);
|
||||||
|
|
||||||
job.next_run = (|| -> Option<i64> {
|
job.next_run = (|| -> Option<i64> {
|
||||||
let schedule = job.schedule.as_ref()?;
|
let schedule = job.schedule.as_ref()?;
|
||||||
@ -84,7 +76,7 @@ pub fn list_sync_jobs(
|
|||||||
}
|
}
|
||||||
)]
|
)]
|
||||||
/// Runs the sync jobs manually.
|
/// Runs the sync jobs manually.
|
||||||
async fn run_sync_job(
|
fn run_sync_job(
|
||||||
id: String,
|
id: String,
|
||||||
_info: &ApiMethod,
|
_info: &ApiMethod,
|
||||||
rpcenv: &mut dyn RpcEnvironment,
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
@ -95,26 +87,9 @@ async fn run_sync_job(
|
|||||||
|
|
||||||
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
||||||
|
|
||||||
let delete = sync_job.remove_vanished.unwrap_or(true);
|
let job = Job::new("syncjob", &id)?;
|
||||||
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()), userid, false, move |worker| async move {
|
let upid_str = do_sync_job(job, sync_job, &userid, None)?;
|
||||||
|
|
||||||
worker.log(format!("sync job '{}' start", &id));
|
|
||||||
|
|
||||||
crate::client::pull::pull_store(
|
|
||||||
&worker,
|
|
||||||
&client,
|
|
||||||
&src_repo,
|
|
||||||
tgt_store.clone(),
|
|
||||||
delete,
|
|
||||||
Userid::backup_userid().clone(),
|
|
||||||
).await?;
|
|
||||||
|
|
||||||
worker.log(format!("sync job '{}' end", &id));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(upid_str)
|
Ok(upid_str)
|
||||||
}
|
}
|
||||||
|
@ -83,6 +83,8 @@ pub fn create_sync_job(param: Value) -> Result<(), Error> {
|
|||||||
|
|
||||||
sync::save_config(&config)?;
|
sync::save_config(&config)?;
|
||||||
|
|
||||||
|
crate::config::jobstate::create_state_file("syncjob", &sync_job.id)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,6 +266,8 @@ pub fn delete_sync_job(id: String, digest: Option<String>) -> Result<(), Error>
|
|||||||
|
|
||||||
sync::save_config(&config)?;
|
sync::save_config(&config)?;
|
||||||
|
|
||||||
|
crate::config::jobstate::remove_state_file("syncjob", &id)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ use crate::api2::types::*;
|
|||||||
use crate::config::acl::PRIV_SYS_CONSOLE;
|
use crate::config::acl::PRIV_SYS_CONSOLE;
|
||||||
use crate::server::WorkerTask;
|
use crate::server::WorkerTask;
|
||||||
use crate::tools;
|
use crate::tools;
|
||||||
|
use crate::tools::ticket::{self, Empty, Ticket};
|
||||||
|
|
||||||
pub mod disks;
|
pub mod disks;
|
||||||
pub mod dns;
|
pub mod dns;
|
||||||
@ -105,12 +106,11 @@ async fn termproxy(
|
|||||||
let listener = TcpListener::bind("localhost:0")?;
|
let listener = TcpListener::bind("localhost:0")?;
|
||||||
let port = listener.local_addr()?.port();
|
let port = listener.local_addr()?.port();
|
||||||
|
|
||||||
let ticket = tools::ticket::assemble_term_ticket(
|
let ticket = Ticket::new(ticket::TERM_PREFIX, &Empty)?
|
||||||
crate::auth_helpers::private_auth_key(),
|
.sign(
|
||||||
&userid,
|
crate::auth_helpers::private_auth_key(),
|
||||||
&path,
|
Some(&ticket::term_aad(&userid, &path, port)),
|
||||||
port,
|
)?;
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut command = Vec::new();
|
let mut command = Vec::new();
|
||||||
match cmd.as_ref().map(|x| x.as_str()) {
|
match cmd.as_ref().map(|x| x.as_str()) {
|
||||||
@ -273,17 +273,16 @@ fn upgrade_to_websocket(
|
|||||||
) -> ApiResponseFuture {
|
) -> ApiResponseFuture {
|
||||||
async move {
|
async move {
|
||||||
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
||||||
let ticket = tools::required_string_param(¶m, "vncticket")?.to_owned();
|
let ticket = tools::required_string_param(¶m, "vncticket")?;
|
||||||
let port: u16 = tools::required_integer_param(¶m, "port")? as u16;
|
let port: u16 = tools::required_integer_param(¶m, "port")? as u16;
|
||||||
|
|
||||||
// will be checked again by termproxy
|
// will be checked again by termproxy
|
||||||
tools::ticket::verify_term_ticket(
|
Ticket::<Empty>::parse(ticket)?
|
||||||
crate::auth_helpers::public_auth_key(),
|
.verify(
|
||||||
&userid,
|
crate::auth_helpers::public_auth_key(),
|
||||||
&"/system",
|
ticket::TERM_PREFIX,
|
||||||
port,
|
Some(&ticket::term_aad(&userid, "/system", port)),
|
||||||
&ticket,
|
)?;
|
||||||
)?;
|
|
||||||
|
|
||||||
let (ws, response) = WebSocket::new(parts.headers)?;
|
let (ws, response) = WebSocket::new(parts.headers)?;
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ use crate::tools::systemd::{self, types::*};
|
|||||||
use crate::server::WorkerTask;
|
use crate::server::WorkerTask;
|
||||||
|
|
||||||
use crate::api2::types::*;
|
use crate::api2::types::*;
|
||||||
|
use crate::config::datastore::DataStoreConfig;
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
properties: {
|
properties: {
|
||||||
@ -175,9 +176,69 @@ pub fn create_datastore_disk(
|
|||||||
Ok(upid_str)
|
Ok(upid_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[api(
|
||||||
|
protected: true,
|
||||||
|
input: {
|
||||||
|
properties: {
|
||||||
|
node: {
|
||||||
|
schema: NODE_SCHEMA,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
schema: DATASTORE_SCHEMA,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
access: {
|
||||||
|
permission: &Permission::Privilege(&["system", "disks"], PRIV_SYS_MODIFY, false),
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
/// Remove a Filesystem mounted under '/mnt/datastore/<name>'.".
|
||||||
|
pub fn delete_datastore_disk(name: String) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let path = format!("/mnt/datastore/{}", name);
|
||||||
|
// path of datastore cannot be changed
|
||||||
|
let (config, _) = crate::config::datastore::config()?;
|
||||||
|
let datastores: Vec<DataStoreConfig> = config.convert_to_typed_array("datastore")?;
|
||||||
|
let conflicting_datastore: Option<DataStoreConfig> = datastores.into_iter()
|
||||||
|
.filter(|ds| ds.path == path)
|
||||||
|
.next();
|
||||||
|
|
||||||
|
if let Some(conflicting_datastore) = conflicting_datastore {
|
||||||
|
bail!("Can't remove '{}' since it's required by datastore '{}'",
|
||||||
|
conflicting_datastore.path, conflicting_datastore.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable systemd mount-unit
|
||||||
|
let mut mount_unit_name = systemd::escape_unit(&path, true);
|
||||||
|
mount_unit_name.push_str(".mount");
|
||||||
|
systemd::disable_unit(&mount_unit_name)?;
|
||||||
|
|
||||||
|
// delete .mount-file
|
||||||
|
let mount_unit_path = format!("/etc/systemd/system/{}", mount_unit_name);
|
||||||
|
let full_path = std::path::Path::new(&mount_unit_path);
|
||||||
|
log::info!("removing systemd mount unit {:?}", full_path);
|
||||||
|
std::fs::remove_file(&full_path)?;
|
||||||
|
|
||||||
|
// try to unmount, if that fails tell the user to reboot or unmount manually
|
||||||
|
let mut command = std::process::Command::new("umount");
|
||||||
|
command.arg(&path);
|
||||||
|
match crate::tools::run_command(command, None) {
|
||||||
|
Err(_) => bail!(
|
||||||
|
"Could not umount '{}' since it is busy. It will stay mounted \
|
||||||
|
until the next reboot or until unmounted manually!",
|
||||||
|
path
|
||||||
|
),
|
||||||
|
Ok(_) => Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEM_ROUTER: Router = Router::new()
|
||||||
|
.delete(&API_METHOD_DELETE_DATASTORE_DISK);
|
||||||
|
|
||||||
pub const ROUTER: Router = Router::new()
|
pub const ROUTER: Router = Router::new()
|
||||||
.get(&API_METHOD_LIST_DATASTORE_MOUNTS)
|
.get(&API_METHOD_LIST_DATASTORE_MOUNTS)
|
||||||
.post(&API_METHOD_CREATE_DATASTORE_DISK);
|
.post(&API_METHOD_CREATE_DATASTORE_DISK)
|
||||||
|
.match_all("name", &ITEM_ROUTER);
|
||||||
|
|
||||||
|
|
||||||
fn create_datastore_mount_unit(
|
fn create_datastore_mount_unit(
|
||||||
|
@ -4,12 +4,13 @@ use anyhow::{bail, Error};
|
|||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use proxmox::{sortable, identity, list_subdirs_api_method};
|
use proxmox::{sortable, identity, list_subdirs_api_method};
|
||||||
use proxmox::api::{api, Router, Permission};
|
use proxmox::api::{api, Router, Permission, RpcEnvironment};
|
||||||
use proxmox::api::router::SubdirMap;
|
use proxmox::api::router::SubdirMap;
|
||||||
use proxmox::api::schema::*;
|
use proxmox::api::schema::*;
|
||||||
|
|
||||||
use crate::api2::types::*;
|
use crate::api2::types::*;
|
||||||
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
|
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
|
||||||
|
use crate::server::WorkerTask;
|
||||||
|
|
||||||
static SERVICE_NAME_LIST: [&str; 7] = [
|
static SERVICE_NAME_LIST: [&str; 7] = [
|
||||||
"proxmox-backup",
|
"proxmox-backup",
|
||||||
@ -181,31 +182,43 @@ fn get_service_state(
|
|||||||
Ok(json_service_state(&service, status))
|
Ok(json_service_state(&service, status))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_service_command(service: &str, cmd: &str) -> Result<Value, Error> {
|
fn run_service_command(service: &str, cmd: &str, userid: Userid) -> Result<Value, Error> {
|
||||||
|
|
||||||
// fixme: run background worker (fork_worker) ???
|
let workerid = format!("srv{}", &cmd);
|
||||||
|
|
||||||
let cmd = match cmd {
|
let cmd = match cmd {
|
||||||
"start"|"stop"|"restart"=> cmd,
|
"start"|"stop"|"restart"=> cmd.to_string(),
|
||||||
"reload" => "try-reload-or-restart", // some services do not implement reload
|
"reload" => "try-reload-or-restart".to_string(), // some services do not implement reload
|
||||||
_ => bail!("unknown service command '{}'", cmd),
|
_ => bail!("unknown service command '{}'", cmd),
|
||||||
};
|
};
|
||||||
|
let service = service.to_string();
|
||||||
|
|
||||||
if service == "proxmox-backup" && cmd == "stop" {
|
let upid = WorkerTask::new_thread(
|
||||||
bail!("invalid service cmd '{} {}' cannot stop essential service!", service, cmd);
|
&workerid,
|
||||||
}
|
Some(service.clone()),
|
||||||
|
userid,
|
||||||
|
false,
|
||||||
|
move |_worker| {
|
||||||
|
|
||||||
let real_service_name = real_service_name(service);
|
if service == "proxmox-backup" && cmd == "stop" {
|
||||||
|
bail!("invalid service cmd '{} {}' cannot stop essential service!", service, cmd);
|
||||||
|
}
|
||||||
|
|
||||||
let status = Command::new("systemctl")
|
let real_service_name = real_service_name(&service);
|
||||||
.args(&[cmd, real_service_name])
|
|
||||||
.status()?;
|
|
||||||
|
|
||||||
if !status.success() {
|
let status = Command::new("systemctl")
|
||||||
bail!("systemctl {} failed with {}", cmd, status);
|
.args(&[&cmd, real_service_name])
|
||||||
}
|
.status()?;
|
||||||
|
|
||||||
Ok(Value::Null)
|
if !status.success() {
|
||||||
|
bail!("systemctl {} failed with {}", cmd, status);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(upid.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
@ -228,11 +241,14 @@ fn run_service_command(service: &str, cmd: &str) -> Result<Value, Error> {
|
|||||||
fn start_service(
|
fn start_service(
|
||||||
service: String,
|
service: String,
|
||||||
_param: Value,
|
_param: Value,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
|
|
||||||
|
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
||||||
|
|
||||||
log::info!("starting service {}", service);
|
log::info!("starting service {}", service);
|
||||||
|
|
||||||
run_service_command(&service, "start")
|
run_service_command(&service, "start", userid)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
@ -255,11 +271,14 @@ fn start_service(
|
|||||||
fn stop_service(
|
fn stop_service(
|
||||||
service: String,
|
service: String,
|
||||||
_param: Value,
|
_param: Value,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
|
|
||||||
|
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
||||||
|
|
||||||
log::info!("stopping service {}", service);
|
log::info!("stopping service {}", service);
|
||||||
|
|
||||||
run_service_command(&service, "stop")
|
run_service_command(&service, "stop", userid)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
@ -282,15 +301,18 @@ fn stop_service(
|
|||||||
fn restart_service(
|
fn restart_service(
|
||||||
service: String,
|
service: String,
|
||||||
_param: Value,
|
_param: Value,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
|
|
||||||
|
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
||||||
|
|
||||||
log::info!("re-starting service {}", service);
|
log::info!("re-starting service {}", service);
|
||||||
|
|
||||||
if &service == "proxmox-backup-proxy" {
|
if &service == "proxmox-backup-proxy" {
|
||||||
// special case, avoid aborting running tasks
|
// special case, avoid aborting running tasks
|
||||||
run_service_command(&service, "reload")
|
run_service_command(&service, "reload", userid)
|
||||||
} else {
|
} else {
|
||||||
run_service_command(&service, "restart")
|
run_service_command(&service, "restart", userid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,11 +336,14 @@ fn restart_service(
|
|||||||
fn reload_service(
|
fn reload_service(
|
||||||
service: String,
|
service: String,
|
||||||
_param: Value,
|
_param: Value,
|
||||||
|
rpcenv: &mut dyn RpcEnvironment,
|
||||||
) -> Result<Value, Error> {
|
) -> Result<Value, Error> {
|
||||||
|
|
||||||
|
let userid: Userid = rpcenv.get_user().unwrap().parse()?;
|
||||||
|
|
||||||
log::info!("reloading service {}", service);
|
log::info!("reloading service {}", service);
|
||||||
|
|
||||||
run_service_command(&service, "reload")
|
run_service_command(&service, "reload", userid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ use proxmox::{identity, list_subdirs_api_method, sortable};
|
|||||||
|
|
||||||
use crate::tools;
|
use crate::tools;
|
||||||
use crate::api2::types::*;
|
use crate::api2::types::*;
|
||||||
use crate::server::{self, UPID};
|
use crate::server::{self, UPID, TaskState};
|
||||||
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
|
use crate::config::acl::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY};
|
||||||
use crate::config::cached_user_info::CachedUserInfo;
|
use crate::config::cached_user_info::CachedUserInfo;
|
||||||
|
|
||||||
@ -105,9 +105,9 @@ async fn get_task_status(
|
|||||||
if crate::server::worker_is_active(&upid).await? {
|
if crate::server::worker_is_active(&upid).await? {
|
||||||
result["status"] = Value::from("running");
|
result["status"] = Value::from("running");
|
||||||
} else {
|
} else {
|
||||||
let exitstatus = crate::server::upid_read_status(&upid).unwrap_or(String::from("unknown"));
|
let exitstatus = crate::server::upid_read_status(&upid).unwrap_or(TaskState::Unknown { endtime: 0 });
|
||||||
result["status"] = Value::from("stopped");
|
result["status"] = Value::from("stopped");
|
||||||
result["exitstatus"] = Value::from(exitstatus);
|
result["exitstatus"] = Value::from(exitstatus.to_string());
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@ -352,8 +352,9 @@ pub fn list_tasks(
|
|||||||
|
|
||||||
if let Some(ref state) = info.state {
|
if let Some(ref state) = info.state {
|
||||||
if running { continue; }
|
if running { continue; }
|
||||||
if errors && state.1 == "OK" {
|
match state {
|
||||||
continue;
|
crate::server::TaskState::OK { .. } if errors => continue,
|
||||||
|
_ => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
use std::sync::{Arc};
|
use std::sync::{Arc};
|
||||||
|
|
||||||
use anyhow::{format_err, Error};
|
use anyhow::{format_err, Error};
|
||||||
|
use futures::{select, future::FutureExt};
|
||||||
|
|
||||||
use proxmox::api::api;
|
use proxmox::api::api;
|
||||||
use proxmox::api::{ApiMethod, Router, RpcEnvironment, Permission};
|
use proxmox::api::{ApiMethod, Router, RpcEnvironment, Permission};
|
||||||
@ -12,6 +13,8 @@ use crate::client::{HttpClient, HttpClientOptions, BackupRepository, pull::pull_
|
|||||||
use crate::api2::types::*;
|
use crate::api2::types::*;
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
remote,
|
remote,
|
||||||
|
sync::SyncJobConfig,
|
||||||
|
jobstate::Job,
|
||||||
acl::{PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_PRUNE, PRIV_REMOTE_READ},
|
acl::{PRIV_DATASTORE_BACKUP, PRIV_DATASTORE_PRUNE, PRIV_REMOTE_READ},
|
||||||
cached_user_info::CachedUserInfo,
|
cached_user_info::CachedUserInfo,
|
||||||
};
|
};
|
||||||
@ -62,6 +65,68 @@ pub async fn get_pull_parameters(
|
|||||||
Ok((client, src_repo, tgt_store))
|
Ok((client, src_repo, tgt_store))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn do_sync_job(
|
||||||
|
mut job: Job,
|
||||||
|
sync_job: SyncJobConfig,
|
||||||
|
userid: &Userid,
|
||||||
|
schedule: Option<String>,
|
||||||
|
) -> Result<String, Error> {
|
||||||
|
|
||||||
|
let job_id = job.jobname().to_string();
|
||||||
|
let worker_type = job.jobtype().to_string();
|
||||||
|
|
||||||
|
let upid_str = WorkerTask::spawn(
|
||||||
|
&worker_type,
|
||||||
|
Some(job.jobname().to_string()),
|
||||||
|
userid.clone(),
|
||||||
|
false,
|
||||||
|
move |worker| async move {
|
||||||
|
|
||||||
|
job.start(&worker.upid().to_string())?;
|
||||||
|
|
||||||
|
let worker2 = worker.clone();
|
||||||
|
|
||||||
|
let worker_future = async move {
|
||||||
|
|
||||||
|
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?;
|
||||||
|
|
||||||
|
worker.log(format!("Starting datastore sync job '{}'", job_id));
|
||||||
|
if let Some(event_str) = schedule {
|
||||||
|
worker.log(format!("task triggered by schedule '{}'", event_str));
|
||||||
|
}
|
||||||
|
worker.log(format!("Sync datastore '{}' from '{}/{}'",
|
||||||
|
sync_job.store, sync_job.remote, sync_job.remote_store));
|
||||||
|
|
||||||
|
crate::client::pull::pull_store(&worker, &client, &src_repo, tgt_store.clone(), delete, Userid::backup_userid().clone()).await?;
|
||||||
|
|
||||||
|
worker.log(format!("sync job '{}' end", &job_id));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut abort_future = worker2.abort_future().map(|_| Err(format_err!("sync aborted")));
|
||||||
|
|
||||||
|
let res = select!{
|
||||||
|
worker = worker_future.fuse() => worker,
|
||||||
|
abort = abort_future => abort,
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = worker2.create_state(&res);
|
||||||
|
|
||||||
|
match job.finish(status) {
|
||||||
|
Ok(_) => {},
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("could not finish job state: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(upid_str)
|
||||||
|
}
|
||||||
|
|
||||||
#[api(
|
#[api(
|
||||||
input: {
|
input: {
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -595,7 +595,7 @@ impl From<crate::server::TaskListInfo> for TaskListItem {
|
|||||||
fn from(info: crate::server::TaskListInfo) -> Self {
|
fn from(info: crate::server::TaskListInfo) -> Self {
|
||||||
let (endtime, status) = info
|
let (endtime, status) = info
|
||||||
.state
|
.state
|
||||||
.map_or_else(|| (None, None), |(a,b)| (Some(a), Some(b)));
|
.map_or_else(|| (None, None), |a| (Some(a.endtime()), Some(a.to_string())));
|
||||||
|
|
||||||
TaskListItem {
|
TaskListItem {
|
||||||
upid: info.upid_str,
|
upid: info.upid_str,
|
||||||
|
@ -45,6 +45,31 @@ pub struct BackupGroup {
|
|||||||
backup_id: String,
|
backup_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::cmp::Ord for BackupGroup {
|
||||||
|
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
let type_order = self.backup_type.cmp(&other.backup_type);
|
||||||
|
if type_order != std::cmp::Ordering::Equal {
|
||||||
|
return type_order;
|
||||||
|
}
|
||||||
|
// try to compare IDs numerically
|
||||||
|
let id_self = self.backup_id.parse::<u64>();
|
||||||
|
let id_other = other.backup_id.parse::<u64>();
|
||||||
|
match (id_self, id_other) {
|
||||||
|
(Ok(id_self), Ok(id_other)) => id_self.cmp(&id_other),
|
||||||
|
(Ok(_), Err(_)) => std::cmp::Ordering::Less,
|
||||||
|
(Err(_), Ok(_)) => std::cmp::Ordering::Greater,
|
||||||
|
_ => self.backup_id.cmp(&other.backup_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::cmp::PartialOrd for BackupGroup {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl BackupGroup {
|
impl BackupGroup {
|
||||||
|
|
||||||
pub fn new<T: Into<String>, U: Into<String>>(backup_type: T, backup_id: U) -> Self {
|
pub fn new<T: Into<String>, U: Into<String>>(backup_type: T, backup_id: U) -> Self {
|
||||||
|
@ -11,7 +11,6 @@ use anyhow::{bail, format_err, Error};
|
|||||||
|
|
||||||
use proxmox::tools::io::ReadExt;
|
use proxmox::tools::io::ReadExt;
|
||||||
use proxmox::tools::uuid::Uuid;
|
use proxmox::tools::uuid::Uuid;
|
||||||
use proxmox::tools::vec;
|
|
||||||
use proxmox::tools::mmap::Mmap;
|
use proxmox::tools::mmap::Mmap;
|
||||||
use pxar::accessor::{MaybeReady, ReadAt, ReadAtOperation};
|
use pxar::accessor::{MaybeReady, ReadAt, ReadAtOperation};
|
||||||
|
|
||||||
@ -41,6 +40,24 @@ proxmox::static_assert_size!(DynamicIndexHeader, 4096);
|
|||||||
// pub data: DynamicIndexHeaderData,
|
// pub data: DynamicIndexHeaderData,
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
impl DynamicIndexHeader {
|
||||||
|
/// Convenience method to allocate a zero-initialized header struct.
|
||||||
|
pub fn zeroed() -> Box<Self> {
|
||||||
|
unsafe {
|
||||||
|
Box::from_raw(std::alloc::alloc_zeroed(std::alloc::Layout::new::<Self>()) as *mut Self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
|
unsafe {
|
||||||
|
std::slice::from_raw_parts(
|
||||||
|
self as *const Self as *const u8,
|
||||||
|
std::mem::size_of::<Self>(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
pub struct DynamicEntry {
|
pub struct DynamicEntry {
|
||||||
@ -489,27 +506,16 @@ impl DynamicIndexWriter {
|
|||||||
|
|
||||||
let mut writer = BufWriter::with_capacity(1024 * 1024, file);
|
let mut writer = BufWriter::with_capacity(1024 * 1024, file);
|
||||||
|
|
||||||
let header_size = std::mem::size_of::<DynamicIndexHeader>();
|
|
||||||
|
|
||||||
// todo: use static assertion when available in rust
|
|
||||||
if header_size != 4096 {
|
|
||||||
panic!("got unexpected header size");
|
|
||||||
}
|
|
||||||
|
|
||||||
let ctime = epoch_now_u64()?;
|
let ctime = epoch_now_u64()?;
|
||||||
|
|
||||||
let uuid = Uuid::generate();
|
let uuid = Uuid::generate();
|
||||||
|
|
||||||
let mut buffer = vec::zeroed(header_size);
|
let mut header = DynamicIndexHeader::zeroed();
|
||||||
let header = crate::tools::map_struct_mut::<DynamicIndexHeader>(&mut buffer)?;
|
|
||||||
|
|
||||||
header.magic = super::DYNAMIC_SIZED_CHUNK_INDEX_1_0;
|
header.magic = super::DYNAMIC_SIZED_CHUNK_INDEX_1_0;
|
||||||
header.ctime = u64::to_le(ctime);
|
header.ctime = u64::to_le(ctime);
|
||||||
header.uuid = *uuid.as_bytes();
|
header.uuid = *uuid.as_bytes();
|
||||||
|
// header.index_csum = [0u8; 32];
|
||||||
header.index_csum = [0u8; 32];
|
writer.write_all(header.as_bytes())?;
|
||||||
|
|
||||||
writer.write_all(&buffer)?;
|
|
||||||
|
|
||||||
let csum = Some(openssl::sha::Sha256::new());
|
let csum = Some(openssl::sha::Sha256::new());
|
||||||
|
|
||||||
|
@ -50,7 +50,17 @@ fn verify_index_chunks(
|
|||||||
worker.fail_on_abort()?;
|
worker.fail_on_abort()?;
|
||||||
|
|
||||||
let info = index.chunk_info(pos).unwrap();
|
let info = index.chunk_info(pos).unwrap();
|
||||||
let size = info.range.end - info.range.start;
|
|
||||||
|
if verified_chunks.contains(&info.digest) {
|
||||||
|
continue; // already verified
|
||||||
|
}
|
||||||
|
|
||||||
|
if corrupt_chunks.contains(&info.digest) {
|
||||||
|
let digest_str = proxmox::tools::digest_to_hex(&info.digest);
|
||||||
|
worker.log(format!("chunk {} was marked as corrupt", digest_str));
|
||||||
|
errors += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let chunk = match datastore.load_chunk(&info.digest) {
|
let chunk = match datastore.load_chunk(&info.digest) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -81,20 +91,14 @@ fn verify_index_chunks(
|
|||||||
errors += 1;
|
errors += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !verified_chunks.contains(&info.digest) {
|
let size = info.range.end - info.range.start;
|
||||||
if !corrupt_chunks.contains(&info.digest) {
|
|
||||||
if let Err(err) = chunk.verify_unencrypted(size as usize, &info.digest) {
|
if let Err(err) = chunk.verify_unencrypted(size as usize, &info.digest) {
|
||||||
corrupt_chunks.insert(info.digest);
|
corrupt_chunks.insert(info.digest);
|
||||||
worker.log(format!("{}", err));
|
worker.log(format!("{}", err));
|
||||||
errors += 1;
|
errors += 1;
|
||||||
} else {
|
} else {
|
||||||
verified_chunks.insert(info.digest);
|
verified_chunks.insert(info.digest);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let digest_str = proxmox::tools::digest_to_hex(&info.digest);
|
|
||||||
worker.log(format!("chunk {} was marked as corrupt", digest_str));
|
|
||||||
errors += 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +271,7 @@ pub fn verify_all_backups(datastore: &DataStore, worker: &WorkerTask) -> Result<
|
|||||||
|
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
let list = match BackupGroup::list_groups(&datastore.base_path()) {
|
let mut list = match BackupGroup::list_groups(&datastore.base_path()) {
|
||||||
Ok(list) => list,
|
Ok(list) => list,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
worker.log(format!("verify datastore {} - unable to list backups: {}", datastore.name(), err));
|
worker.log(format!("verify datastore {} - unable to list backups: {}", datastore.name(), err));
|
||||||
@ -275,6 +279,8 @@ pub fn verify_all_backups(datastore: &DataStore, worker: &WorkerTask) -> Result<
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
list.sort_unstable();
|
||||||
|
|
||||||
worker.log(format!("verify datastore {}", datastore.name()));
|
worker.log(format!("verify datastore {}", datastore.name()));
|
||||||
|
|
||||||
for group in list {
|
for group in list {
|
||||||
|
@ -37,6 +37,7 @@ async fn run() -> Result<(), Error> {
|
|||||||
config::update_self_signed_cert(false)?;
|
config::update_self_signed_cert(false)?;
|
||||||
|
|
||||||
proxmox_backup::rrd::create_rrdb_dir()?;
|
proxmox_backup::rrd::create_rrdb_dir()?;
|
||||||
|
proxmox_backup::config::jobstate::create_jobstate_dir()?;
|
||||||
|
|
||||||
if let Err(err) = generate_auth_key() {
|
if let Err(err) = generate_auth_key() {
|
||||||
bail!("unable to generate auth key - {}", err);
|
bail!("unable to generate auth key - {}", err);
|
||||||
|
@ -9,7 +9,7 @@ use proxmox_backup::tools;
|
|||||||
use proxmox_backup::config;
|
use proxmox_backup::config;
|
||||||
use proxmox_backup::api2::{self, types::* };
|
use proxmox_backup::api2::{self, types::* };
|
||||||
use proxmox_backup::client::*;
|
use proxmox_backup::client::*;
|
||||||
use proxmox_backup::tools::ticket::*;
|
use proxmox_backup::tools::ticket::Ticket;
|
||||||
use proxmox_backup::auth_helpers::*;
|
use proxmox_backup::auth_helpers::*;
|
||||||
|
|
||||||
mod proxmox_backup_manager;
|
mod proxmox_backup_manager;
|
||||||
@ -59,12 +59,8 @@ fn connect() -> Result<HttpClient, Error> {
|
|||||||
.verify_cert(false); // not required for connection to localhost
|
.verify_cert(false); // not required for connection to localhost
|
||||||
|
|
||||||
let client = if uid.is_root() {
|
let client = if uid.is_root() {
|
||||||
let ticket = assemble_rsa_ticket(
|
let ticket = Ticket::new("PBS", Userid::root_userid())?
|
||||||
private_auth_key(),
|
.sign(private_auth_key(), None)?;
|
||||||
"PBS",
|
|
||||||
Some(Userid::root_userid()),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
options = options.password(Some(ticket));
|
options = options.password(Some(ticket));
|
||||||
HttpClient::new("localhost", Userid::root_userid(), options)?
|
HttpClient::new("localhost", Userid::root_userid(), options)?
|
||||||
} else {
|
} else {
|
||||||
|
@ -18,6 +18,8 @@ use proxmox_backup::server::{ApiConfig, rest::*};
|
|||||||
use proxmox_backup::auth_helpers::*;
|
use proxmox_backup::auth_helpers::*;
|
||||||
use proxmox_backup::tools::disks::{ DiskManage, zfs_pool_stats };
|
use proxmox_backup::tools::disks::{ DiskManage, zfs_pool_stats };
|
||||||
|
|
||||||
|
use proxmox_backup::api2::pull::do_sync_job;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
proxmox_backup::tools::setup_safe_path_env();
|
proxmox_backup::tools::setup_safe_path_env();
|
||||||
|
|
||||||
@ -472,10 +474,7 @@ async fn schedule_datastore_prune() {
|
|||||||
async fn schedule_datastore_sync_jobs() {
|
async fn schedule_datastore_sync_jobs() {
|
||||||
|
|
||||||
use proxmox_backup::{
|
use proxmox_backup::{
|
||||||
backup::DataStore,
|
config::{ sync::{self, SyncJobConfig}, jobstate::{self, Job} },
|
||||||
client::{ HttpClient, HttpClientOptions, BackupRepository, pull::pull_store },
|
|
||||||
server::{ WorkerTask },
|
|
||||||
config::{ sync::{self, SyncJobConfig}, remote::{self, Remote} },
|
|
||||||
tools::systemd::time::{ parse_calendar_event, compute_next_event },
|
tools::systemd::time::{ parse_calendar_event, compute_next_event },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -487,14 +486,6 @@ async fn schedule_datastore_sync_jobs() {
|
|||||||
Ok((config, _digest)) => config,
|
Ok((config, _digest)) => config,
|
||||||
};
|
};
|
||||||
|
|
||||||
let remote_config = match remote::config() {
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("unable to read remote config - {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Ok((config, _digest)) => config,
|
|
||||||
};
|
|
||||||
|
|
||||||
for (job_id, (_, job_config)) in config.sections {
|
for (job_id, (_, job_config)) in config.sections {
|
||||||
let job_config: SyncJobConfig = match serde_json::from_value(job_config) {
|
let job_config: SyncJobConfig = match serde_json::from_value(job_config) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@ -519,16 +510,10 @@ async fn schedule_datastore_sync_jobs() {
|
|||||||
|
|
||||||
let worker_type = "syncjob";
|
let worker_type = "syncjob";
|
||||||
|
|
||||||
let last = match lookup_last_worker(worker_type, &job_id) {
|
let last = match jobstate::last_run_time(worker_type, &job_id) {
|
||||||
Ok(Some(upid)) => {
|
Ok(time) => time,
|
||||||
if proxmox_backup::server::worker_is_active_local(&upid) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
upid.starttime
|
|
||||||
},
|
|
||||||
Ok(None) => 0,
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("lookup_last_job_start failed: {}", err);
|
eprintln!("could not get last run time of {} {}: {}", worker_type, job_id, err);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -550,57 +535,15 @@ async fn schedule_datastore_sync_jobs() {
|
|||||||
};
|
};
|
||||||
if next > now { continue; }
|
if next > now { continue; }
|
||||||
|
|
||||||
|
let job = match Job::new(worker_type, &job_id) {
|
||||||
let job_id2 = job_id.clone();
|
Ok(job) => job,
|
||||||
|
Err(_) => continue, // could not get lock
|
||||||
let tgt_store = match DataStore::lookup_datastore(&job_config.store) {
|
|
||||||
Ok(datastore) => datastore,
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("lookup_datastore '{}' failed - {}", job_config.store, err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let remote: Remote = match remote_config.lookup("remote", &job_config.remote) {
|
|
||||||
Ok(remote) => remote,
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("remote_config lookup failed: {}", err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let userid = Userid::backup_userid().clone();
|
let userid = Userid::backup_userid().clone();
|
||||||
|
|
||||||
let delete = job_config.remove_vanished.unwrap_or(true);
|
if let Err(err) = do_sync_job(job, job_config, &userid, Some(event_str)) {
|
||||||
|
eprintln!("unable to start datastore sync job {} - {}", &job_id, err);
|
||||||
if let Err(err) = WorkerTask::spawn(
|
|
||||||
worker_type,
|
|
||||||
Some(job_id.clone()),
|
|
||||||
userid.clone(),
|
|
||||||
false,
|
|
||||||
move |worker| async move {
|
|
||||||
worker.log(format!("Starting datastore sync job '{}'", job_id));
|
|
||||||
worker.log(format!("task triggered by schedule '{}'", event_str));
|
|
||||||
worker.log(format!("Sync datastore '{}' from '{}/{}'",
|
|
||||||
job_config.store, job_config.remote, job_config.remote_store));
|
|
||||||
|
|
||||||
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), job_config.remote_store);
|
|
||||||
|
|
||||||
pull_store(&worker, &client, &src_repo, tgt_store, delete, userid).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
eprintln!("unable to start datastore sync job {} - {}", job_id2, err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,7 @@ struct Speed {
|
|||||||
struct BenchmarkResult {
|
struct BenchmarkResult {
|
||||||
/// TLS upload speed
|
/// TLS upload speed
|
||||||
tls: Speed,
|
tls: Speed,
|
||||||
/// SHA256 checksum comptation speed
|
/// SHA256 checksum computation speed
|
||||||
sha256: Speed,
|
sha256: Speed,
|
||||||
/// ZStd level 1 compression speed
|
/// ZStd level 1 compression speed
|
||||||
compress: Speed,
|
compress: Speed,
|
||||||
@ -187,7 +187,7 @@ fn render_result(
|
|||||||
.header("TLS (maximal backup upload speed)")
|
.header("TLS (maximal backup upload speed)")
|
||||||
.right_align(false).renderer(render_speed))
|
.right_align(false).renderer(render_speed))
|
||||||
.column(ColumnConfig::new("sha256")
|
.column(ColumnConfig::new("sha256")
|
||||||
.header("SHA256 checksum comptation speed")
|
.header("SHA256 checksum computation speed")
|
||||||
.right_align(false).renderer(render_speed))
|
.right_align(false).renderer(render_speed))
|
||||||
.column(ColumnConfig::new("compress")
|
.column(ColumnConfig::new("compress")
|
||||||
.header("ZStd level 1 compression speed")
|
.header("ZStd level 1 compression speed")
|
||||||
|
@ -18,6 +18,7 @@ use crate::buildcfg;
|
|||||||
pub mod acl;
|
pub mod acl;
|
||||||
pub mod cached_user_info;
|
pub mod cached_user_info;
|
||||||
pub mod datastore;
|
pub mod datastore;
|
||||||
|
pub mod jobstate;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod remote;
|
pub mod remote;
|
||||||
pub mod sync;
|
pub mod sync;
|
||||||
|
263
src/config/jobstate.rs
Normal file
263
src/config/jobstate.rs
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
//! Generic JobState handling
|
||||||
|
//!
|
||||||
|
//! A 'Job' can have 3 states
|
||||||
|
//! - Created, when a schedule was created but never executed
|
||||||
|
//! - Started, when a job is running right now
|
||||||
|
//! - Finished, when a job was running in the past
|
||||||
|
//!
|
||||||
|
//! and is identified by 2 values: jobtype and jobname (e.g. 'syncjob' and 'myfirstsyncjob')
|
||||||
|
//!
|
||||||
|
//! This module Provides 2 helper structs to handle those coniditons
|
||||||
|
//! 'Job' which handles locking and writing to a file
|
||||||
|
//! 'JobState' which is the actual state
|
||||||
|
//!
|
||||||
|
//! an example usage would be
|
||||||
|
//! ```no_run
|
||||||
|
//! # use anyhow::{bail, Error};
|
||||||
|
//! # use proxmox_backup::server::TaskState;
|
||||||
|
//! # use proxmox_backup::config::jobstate::*;
|
||||||
|
//! # fn some_code() -> TaskState { TaskState::OK { endtime: 0 } }
|
||||||
|
//! # fn code() -> Result<(), Error> {
|
||||||
|
//! // locks the correct file under /var/lib
|
||||||
|
//! // or fails if someone else holds the lock
|
||||||
|
//! let mut job = match Job::new("jobtype", "jobname") {
|
||||||
|
//! Ok(job) => job,
|
||||||
|
//! Err(err) => bail!("could not lock jobstate"),
|
||||||
|
//! };
|
||||||
|
//!
|
||||||
|
//! // job holds the lock, we can start it
|
||||||
|
//! job.start("someupid")?;
|
||||||
|
//! // do something
|
||||||
|
//! let task_state = some_code();
|
||||||
|
//! job.finish(task_state)?;
|
||||||
|
//!
|
||||||
|
//! // release the lock
|
||||||
|
//! drop(job);
|
||||||
|
//! # Ok(())
|
||||||
|
//! # }
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
use std::fs::File;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{bail, format_err, Error};
|
||||||
|
use proxmox::tools::fs::{
|
||||||
|
create_path, file_read_optional_string, open_file_locked, replace_file, CreateOptions,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::server::{upid_read_status, worker_is_active_local, TaskState, UPID};
|
||||||
|
use crate::tools::epoch_now_u64;
|
||||||
|
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
/// Represents the State of a specific Job
|
||||||
|
pub enum JobState {
|
||||||
|
/// A job was created at 'time', but never started/finished
|
||||||
|
Created { time: i64 },
|
||||||
|
/// The Job was last started in 'upid',
|
||||||
|
Started { upid: String },
|
||||||
|
/// The Job was last started in 'upid', which finished with 'state'
|
||||||
|
Finished { upid: String, state: TaskState },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a Job and holds the correct lock
|
||||||
|
pub struct Job {
|
||||||
|
jobtype: String,
|
||||||
|
jobname: String,
|
||||||
|
/// The State of the job
|
||||||
|
pub state: JobState,
|
||||||
|
_lock: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
const JOB_STATE_BASEDIR: &str = "/var/lib/proxmox-backup/jobstates";
|
||||||
|
|
||||||
|
/// Create jobstate stat dir with correct permission
|
||||||
|
pub fn create_jobstate_dir() -> Result<(), Error> {
|
||||||
|
let backup_user = crate::backup::backup_user()?;
|
||||||
|
let opts = CreateOptions::new()
|
||||||
|
.owner(backup_user.uid)
|
||||||
|
.group(backup_user.gid);
|
||||||
|
|
||||||
|
create_path(JOB_STATE_BASEDIR, None, Some(opts))
|
||||||
|
.map_err(|err: Error| format_err!("unable to create rrdb stat dir - {}", err))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_path(jobtype: &str, jobname: &str) -> PathBuf {
|
||||||
|
let mut path = PathBuf::from(JOB_STATE_BASEDIR);
|
||||||
|
path.push(format!("{}-{}.json", jobtype, jobname));
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_lock<P>(path: P) -> Result<File, Error>
|
||||||
|
where
|
||||||
|
P: AsRef<Path>,
|
||||||
|
{
|
||||||
|
let mut path = path.as_ref().to_path_buf();
|
||||||
|
path.set_extension("lck");
|
||||||
|
let lock = open_file_locked(&path, Duration::new(10, 0))?;
|
||||||
|
let backup_user = crate::backup::backup_user()?;
|
||||||
|
nix::unistd::chown(&path, Some(backup_user.uid), Some(backup_user.gid))?;
|
||||||
|
Ok(lock)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the statefile of a job, this is useful if we delete a job
|
||||||
|
pub fn remove_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> {
|
||||||
|
let mut path = get_path(jobtype, jobname);
|
||||||
|
let _lock = get_lock(&path)?;
|
||||||
|
std::fs::remove_file(&path).map_err(|err| {
|
||||||
|
format_err!(
|
||||||
|
"cannot remove statefile for {} - {}: {}",
|
||||||
|
jobtype,
|
||||||
|
jobname,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
path.set_extension("lck");
|
||||||
|
// ignore errors
|
||||||
|
let _ = std::fs::remove_file(&path).map_err(|err| {
|
||||||
|
format_err!(
|
||||||
|
"cannot remove lockfile for {} - {}: {}",
|
||||||
|
jobtype,
|
||||||
|
jobname,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the statefile with the state 'Created'
|
||||||
|
/// overwrites if it exists already
|
||||||
|
pub fn create_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> {
|
||||||
|
let mut job = Job::new(jobtype, jobname)?;
|
||||||
|
job.write_state()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the last run time of a job by reading the statefile
|
||||||
|
/// Note that this is not locked
|
||||||
|
pub fn last_run_time(jobtype: &str, jobname: &str) -> Result<i64, Error> {
|
||||||
|
match JobState::load(jobtype, jobname)? {
|
||||||
|
JobState::Created { time } => Ok(time),
|
||||||
|
JobState::Started { upid } | JobState::Finished { upid, .. } => {
|
||||||
|
let upid: UPID = upid
|
||||||
|
.parse()
|
||||||
|
.map_err(|err| format_err!("could not parse upid from state: {}", err))?;
|
||||||
|
Ok(upid.starttime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JobState {
|
||||||
|
/// Loads and deserializes the jobstate from type and name.
|
||||||
|
/// When the loaded state indicates a started UPID,
|
||||||
|
/// we go and check if it has already stopped, and
|
||||||
|
/// returning the correct state.
|
||||||
|
///
|
||||||
|
/// This does not update the state in the file.
|
||||||
|
pub fn load(jobtype: &str, jobname: &str) -> Result<Self, Error> {
|
||||||
|
if let Some(state) = file_read_optional_string(get_path(jobtype, jobname))? {
|
||||||
|
match serde_json::from_str(&state)? {
|
||||||
|
JobState::Started { upid } => {
|
||||||
|
let parsed: UPID = upid
|
||||||
|
.parse()
|
||||||
|
.map_err(|err| format_err!("error parsing upid: {}", err))?;
|
||||||
|
|
||||||
|
if !worker_is_active_local(&parsed) {
|
||||||
|
let state = upid_read_status(&parsed)
|
||||||
|
.map_err(|err| format_err!("error reading upid log status: {}", err))?;
|
||||||
|
|
||||||
|
Ok(JobState::Finished { upid, state })
|
||||||
|
} else {
|
||||||
|
Ok(JobState::Started { upid })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => Ok(other),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(JobState::Created {
|
||||||
|
time: epoch_now_u64()? as i64 - 30,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Job {
|
||||||
|
/// Creates a new instance of a job with the correct lock held
|
||||||
|
/// (will be hold until the job is dropped again).
|
||||||
|
///
|
||||||
|
/// This does not load the state from the file, to do that,
|
||||||
|
/// 'load' must be called
|
||||||
|
pub fn new(jobtype: &str, jobname: &str) -> Result<Self, Error> {
|
||||||
|
let path = get_path(jobtype, jobname);
|
||||||
|
|
||||||
|
let _lock = get_lock(&path)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
jobtype: jobtype.to_string(),
|
||||||
|
jobname: jobname.to_string(),
|
||||||
|
state: JobState::Created {
|
||||||
|
time: epoch_now_u64()? as i64,
|
||||||
|
},
|
||||||
|
_lock,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the job and update the statefile accordingly
|
||||||
|
/// Fails if the job was already started
|
||||||
|
pub fn start(&mut self, upid: &str) -> Result<(), Error> {
|
||||||
|
match self.state {
|
||||||
|
JobState::Started { .. } => {
|
||||||
|
bail!("cannot start job that is started!");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = JobState::Started {
|
||||||
|
upid: upid.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.write_state()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish the job and update the statefile accordingly with the given taskstate
|
||||||
|
/// Fails if the job was not yet started
|
||||||
|
pub fn finish(&mut self, state: TaskState) -> Result<(), Error> {
|
||||||
|
let upid = match &self.state {
|
||||||
|
JobState::Created { .. } => bail!("cannot finish when not started"),
|
||||||
|
JobState::Started { upid } => upid,
|
||||||
|
JobState::Finished { upid, .. } => upid,
|
||||||
|
}
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
self.state = JobState::Finished { upid, state };
|
||||||
|
|
||||||
|
self.write_state()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jobtype(&self) -> &str {
|
||||||
|
&self.jobtype
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn jobname(&self) -> &str {
|
||||||
|
&self.jobname
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_state(&mut self) -> Result<(), Error> {
|
||||||
|
let serialized = serde_json::to_string(&self.state)?;
|
||||||
|
let path = get_path(&self.jobtype, &self.jobname);
|
||||||
|
|
||||||
|
let backup_user = crate::backup::backup_user()?;
|
||||||
|
let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644);
|
||||||
|
// set the correct owner/group/permissions while saving file
|
||||||
|
// owner(rw) = backup, group(r)= backup
|
||||||
|
let options = CreateOptions::new()
|
||||||
|
.perm(mode)
|
||||||
|
.owner(backup_user.uid)
|
||||||
|
.group(backup_user.gid);
|
||||||
|
|
||||||
|
replace_file(path, serialized.as_bytes(), options)
|
||||||
|
}
|
||||||
|
}
|
@ -600,4 +600,101 @@ mod test {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_config_parser_no_blank_1() -> Result<(), Error> {
|
||||||
|
let input = "auto lo\n\
|
||||||
|
iface lo inet loopback\n\
|
||||||
|
iface lo inet6 loopback\n\
|
||||||
|
auto ens18\n\
|
||||||
|
iface ens18 inet static\n\
|
||||||
|
\taddress 192.168.20.144/20\n\
|
||||||
|
\tgateway 192.168.16.1\n\
|
||||||
|
# comment\n\
|
||||||
|
iface ens20 inet static\n\
|
||||||
|
\taddress 192.168.20.145/20\n\
|
||||||
|
iface ens21 inet manual\n\
|
||||||
|
iface ens22 inet manual\n";
|
||||||
|
|
||||||
|
let mut parser = NetworkParser::new(&input.as_bytes()[..]);
|
||||||
|
|
||||||
|
let config = parser.parse_interfaces(None)?;
|
||||||
|
|
||||||
|
let output = String::try_from(config)?;
|
||||||
|
|
||||||
|
let expected = "auto lo\n\
|
||||||
|
iface lo inet loopback\n\
|
||||||
|
\n\
|
||||||
|
iface lo inet6 loopback\n\
|
||||||
|
\n\
|
||||||
|
auto ens18\n\
|
||||||
|
iface ens18 inet static\n\
|
||||||
|
\taddress 192.168.20.144/20\n\
|
||||||
|
\tgateway 192.168.16.1\n\
|
||||||
|
#comment\n\
|
||||||
|
\n\
|
||||||
|
iface ens20 inet static\n\
|
||||||
|
\taddress 192.168.20.145/20\n\
|
||||||
|
\n\
|
||||||
|
iface ens21 inet manual\n\
|
||||||
|
\n\
|
||||||
|
iface ens22 inet manual\n\
|
||||||
|
\n";
|
||||||
|
assert_eq!(output, expected);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_network_config_parser_no_blank_2() -> Result<(), Error> {
|
||||||
|
// Adapted from bug 2926
|
||||||
|
let input = "### Hetzner Online GmbH installimage\n\
|
||||||
|
\n\
|
||||||
|
source /etc/network/interfaces.d/*\n\
|
||||||
|
\n\
|
||||||
|
auto lo\n\
|
||||||
|
iface lo inet loopback\n\
|
||||||
|
iface lo inet6 loopback\n\
|
||||||
|
\n\
|
||||||
|
auto enp4s0\n\
|
||||||
|
iface enp4s0 inet static\n\
|
||||||
|
\taddress 10.10.10.10/24\n\
|
||||||
|
\tgateway 10.10.10.1\n\
|
||||||
|
\t# route 10.10.20.10/24 via 10.10.20.1\n\
|
||||||
|
\tup route add -net 10.10.20.10 netmask 255.255.255.0 gw 10.10.20.1 dev enp4s0\n\
|
||||||
|
\n\
|
||||||
|
iface enp4s0 inet6 static\n\
|
||||||
|
\taddress fe80::5496:35ff:fe99:5a6a/64\n\
|
||||||
|
\tgateway fe80::1\n";
|
||||||
|
|
||||||
|
let mut parser = NetworkParser::new(&input.as_bytes()[..]);
|
||||||
|
|
||||||
|
let config = parser.parse_interfaces(None)?;
|
||||||
|
|
||||||
|
let output = String::try_from(config)?;
|
||||||
|
|
||||||
|
let expected = "### Hetzner Online GmbH installimage\n\
|
||||||
|
\n\
|
||||||
|
source /etc/network/interfaces.d/*\n\
|
||||||
|
\n\
|
||||||
|
auto lo\n\
|
||||||
|
iface lo inet loopback\n\
|
||||||
|
\n\
|
||||||
|
iface lo inet6 loopback\n\
|
||||||
|
\n\
|
||||||
|
auto enp4s0\n\
|
||||||
|
iface enp4s0 inet static\n\
|
||||||
|
\taddress 10.10.10.10/24\n\
|
||||||
|
\tgateway 10.10.10.1\n\
|
||||||
|
\t# route 10.10.20.10/24 via 10.10.20.1\n\
|
||||||
|
\tup route add -net 10.10.20.10 netmask 255.255.255.0 gw 10.10.20.1 dev enp4s0\n\
|
||||||
|
\n\
|
||||||
|
iface enp4s0 inet6 static\n\
|
||||||
|
\taddress fe80::5496:35ff:fe99:5a6a/64\n\
|
||||||
|
\tgateway fe80::1\n\
|
||||||
|
\n";
|
||||||
|
assert_eq!(output, expected);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -210,9 +210,7 @@ impl <R: BufRead> NetworkParser<R> {
|
|||||||
self.eat(Token::Newline)?;
|
self.eat(Token::Newline)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Token::Newline => break,
|
_ => break,
|
||||||
Token::EOF => break,
|
|
||||||
unexpected => bail!("unexpected token {:?} (expected iface attribute)", unexpected),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match self.peek()? {
|
match self.peek()? {
|
||||||
|
@ -29,6 +29,7 @@ use super::ApiConfig;
|
|||||||
use crate::auth_helpers::*;
|
use crate::auth_helpers::*;
|
||||||
use crate::api2::types::Userid;
|
use crate::api2::types::Userid;
|
||||||
use crate::tools;
|
use crate::tools;
|
||||||
|
use crate::tools::ticket::Ticket;
|
||||||
use crate::config::cached_user_info::CachedUserInfo;
|
use crate::config::cached_user_info::CachedUserInfo;
|
||||||
|
|
||||||
extern "C" { fn tzset(); }
|
extern "C" { fn tzset(); }
|
||||||
@ -463,17 +464,11 @@ fn check_auth(
|
|||||||
token: &Option<String>,
|
token: &Option<String>,
|
||||||
user_info: &CachedUserInfo,
|
user_info: &CachedUserInfo,
|
||||||
) -> Result<Userid, Error> {
|
) -> Result<Userid, Error> {
|
||||||
|
|
||||||
let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
|
let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
|
||||||
|
|
||||||
let userid = match ticket {
|
let ticket = ticket.as_ref().map(String::as_str);
|
||||||
Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) {
|
let userid: Userid = Ticket::parse(&ticket.ok_or_else(|| format_err!("missing ticket"))?)?
|
||||||
Ok((_age, Some(userid))) => userid,
|
.verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)?;
|
||||||
Ok((_, None)) => bail!("ticket without username."),
|
|
||||||
Err(err) => return Err(err),
|
|
||||||
}
|
|
||||||
None => bail!("missing ticket"),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !user_info.is_active_user(&userid) {
|
if !user_info.is_active_user(&userid) {
|
||||||
bail!("user account disabled or expired.");
|
bail!("user account disabled or expired.");
|
||||||
|
@ -2,9 +2,9 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||||||
|
|
||||||
use anyhow::{bail, Error};
|
use anyhow::{bail, Error};
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
|
use proxmox::api::schema::{ApiStringFormat, Schema, StringSchema};
|
||||||
|
use proxmox::const_regex;
|
||||||
use proxmox::sys::linux::procfs;
|
use proxmox::sys::linux::procfs;
|
||||||
|
|
||||||
use crate::api2::types::Userid;
|
use crate::api2::types::Userid;
|
||||||
@ -20,6 +20,7 @@ use crate::api2::types::Userid;
|
|||||||
/// ```
|
/// ```
|
||||||
/// Please note that we use tokio, so a single thread can run multiple
|
/// Please note that we use tokio, so a single thread can run multiple
|
||||||
/// tasks.
|
/// tasks.
|
||||||
|
// #[api] - manually implemented API type
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct UPID {
|
pub struct UPID {
|
||||||
/// The Unix PID
|
/// The Unix PID
|
||||||
@ -40,7 +41,26 @@ pub struct UPID {
|
|||||||
pub node: String,
|
pub node: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxmox::forward_serialize_to_display!(UPID);
|
||||||
|
proxmox::forward_deserialize_to_from_str!(UPID);
|
||||||
|
|
||||||
|
const_regex! {
|
||||||
|
pub PROXMOX_UPID_REGEX = concat!(
|
||||||
|
r"^UPID:(?P<node>[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P<pid>[0-9A-Fa-f]{8}):",
|
||||||
|
r"(?P<pstart>[0-9A-Fa-f]{8,9}):(?P<task_id>[0-9A-Fa-f]{8,16}):(?P<starttime>[0-9A-Fa-f]{8}):",
|
||||||
|
r"(?P<wtype>[^:\s]+):(?P<wid>[^:\s]*):(?P<userid>[^:\s]+):$"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const PROXMOX_UPID_FORMAT: ApiStringFormat =
|
||||||
|
ApiStringFormat::Pattern(&PROXMOX_UPID_REGEX);
|
||||||
|
|
||||||
impl UPID {
|
impl UPID {
|
||||||
|
pub const API_SCHEMA: Schema = StringSchema::new("Unique Process/Task Identifier")
|
||||||
|
.min_length("UPID:N:12345678:12345678:12345678:::".len())
|
||||||
|
.max_length(128) // arbitrary
|
||||||
|
.format(&PROXMOX_UPID_FORMAT)
|
||||||
|
.schema();
|
||||||
|
|
||||||
/// Create a new UPID
|
/// Create a new UPID
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@ -92,17 +112,7 @@ impl std::str::FromStr for UPID {
|
|||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if let Some(cap) = PROXMOX_UPID_REGEX.captures(s) {
|
||||||
lazy_static! {
|
|
||||||
static ref REGEX: Regex = Regex::new(concat!(
|
|
||||||
r"^UPID:(?P<node>[a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?):(?P<pid>[0-9A-Fa-f]{8}):",
|
|
||||||
r"(?P<pstart>[0-9A-Fa-f]{8,9}):(?P<task_id>[0-9A-Fa-f]{8,16}):(?P<starttime>[0-9A-Fa-f]{8}):",
|
|
||||||
r"(?P<wtype>[^:\s]+):(?P<wid>[^:\s]*):(?P<userid>[^:\s]+):$"
|
|
||||||
)).unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(cap) = REGEX.captures(s) {
|
|
||||||
|
|
||||||
Ok(UPID {
|
Ok(UPID {
|
||||||
pid: i32::from_str_radix(&cap["pid"], 16).unwrap(),
|
pid: i32::from_str_radix(&cap["pid"], 16).unwrap(),
|
||||||
pstart: u64::from_str_radix(&cap["pstart"], 16).unwrap(),
|
pstart: u64::from_str_radix(&cap["pstart"], 16).unwrap(),
|
||||||
|
@ -11,6 +11,7 @@ use futures::*;
|
|||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use nix::unistd::Pid;
|
use nix::unistd::Pid;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
use proxmox::sys::linux::procfs;
|
use proxmox::sys::linux::procfs;
|
||||||
@ -155,7 +156,7 @@ pub async fn abort_worker(upid: UPID) -> Result<(), Error> {
|
|||||||
super::send_command(socketname, cmd).map_ok(|_| ()).await
|
super::send_command(socketname, cmd).map_ok(|_| ()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_worker_status_line(line: &str) -> Result<(String, UPID, Option<(i64, String)>), Error> {
|
fn parse_worker_status_line(line: &str) -> Result<(String, UPID, Option<TaskState>), Error> {
|
||||||
|
|
||||||
let data = line.splitn(3, ' ').collect::<Vec<&str>>();
|
let data = line.splitn(3, ' ').collect::<Vec<&str>>();
|
||||||
|
|
||||||
@ -165,7 +166,8 @@ fn parse_worker_status_line(line: &str) -> Result<(String, UPID, Option<(i64, St
|
|||||||
1 => Ok((data[0].to_owned(), data[0].parse::<UPID>()?, None)),
|
1 => Ok((data[0].to_owned(), data[0].parse::<UPID>()?, None)),
|
||||||
3 => {
|
3 => {
|
||||||
let endtime = i64::from_str_radix(data[1], 16)?;
|
let endtime = i64::from_str_radix(data[1], 16)?;
|
||||||
Ok((data[0].to_owned(), data[0].parse::<UPID>()?, Some((endtime, data[2].to_owned()))))
|
let state = TaskState::from_endtime_and_message(endtime, data[2])?;
|
||||||
|
Ok((data[0].to_owned(), data[0].parse::<UPID>()?, Some(state)))
|
||||||
}
|
}
|
||||||
_ => bail!("wrong number of components"),
|
_ => bail!("wrong number of components"),
|
||||||
}
|
}
|
||||||
@ -189,9 +191,12 @@ pub fn create_task_log_dirs() -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read exits status from task log file
|
/// Read endtime (time of last log line) and exitstatus from task log file
|
||||||
pub fn upid_read_status(upid: &UPID) -> Result<String, Error> {
|
/// If there is not a single line with at valid datetime, we assume the
|
||||||
let mut status = String::from("unknown");
|
/// starttime to be the endtime
|
||||||
|
pub fn upid_read_status(upid: &UPID) -> Result<TaskState, Error> {
|
||||||
|
let mut endtime = upid.starttime;
|
||||||
|
let mut status = TaskState::Unknown { endtime };
|
||||||
|
|
||||||
let path = upid.log_path();
|
let path = upid.log_path();
|
||||||
|
|
||||||
@ -207,17 +212,19 @@ pub fn upid_read_status(upid: &UPID) -> Result<String, Error> {
|
|||||||
for line in reader.lines() {
|
for line in reader.lines() {
|
||||||
let line = line?;
|
let line = line?;
|
||||||
|
|
||||||
let mut iter = line.splitn(2, ": TASK ");
|
let mut iter = line.splitn(2, ": ");
|
||||||
if iter.next() == None { continue; }
|
if let Some(time_str) = iter.next() {
|
||||||
match iter.next() {
|
endtime = chrono::DateTime::parse_from_rfc3339(time_str)
|
||||||
|
.map_err(|err| format_err!("cannot parse '{}': {}", time_str, err))?
|
||||||
|
.timestamp();
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match iter.next().and_then(|rest| rest.strip_prefix("TASK ")) {
|
||||||
None => continue,
|
None => continue,
|
||||||
Some(rest) => {
|
Some(rest) => {
|
||||||
if rest == "OK" {
|
if let Ok(state) = TaskState::from_endtime_and_message(endtime, rest) {
|
||||||
status = String::from(rest);
|
status = state;
|
||||||
} else if rest.starts_with("WARNINGS: ") {
|
|
||||||
status = String::from(rest);
|
|
||||||
} else if rest.starts_with("ERROR: ") {
|
|
||||||
status = String::from(&rest[7..]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,6 +233,76 @@ pub fn upid_read_status(upid: &UPID) -> Result<String, Error> {
|
|||||||
Ok(status)
|
Ok(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Task State
|
||||||
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum TaskState {
|
||||||
|
/// The Task ended with an undefined state
|
||||||
|
Unknown { endtime: i64 },
|
||||||
|
/// The Task ended and there were no errors or warnings
|
||||||
|
OK { endtime: i64 },
|
||||||
|
/// The Task had 'count' amount of warnings and no errors
|
||||||
|
Warning { count: u64, endtime: i64 },
|
||||||
|
/// The Task ended with the error described in 'message'
|
||||||
|
Error { message: String, endtime: i64 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskState {
|
||||||
|
pub fn endtime(&self) -> i64 {
|
||||||
|
match *self {
|
||||||
|
TaskState::Unknown { endtime } => endtime,
|
||||||
|
TaskState::OK { endtime } => endtime,
|
||||||
|
TaskState::Warning { endtime, .. } => endtime,
|
||||||
|
TaskState::Error { endtime, .. } => endtime,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn result_text(&self) -> String {
|
||||||
|
match self {
|
||||||
|
TaskState::Error { message, .. } => format!("TASK ERROR: {}", message),
|
||||||
|
other => format!("TASK {}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_endtime_and_message(endtime: i64, s: &str) -> Result<Self, Error> {
|
||||||
|
if s == "unknown" {
|
||||||
|
Ok(TaskState::Unknown { endtime })
|
||||||
|
} else if s == "OK" {
|
||||||
|
Ok(TaskState::OK { endtime })
|
||||||
|
} else if s.starts_with("WARNINGS: ") {
|
||||||
|
let count: u64 = s[10..].parse()?;
|
||||||
|
Ok(TaskState::Warning{ count, endtime })
|
||||||
|
} else if s.len() > 0 {
|
||||||
|
let message = if s.starts_with("ERROR: ") { &s[7..] } else { s }.to_string();
|
||||||
|
Ok(TaskState::Error{ message, endtime })
|
||||||
|
} else {
|
||||||
|
bail!("unable to parse Task Status '{}'", s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::cmp::PartialOrd for TaskState {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.endtime().cmp(&other.endtime()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::cmp::Ord for TaskState {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
self.endtime().cmp(&other.endtime())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TaskState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
TaskState::Unknown { .. } => write!(f, "unknown"),
|
||||||
|
TaskState::OK { .. }=> write!(f, "OK"),
|
||||||
|
TaskState::Warning { count, .. } => write!(f, "WARNINGS: {}", count),
|
||||||
|
TaskState::Error { message, .. } => write!(f, "{}", message),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Task details including parsed UPID
|
/// Task details including parsed UPID
|
||||||
///
|
///
|
||||||
/// If there is no `state`, the task is still running.
|
/// If there is no `state`, the task is still running.
|
||||||
@ -236,9 +313,7 @@ pub struct TaskListInfo {
|
|||||||
/// UPID string representation
|
/// UPID string representation
|
||||||
pub upid_str: String,
|
pub upid_str: String,
|
||||||
/// Task `(endtime, status)` if already finished
|
/// Task `(endtime, status)` if already finished
|
||||||
///
|
pub state: Option<TaskState>, // endtime, status
|
||||||
/// The `status` is either `unknown`, `OK`, `WARN`, or `ERROR: ...`
|
|
||||||
pub state: Option<(i64, String)>, // endtime, status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// atomically read/update the task list, update status of finished tasks
|
// atomically read/update the task list, update status of finished tasks
|
||||||
@ -278,14 +353,14 @@ fn update_active_workers(new_upid: Option<&UPID>) -> Result<Vec<TaskListInfo>, E
|
|||||||
None => {
|
None => {
|
||||||
println!("Detected stopped UPID {}", upid_str);
|
println!("Detected stopped UPID {}", upid_str);
|
||||||
let status = upid_read_status(&upid)
|
let status = upid_read_status(&upid)
|
||||||
.unwrap_or_else(|_| String::from("unknown"));
|
.unwrap_or_else(|_| TaskState::Unknown { endtime: Local::now().timestamp() });
|
||||||
finish_list.push(TaskListInfo {
|
finish_list.push(TaskListInfo {
|
||||||
upid, upid_str, state: Some((Local::now().timestamp(), status))
|
upid, upid_str, state: Some(status)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
Some((endtime, status)) => {
|
Some(status) => {
|
||||||
finish_list.push(TaskListInfo {
|
finish_list.push(TaskListInfo {
|
||||||
upid, upid_str, state: Some((endtime, status))
|
upid, upid_str, state: Some(status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -321,7 +396,7 @@ fn update_active_workers(new_upid: Option<&UPID>) -> Result<Vec<TaskListInfo>, E
|
|||||||
|
|
||||||
task_list.sort_unstable_by(|b, a| { // lastest on top
|
task_list.sort_unstable_by(|b, a| { // lastest on top
|
||||||
match (&a.state, &b.state) {
|
match (&a.state, &b.state) {
|
||||||
(Some(s1), Some(s2)) => s1.0.cmp(&s2.0),
|
(Some(s1), Some(s2)) => s1.cmp(&s2),
|
||||||
(Some(_), None) => std::cmp::Ordering::Less,
|
(Some(_), None) => std::cmp::Ordering::Less,
|
||||||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||||||
_ => a.upid.starttime.cmp(&b.upid.starttime),
|
_ => a.upid.starttime.cmp(&b.upid.starttime),
|
||||||
@ -330,8 +405,8 @@ fn update_active_workers(new_upid: Option<&UPID>) -> Result<Vec<TaskListInfo>, E
|
|||||||
|
|
||||||
let mut raw = String::new();
|
let mut raw = String::new();
|
||||||
for info in &task_list {
|
for info in &task_list {
|
||||||
if let Some((endtime, status)) = &info.state {
|
if let Some(status) = &info.state {
|
||||||
raw.push_str(&format!("{} {:08X} {}\n", info.upid_str, endtime, status));
|
raw.push_str(&format!("{} {:08X} {}\n", info.upid_str, status.endtime(), status));
|
||||||
} else {
|
} else {
|
||||||
raw.push_str(&info.upid_str);
|
raw.push_str(&info.upid_str);
|
||||||
raw.push('\n');
|
raw.push('\n');
|
||||||
@ -473,8 +548,6 @@ impl WorkerTask {
|
|||||||
{
|
{
|
||||||
println!("register worker thread");
|
println!("register worker thread");
|
||||||
|
|
||||||
let (p, c) = oneshot::channel::<()>();
|
|
||||||
|
|
||||||
let worker = WorkerTask::new(worker_type, worker_id, userid, to_stdout)?;
|
let worker = WorkerTask::new(worker_type, worker_id, userid, to_stdout)?;
|
||||||
let upid_str = worker.upid.to_string();
|
let upid_str = worker.upid.to_string();
|
||||||
|
|
||||||
@ -495,31 +568,30 @@ impl WorkerTask {
|
|||||||
};
|
};
|
||||||
|
|
||||||
worker.log_result(&result);
|
worker.log_result(&result);
|
||||||
p.send(()).unwrap();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
tokio::spawn(c.map(|_| ()));
|
|
||||||
|
|
||||||
Ok(upid_str)
|
Ok(upid_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get the Text of the result
|
/// create state from self and a result
|
||||||
pub fn get_log_text(&self, result: &Result<(), Error>) -> String {
|
pub fn create_state(&self, result: &Result<(), Error>) -> TaskState {
|
||||||
|
|
||||||
let warn_count = self.data.lock().unwrap().warn_count;
|
let warn_count = self.data.lock().unwrap().warn_count;
|
||||||
|
|
||||||
|
let endtime = Local::now().timestamp();
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
format!("ERROR: {}", err)
|
TaskState::Error { message: err.to_string(), endtime }
|
||||||
} else if warn_count > 0 {
|
} else if warn_count > 0 {
|
||||||
format!("WARNINGS: {}", warn_count)
|
TaskState::Warning { count: warn_count, endtime }
|
||||||
} else {
|
} else {
|
||||||
"OK".to_string()
|
TaskState::OK { endtime }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log task result, remove task from running list
|
/// Log task result, remove task from running list
|
||||||
pub fn log_result(&self, result: &Result<(), Error>) {
|
pub fn log_result(&self, result: &Result<(), Error>) {
|
||||||
self.log(format!("TASK {}", self.get_log_text(result)));
|
let state = self.create_state(result);
|
||||||
|
self.log(state.result_text());
|
||||||
|
|
||||||
WORKER_TASK_LIST.lock().unwrap().remove(&self.upid.task_id);
|
WORKER_TASK_LIST.lock().unwrap().remove(&self.upid.task_id);
|
||||||
let _ = update_active_workers(None);
|
let _ = update_active_workers(None);
|
||||||
|
26
src/tools.rs
26
src/tools.rs
@ -62,32 +62,6 @@ pub trait BufferedRead {
|
|||||||
fn buffered_read(&mut self, offset: u64) -> Result<&[u8], Error>;
|
fn buffered_read(&mut self, offset: u64) -> Result<&[u8], Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Directly map a type into a binary buffer. This is mostly useful
|
|
||||||
/// for reading structured data from a byte stream (file). You need to
|
|
||||||
/// make sure that the buffer location does not change, so please
|
|
||||||
/// avoid vec resize while you use such map.
|
|
||||||
///
|
|
||||||
/// This function panics if the buffer is not large enough.
|
|
||||||
pub fn map_struct<T>(buffer: &[u8]) -> Result<&T, Error> {
|
|
||||||
if buffer.len() < ::std::mem::size_of::<T>() {
|
|
||||||
bail!("unable to map struct - buffer too small");
|
|
||||||
}
|
|
||||||
Ok(unsafe { &*(buffer.as_ptr() as *const T) })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Directly map a type into a mutable binary buffer. This is mostly
|
|
||||||
/// useful for writing structured data into a byte stream (file). You
|
|
||||||
/// need to make sure that the buffer location does not change, so
|
|
||||||
/// please avoid vec resize while you use such map.
|
|
||||||
///
|
|
||||||
/// This function panics if the buffer is not large enough.
|
|
||||||
pub fn map_struct_mut<T>(buffer: &mut [u8]) -> Result<&mut T, Error> {
|
|
||||||
if buffer.len() < ::std::mem::size_of::<T>() {
|
|
||||||
bail!("unable to map struct - buffer too small");
|
|
||||||
}
|
|
||||||
Ok(unsafe { &mut *(buffer.as_ptr() as *mut T) })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Split a file into equal sized chunks. The last chunk may be
|
/// Split a file into equal sized chunks. The last chunk may be
|
||||||
/// smaller. Note: We cannot implement an `Iterator`, because iterators
|
/// smaller. Note: We cannot implement an `Iterator`, because iterators
|
||||||
/// cannot return a borrowed buffer ref (we want zero-copy)
|
/// cannot return a borrowed buffer ref (we want zero-copy)
|
||||||
|
@ -67,6 +67,19 @@ fn parse_zpool_status_vdev(i: &str) -> IResult<&str, ZFSPoolVDevState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let (i, state) = preceded(multispace1, notspace1)(i)?;
|
let (i, state) = preceded(multispace1, notspace1)(i)?;
|
||||||
|
if let Ok((n, _)) = preceded(multispace0, line_ending)(i) { // spares
|
||||||
|
let vdev = ZFSPoolVDevState {
|
||||||
|
name: vdev_name.to_string(),
|
||||||
|
lvl: indent_level,
|
||||||
|
state: Some(state.to_string()),
|
||||||
|
read: None,
|
||||||
|
write: None,
|
||||||
|
cksum: None,
|
||||||
|
msg: None,
|
||||||
|
};
|
||||||
|
return Ok((n, vdev));
|
||||||
|
}
|
||||||
|
|
||||||
let (i, read) = preceded(multispace1, parse_u64)(i)?;
|
let (i, read) = preceded(multispace1, parse_u64)(i)?;
|
||||||
let (i, write) = preceded(multispace1, parse_u64)(i)?;
|
let (i, write) = preceded(multispace1, parse_u64)(i)?;
|
||||||
let (i, cksum) = preceded(multispace1, parse_u64)(i)?;
|
let (i, cksum) = preceded(multispace1, parse_u64)(i)?;
|
||||||
@ -465,3 +478,40 @@ errors: No known data errors
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_zpool_status_parser_spares() -> Result<(), Error> {
|
||||||
|
|
||||||
|
let output = r###" pool: tank
|
||||||
|
state: ONLINE
|
||||||
|
scan: none requested
|
||||||
|
config:
|
||||||
|
|
||||||
|
NAME STATE READ WRITE CKSUM
|
||||||
|
tank ONLINE 0 0 0
|
||||||
|
mirror-0 ONLINE 0 0 0
|
||||||
|
/dev/sda1 ONLINE 0 0 0
|
||||||
|
/dev/sda2 ONLINE 0 0 0
|
||||||
|
mirror-1 ONLINE 0 0 0
|
||||||
|
/dev/sda3 ONLINE 0 0 0
|
||||||
|
/dev/sda4 ONLINE 0 0 0
|
||||||
|
logs
|
||||||
|
/dev/sda5 ONLINE 0 0 0
|
||||||
|
spares
|
||||||
|
/dev/sdb AVAIL
|
||||||
|
/dev/sdc AVAIL
|
||||||
|
|
||||||
|
errors: No known data errors
|
||||||
|
"###;
|
||||||
|
|
||||||
|
let key_value_list = parse_zpool_status(&output)?;
|
||||||
|
for (k, v) in key_value_list {
|
||||||
|
println!("{} => {}", k,v);
|
||||||
|
if k == "config" {
|
||||||
|
let vdev_list = parse_zpool_status_config_tree(&v)?;
|
||||||
|
let _tree = vdev_list_to_tree(&vdev_list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -83,6 +83,17 @@ pub fn reload_daemon() -> Result<(), Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disable_unit(unit: &str) -> Result<(), Error> {
|
||||||
|
|
||||||
|
let mut command = std::process::Command::new("systemctl");
|
||||||
|
command.arg("disable");
|
||||||
|
command.arg(unit);
|
||||||
|
|
||||||
|
crate::tools::run_command(command, None)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn enable_unit(unit: &str) -> Result<(), Error> {
|
pub fn enable_unit(unit: &str) -> Result<(), Error> {
|
||||||
|
|
||||||
let mut command = std::process::Command::new("systemctl");
|
let mut command = std::process::Command::new("systemctl");
|
||||||
|
@ -1,151 +1,321 @@
|
|||||||
//! Generate and verify Authentication tickets
|
//! Generate and verify Authentication tickets
|
||||||
|
|
||||||
use anyhow::{bail, Error};
|
use std::borrow::Cow;
|
||||||
use base64;
|
use std::io;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
use openssl::pkey::{PKey, Public, Private};
|
use anyhow::{bail, format_err, Error};
|
||||||
use openssl::sign::{Signer, Verifier};
|
|
||||||
use openssl::hash::MessageDigest;
|
use openssl::hash::MessageDigest;
|
||||||
|
use openssl::pkey::{HasPublic, PKey, Private};
|
||||||
|
use openssl::sign::{Signer, Verifier};
|
||||||
|
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet};
|
||||||
|
|
||||||
use crate::api2::types::Userid;
|
use crate::api2::types::Userid;
|
||||||
use crate::tools::epoch_now_u64;
|
use crate::tools::epoch_now_u64;
|
||||||
|
|
||||||
pub const TICKET_LIFETIME: i64 = 3600*2; // 2 hours
|
pub const TICKET_LIFETIME: i64 = 3600 * 2; // 2 hours
|
||||||
|
|
||||||
const TERM_PREFIX: &str = "PBSTERM";
|
pub const TERM_PREFIX: &str = "PBSTERM";
|
||||||
|
|
||||||
pub fn assemble_term_ticket(
|
/// Stringified ticket data must not contain colons...
|
||||||
keypair: &PKey<Private>,
|
const TICKET_ASCIISET: &AsciiSet = &percent_encoding::CONTROLS.add(b':');
|
||||||
userid: &Userid,
|
|
||||||
path: &str,
|
/// An empty type implementing [`ToString`] and [`FromStr`](std::str::FromStr), used for tickets
|
||||||
port: u16,
|
/// with no data.
|
||||||
) -> Result<String, Error> {
|
pub struct Empty;
|
||||||
assemble_rsa_ticket(
|
|
||||||
keypair,
|
impl ToString for Empty {
|
||||||
TERM_PREFIX,
|
fn to_string(&self) -> String {
|
||||||
None,
|
String::new()
|
||||||
Some(&format!("{}{}{}", userid, path, port)),
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn verify_term_ticket(
|
impl std::str::FromStr for Empty {
|
||||||
keypair: &PKey<Public>,
|
type Err = Error;
|
||||||
userid: &Userid,
|
|
||||||
path: &str,
|
|
||||||
port: u16,
|
|
||||||
ticket: &str,
|
|
||||||
) -> Result<(i64, Option<Userid>), Error> {
|
|
||||||
verify_rsa_ticket(
|
|
||||||
keypair,
|
|
||||||
TERM_PREFIX,
|
|
||||||
ticket,
|
|
||||||
Some(&format!("{}{}{}", userid, path, port)),
|
|
||||||
-300,
|
|
||||||
TICKET_LIFETIME,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn assemble_rsa_ticket(
|
fn from_str(s: &str) -> Result<Self, Error> {
|
||||||
keypair: &PKey<Private>,
|
if !s.is_empty() {
|
||||||
prefix: &str,
|
bail!("unexpected ticket data, should be empty");
|
||||||
data: Option<&Userid>,
|
|
||||||
secret_data: Option<&str>,
|
|
||||||
) -> Result<String, Error> {
|
|
||||||
|
|
||||||
let epoch = epoch_now_u64()?;
|
|
||||||
|
|
||||||
let timestamp = format!("{:08X}", epoch);
|
|
||||||
|
|
||||||
let mut plain = prefix.to_owned();
|
|
||||||
plain.push(':');
|
|
||||||
|
|
||||||
if let Some(data) = data {
|
|
||||||
use std::fmt::Write;
|
|
||||||
write!(plain, "{}", data)?;
|
|
||||||
plain.push(':');
|
|
||||||
}
|
|
||||||
|
|
||||||
plain.push_str(×tamp);
|
|
||||||
|
|
||||||
let mut full = plain.clone();
|
|
||||||
if let Some(secret) = secret_data {
|
|
||||||
full.push(':');
|
|
||||||
full.push_str(secret);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut signer = Signer::new(MessageDigest::sha256(), &keypair)?;
|
|
||||||
signer.update(full.as_bytes())?;
|
|
||||||
let sign = signer.sign_to_vec()?;
|
|
||||||
|
|
||||||
let sign_b64 = base64::encode_config(&sign, base64::STANDARD_NO_PAD);
|
|
||||||
|
|
||||||
Ok(format!("{}::{}", plain, sign_b64))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify_rsa_ticket(
|
|
||||||
keypair: &PKey<Public>,
|
|
||||||
prefix: &str,
|
|
||||||
ticket: &str,
|
|
||||||
secret_data: Option<&str>,
|
|
||||||
min_age: i64,
|
|
||||||
max_age: i64,
|
|
||||||
) -> Result<(i64, Option<Userid>), Error> {
|
|
||||||
|
|
||||||
use std::collections::VecDeque;
|
|
||||||
|
|
||||||
let mut parts: VecDeque<&str> = ticket.split(':').collect();
|
|
||||||
|
|
||||||
match parts.pop_front() {
|
|
||||||
Some(text) => if text != prefix { bail!("ticket with invalid prefix"); }
|
|
||||||
None => bail!("ticket without prefix"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let sign_b64 = match parts.pop_back() {
|
|
||||||
Some(v) => v,
|
|
||||||
None => bail!("ticket without signature"),
|
|
||||||
};
|
|
||||||
|
|
||||||
match parts.pop_back() {
|
|
||||||
Some(text) => if text != "" { bail!("ticket with invalid signature separator"); }
|
|
||||||
None => bail!("ticket without signature separator"),
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut data = None;
|
|
||||||
|
|
||||||
let mut full = match parts.len() {
|
|
||||||
2 => {
|
|
||||||
data = Some(parts[0].to_owned());
|
|
||||||
format!("{}:{}:{}", prefix, parts[0], parts[1])
|
|
||||||
}
|
}
|
||||||
1 => format!("{}:{}", prefix, parts[0]),
|
Ok(Empty)
|
||||||
_ => bail!("ticket with invalid number of components"),
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
if let Some(secret) = secret_data {
|
/// An API ticket consists of a ticket type (prefix), type-dependent data, optional additional
|
||||||
full.push(':');
|
/// authenticaztion data, a timestamp and a signature. We store these values in the form
|
||||||
full.push_str(secret);
|
/// `<prefix>:<stringified data>:<timestamp>::<signature>`.
|
||||||
|
///
|
||||||
|
/// The signature is made over the string consisting of prefix, data, timestamp and aad joined
|
||||||
|
/// together by colons. If there is no additional authentication data it will be skipped together
|
||||||
|
/// with the colon separating it from the timestamp.
|
||||||
|
pub struct Ticket<T>
|
||||||
|
where
|
||||||
|
T: ToString + std::str::FromStr,
|
||||||
|
{
|
||||||
|
prefix: Cow<'static, str>,
|
||||||
|
data: String,
|
||||||
|
time: i64,
|
||||||
|
signature: Option<Vec<u8>>,
|
||||||
|
_type_marker: PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Ticket<T>
|
||||||
|
where
|
||||||
|
T: ToString + std::str::FromStr,
|
||||||
|
<T as std::str::FromStr>::Err: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
/// Prepare a new ticket for signing.
|
||||||
|
pub fn new(prefix: &'static str, data: &T) -> Result<Self, Error> {
|
||||||
|
Ok(Self {
|
||||||
|
prefix: Cow::Borrowed(prefix),
|
||||||
|
data: data.to_string(),
|
||||||
|
time: epoch_now_u64()? as i64,
|
||||||
|
signature: None,
|
||||||
|
_type_marker: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the ticket prefix.
|
||||||
|
pub fn prefix(&self) -> &str {
|
||||||
|
&self.prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the ticket's time stamp in seconds since the unix epoch.
|
||||||
|
pub fn time(&self) -> i64 {
|
||||||
|
self.time
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the raw string data contained in the ticket. The `verify` method will call `parse()`
|
||||||
|
/// this in the end, so using this method directly is discouraged as it does not verify the
|
||||||
|
/// signature.
|
||||||
|
pub fn raw_data(&self) -> &str {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize the ticket into a writer.
|
||||||
|
///
|
||||||
|
/// This only writes a string. We use `io::write` instead of `fmt::Write` so we can reuse the
|
||||||
|
/// same function for openssl's `Verify`, which only implements `io::Write`.
|
||||||
|
fn write_data(&self, f: &mut dyn io::Write) -> Result<(), Error> {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}:{}:{:08X}",
|
||||||
|
percent_encode(self.prefix.as_bytes(), &TICKET_ASCIISET),
|
||||||
|
percent_encode(self.data.as_bytes(), &TICKET_ASCIISET),
|
||||||
|
self.time,
|
||||||
|
)
|
||||||
|
.map_err(Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write additional authentication data to the verifier.
|
||||||
|
fn write_aad(f: &mut dyn io::Write, aad: Option<&str>) -> Result<(), Error> {
|
||||||
|
if let Some(aad) = aad {
|
||||||
|
write!(f, ":{}", percent_encode(aad.as_bytes(), &TICKET_ASCIISET))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the ticket's time, used mostly for testing.
|
||||||
|
#[cfg(test)]
|
||||||
|
fn change_time(&mut self, time: i64) -> &mut Self {
|
||||||
|
self.time = time;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sign the ticket.
|
||||||
|
pub fn sign(&mut self, keypair: &PKey<Private>, aad: Option<&str>) -> Result<String, Error> {
|
||||||
|
let mut output = Vec::<u8>::new();
|
||||||
|
let mut signer = Signer::new(MessageDigest::sha256(), &keypair)
|
||||||
|
.map_err(|err| format_err!("openssl error creating signer for ticket: {}", err))?;
|
||||||
|
|
||||||
|
self.write_data(&mut output)
|
||||||
|
.map_err(|err| format_err!("error creating ticket: {}", err))?;
|
||||||
|
|
||||||
|
signer
|
||||||
|
.update(&output)
|
||||||
|
.map_err(Error::from)
|
||||||
|
.and_then(|()| Self::write_aad(&mut signer, aad))
|
||||||
|
.map_err(|err| format_err!("error signing ticket: {}", err))?;
|
||||||
|
|
||||||
|
// See `Self::write_data` for why this is safe
|
||||||
|
let mut output = unsafe { String::from_utf8_unchecked(output) };
|
||||||
|
|
||||||
|
let signature = signer
|
||||||
|
.sign_to_vec()
|
||||||
|
.map_err(|err| format_err!("error finishing ticket signature: {}", err))?;
|
||||||
|
|
||||||
|
use std::fmt::Write;
|
||||||
|
write!(
|
||||||
|
&mut output,
|
||||||
|
"::{}",
|
||||||
|
base64::encode_config(&signature, base64::STANDARD_NO_PAD),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.signature = Some(signature);
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `verify` with an additional time frame parameter, not usually required since we always use
|
||||||
|
/// the same time frame.
|
||||||
|
pub fn verify_with_time_frame<P: HasPublic>(
|
||||||
|
&self,
|
||||||
|
keypair: &PKey<P>,
|
||||||
|
prefix: &str,
|
||||||
|
aad: Option<&str>,
|
||||||
|
time_frame: std::ops::Range<i64>,
|
||||||
|
) -> Result<T, Error> {
|
||||||
|
if self.prefix != prefix {
|
||||||
|
bail!("ticket with invalid prefix");
|
||||||
|
}
|
||||||
|
|
||||||
|
let signature = match self.signature.as_ref() {
|
||||||
|
Some(sig) => sig,
|
||||||
|
None => bail!("invalid ticket without signature"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let age = epoch_now_u64()? as i64 - self.time;
|
||||||
|
if age < time_frame.start {
|
||||||
|
bail!("invalid ticket - timestamp newer than expected");
|
||||||
|
}
|
||||||
|
if age > time_frame.end {
|
||||||
|
bail!("invalid ticket - expired");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut verifier = Verifier::new(MessageDigest::sha256(), &keypair)?;
|
||||||
|
|
||||||
|
self.write_data(&mut verifier)
|
||||||
|
.and_then(|()| Self::write_aad(&mut verifier, aad))
|
||||||
|
.map_err(|err| format_err!("error verifying ticket: {}", err))?;
|
||||||
|
|
||||||
|
let is_valid: bool = verifier
|
||||||
|
.verify(&signature)
|
||||||
|
.map_err(|err| format_err!("openssl error verifying ticket: {}", err))?;
|
||||||
|
|
||||||
|
if !is_valid {
|
||||||
|
bail!("ticket with invalid signature");
|
||||||
|
}
|
||||||
|
|
||||||
|
self.data
|
||||||
|
.parse()
|
||||||
|
.map_err(|err| format_err!("failed to parse contained ticket data: {:?}", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the ticket with the provided key pair. The additional authentication data needs to
|
||||||
|
/// match the one used when generating the ticket, and the ticket's age must fall into the time
|
||||||
|
/// frame.
|
||||||
|
pub fn verify<P: HasPublic>(
|
||||||
|
&self,
|
||||||
|
keypair: &PKey<P>,
|
||||||
|
prefix: &str,
|
||||||
|
aad: Option<&str>,
|
||||||
|
) -> Result<T, Error> {
|
||||||
|
self.verify_with_time_frame(keypair, prefix, aad, -300..TICKET_LIFETIME)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a ticket string.
|
||||||
|
pub fn parse(ticket: &str) -> Result<Self, Error> {
|
||||||
|
let mut parts = ticket.splitn(4, ':');
|
||||||
|
|
||||||
|
let prefix = percent_decode_str(
|
||||||
|
parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format_err!("ticket without prefix"))?,
|
||||||
|
)
|
||||||
|
.decode_utf8()
|
||||||
|
.map_err(|err| format_err!("invalid ticket, error decoding prefix: {}", err))?;
|
||||||
|
|
||||||
|
let data = percent_decode_str(
|
||||||
|
parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format_err!("ticket without data"))?,
|
||||||
|
)
|
||||||
|
.decode_utf8()
|
||||||
|
.map_err(|err| format_err!("invalid ticket, error decoding data: {}", err))?;
|
||||||
|
|
||||||
|
let time = i64::from_str_radix(
|
||||||
|
parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format_err!("ticket without timestamp"))?,
|
||||||
|
16,
|
||||||
|
)
|
||||||
|
.map_err(|err| format_err!("ticket with bad timestamp: {}", err))?;
|
||||||
|
|
||||||
|
let remainder = parts
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format_err!("ticket without signature"))?;
|
||||||
|
// <prefix>:<data>:<time>::signature - the 4th `.next()` swallows the first colon in the
|
||||||
|
// double-colon!
|
||||||
|
if !remainder.starts_with(':') {
|
||||||
|
bail!("ticket without signature separator");
|
||||||
|
}
|
||||||
|
let signature = base64::decode_config(&remainder[1..], base64::STANDARD_NO_PAD)
|
||||||
|
.map_err(|err| format_err!("ticket with bad signature: {}", err))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
prefix: Cow::Owned(prefix.into_owned()),
|
||||||
|
data: data.into_owned(),
|
||||||
|
time,
|
||||||
|
signature: Some(signature),
|
||||||
|
_type_marker: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn term_aad(userid: &Userid, path: &str, port: u16) -> String {
|
||||||
|
format!("{}{}{}", userid, path, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use openssl::pkey::{PKey, Private};
|
||||||
|
|
||||||
|
use super::Ticket;
|
||||||
|
use crate::api2::types::Userid;
|
||||||
|
use crate::tools::epoch_now_u64;
|
||||||
|
|
||||||
|
fn simple_test<F>(key: &PKey<Private>, aad: Option<&str>, modify: F)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Ticket<Userid>) -> bool,
|
||||||
|
{
|
||||||
|
let userid = Userid::root_userid();
|
||||||
|
|
||||||
|
let mut ticket = Ticket::new("PREFIX", userid).expect("failed to create Ticket struct");
|
||||||
|
let should_work = modify(&mut ticket);
|
||||||
|
let ticket = ticket.sign(key, aad).expect("failed to sign test ticket");
|
||||||
|
|
||||||
|
let parsed =
|
||||||
|
Ticket::<Userid>::parse(&ticket).expect("failed to parse generated test ticket");
|
||||||
|
if should_work {
|
||||||
|
let check: Userid = parsed
|
||||||
|
.verify(key, "PREFIX", aad)
|
||||||
|
.expect("failed to verify test ticket");
|
||||||
|
|
||||||
|
assert_eq!(*userid, check);
|
||||||
|
} else {
|
||||||
|
parsed
|
||||||
|
.verify(key, "PREFIX", aad)
|
||||||
|
.expect_err("failed to verify test ticket");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tickets() {
|
||||||
|
// first we need keys, for testing we use small keys for speed...
|
||||||
|
let rsa =
|
||||||
|
openssl::rsa::Rsa::generate(1024).expect("failed to generate RSA key for testing");
|
||||||
|
let key = openssl::pkey::PKey::<openssl::pkey::Private>::from_rsa(rsa)
|
||||||
|
.expect("failed to create PKey for RSA key");
|
||||||
|
|
||||||
|
simple_test(&key, Some("secret aad data"), |_| true);
|
||||||
|
simple_test(&key, None, |_| true);
|
||||||
|
simple_test(&key, None, |t| {
|
||||||
|
t.change_time(0);
|
||||||
|
false
|
||||||
|
});
|
||||||
|
simple_test(&key, None, |t| {
|
||||||
|
t.change_time(epoch_now_u64().unwrap() as i64 + 0x1000_0000);
|
||||||
|
false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let sign = base64::decode_config(sign_b64, base64::STANDARD_NO_PAD)?;
|
|
||||||
|
|
||||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &keypair)?;
|
|
||||||
verifier.update(full.as_bytes())?;
|
|
||||||
|
|
||||||
if !verifier.verify(&sign)? {
|
|
||||||
bail!("ticket with invalid signature");
|
|
||||||
}
|
|
||||||
|
|
||||||
let timestamp = i64::from_str_radix(parts.pop_back().unwrap(), 16)?;
|
|
||||||
let now = epoch_now_u64()? as i64;
|
|
||||||
|
|
||||||
let age = now - timestamp;
|
|
||||||
if age < min_age {
|
|
||||||
bail!("invalid ticket - timestamp newer than expected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if age > max_age {
|
|
||||||
bail!("invalid ticket - timestamp too old.");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((age, data.map(|s| s.parse()).transpose()?))
|
|
||||||
}
|
}
|
||||||
|
@ -107,11 +107,27 @@ Ext.define('PBS.config.SyncJobView', {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (value === 'OK') {
|
let parsed = Proxmox.Utils.parse_task_status(value);
|
||||||
return `<i class="fa fa-check good"></i> ${gettext("OK")}`;
|
let text = value;
|
||||||
|
let icon = '';
|
||||||
|
switch (parsed) {
|
||||||
|
case 'unknown':
|
||||||
|
icon = 'question faded';
|
||||||
|
text = Proxmox.Utils.unknownText;
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
icon = 'times critical';
|
||||||
|
text = Proxmox.Utils.errorText + ': ' + value;
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
icon = 'exclamation warning';
|
||||||
|
break;
|
||||||
|
case 'ok':
|
||||||
|
icon = 'check good';
|
||||||
|
text = gettext("OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
return `<i class="fa fa-times critical"></i> ${gettext("Error")}:${value}`;
|
return `<i class="fa fa-${icon}"></i> ${text}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
render_next_run: function(value, metadat, record) {
|
render_next_run: function(value, metadat, record) {
|
||||||
@ -198,26 +214,26 @@ Ext.define('PBS.config.SyncJobView', {
|
|||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
header: gettext('Sync Job'),
|
header: gettext('Sync Job'),
|
||||||
width: 200,
|
width: 100,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
renderer: Ext.String.htmlEncode,
|
renderer: Ext.String.htmlEncode,
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: gettext('Remote'),
|
header: gettext('Remote'),
|
||||||
width: 200,
|
width: 100,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
dataIndex: 'remote',
|
dataIndex: 'remote',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: gettext('Remote Store'),
|
header: gettext('Remote Store'),
|
||||||
width: 200,
|
width: 100,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
dataIndex: 'remote-store',
|
dataIndex: 'remote-store',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: gettext('Local Store'),
|
header: gettext('Local Store'),
|
||||||
width: 200,
|
width: 100,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
dataIndex: 'store',
|
dataIndex: 'store',
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user