├── debian ├── docs ├── source │ └── format ├── proxmox-datacenter-manager-docs.install ├── proxmox-datacenter-manager-docs.lintian-overrides ├── pdm-enterprise.sources ├── proxmox-datacenter-manager.lintian-overrides ├── proxmox-datacenter-manager-client.install ├── proxmox-datacenter-manager.prerm ├── scripts │ └── elf-strip-unused-dependencies.sh ├── proxmox-datacenter-manager-docs.links ├── copyright ├── proxmox-datacenter-manager.install ├── rules └── proxmox-datacenter-manager.postinst ├── rustfmt.toml ├── ui ├── debian │ ├── docs │ ├── source │ │ └── format │ ├── scripts │ │ └── elf-strip-unused-dependencies.sh │ ├── copyright │ ├── rules │ └── control ├── src │ ├── sdn │ │ ├── mod.rs │ │ └── evpn │ │ │ └── mod.rs │ ├── remotes │ │ ├── firewall │ │ │ └── mod.rs │ │ └── mod.rs │ ├── widget │ │ ├── mod.rs │ │ ├── view_selector.rs │ │ └── remote_endpoint_selector.rs │ ├── view_list_context.rs │ ├── search_provider.rs │ ├── load_result.rs │ ├── dashboard │ │ ├── mod.rs │ │ └── resource_tree.rs │ ├── configuration │ │ └── permission_path_selector.rs │ ├── pbs │ │ └── datastore.rs │ ├── certificates.rs │ └── pve │ │ └── remote │ │ └── mod.rs ├── css │ ├── crisp-yew-style.scss │ ├── material-yew-style.scss │ ├── desktop-yew-style.scss │ └── pdm.scss ├── README.md ├── images │ ├── icon-sdn-vnet.svg │ ├── icon-cd-drive.svg │ ├── icon-memory.svg │ ├── favicon.svg │ ├── icon-cpu.svg │ ├── icon-sdn.svg │ ├── proxmox_logo_white.svg │ └── proxmox_logo.svg ├── Trunk.toml ├── index.html ├── index.hbs └── Cargo.toml ├── lib ├── pdm-ui-shared │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── pdm-buildcfg │ ├── Cargo.toml │ ├── build.rs │ └── src │ │ └── lib.rs ├── pdm-search │ └── Cargo.toml ├── pdm-api-types │ ├── src │ │ ├── metric_collection.rs │ │ ├── user.rs │ │ ├── proxy.rs │ │ ├── pbs.rs │ │ ├── translation.rs │ │ └── node_config.rs │ └── Cargo.toml ├── pdm-client │ └── Cargo.toml └── pdm-config │ ├── Cargo.toml │ └── src │ ├── views.rs │ ├── lib.rs │ ├── node.rs │ └── setup.rs ├── docs ├── images │ ├── favicon.ico │ ├── proxmox-logo.png │ └── screenshots │ │ ├── custom-view.png │ │ ├── login-window.png │ │ ├── remote-node-shell.png │ │ ├── all-remotes-firewall.png │ │ ├── all-remotes-updates.png │ │ ├── remote-node-overview.png │ │ └── global-overview-dashboard.png ├── proxmox-datacenter-manager-admin │ ├── description.rst │ └── man1.rst ├── todos.rst ├── faq-release-support-table.csv ├── proxmox-datacenter-manager-client │ ├── description.rst │ └── man1.rst ├── _templates │ ├── sidebar-header.html │ └── index-sidebar.html ├── proxmox-datacenter-api │ ├── description.rst │ └── man1.rst ├── _static │ └── custom.js ├── services.rst ├── command-syntax.rst ├── command-line-tools.rst ├── config │ ├── remotes │ │ └── man5.rst │ └── views │ │ └── man5.rst ├── configuration-files.rst ├── proxmox-datacenter-privileged-api │ ├── description.rst │ └── man1.rst ├── api-viewer │ ├── index.html │ └── Makefile ├── dev │ ├── debug-features.md │ └── pdm-permission-system.md ├── pdm-copyright.rst ├── epilog.rst ├── sysadmin.rst ├── index.rst ├── system-requirements.rst ├── views.rst ├── remotes.rst ├── Makefile ├── installation.rst └── faq.rst ├── server ├── src │ ├── test_support │ │ ├── mod.rs │ │ └── temp.rs │ ├── bin │ │ ├── proxmox-datacenter-api │ │ │ └── tasks │ │ │ │ ├── mod.rs │ │ │ │ └── remote_updates.rs │ │ ├── docgen.rs │ │ └── proxmox-datacenter-manager-banner.rs │ ├── env.rs │ ├── acl.rs │ ├── api │ │ ├── sdn │ │ │ └── mod.rs │ │ ├── config │ │ │ ├── access │ │ │ │ └── mod.rs │ │ │ ├── mod.rs │ │ │ └── notes.rs │ │ ├── nodes │ │ │ ├── status.rs │ │ │ ├── report.rs │ │ │ ├── journal.rs │ │ │ ├── syslog.rs │ │ │ ├── time.rs │ │ │ ├── rrddata.rs │ │ │ ├── mod.rs │ │ │ ├── dns.rs │ │ │ └── sdn.rs │ │ ├── metric_collection.rs │ │ ├── pve │ │ │ └── storage.rs │ │ ├── pbs │ │ │ └── node.rs │ │ ├── mod.rs │ │ └── rrd_common.rs │ ├── task_utils.rs │ ├── lib.rs │ ├── resource_cache.rs │ ├── context.rs │ └── auth │ │ ├── csrf.rs │ │ ├── certs.rs │ │ └── key.rs └── Cargo.toml ├── .gitmodules ├── .cargo └── config.toml ├── services ├── proxmox-datacenter-manager-daily-update.timer ├── proxmox-datacenter-manager-daily-update.service ├── Makefile ├── proxmox-datacenter-manager-banner.service ├── proxmox-datacenter-privileged-api.service └── proxmox-datacenter-api.service ├── cli ├── proxmox-fido2 │ └── Cargo.toml ├── pdmAtoB │ ├── Cargo.toml │ └── src │ │ └── main.rs ├── client │ ├── src │ │ ├── upid.rs │ │ ├── time.rs │ │ └── metric_collection.rs │ └── Cargo.toml ├── admin │ ├── Cargo.toml │ └── src │ │ ├── support_status.rs │ │ └── main.rs └── completions │ └── Makefile ├── .gitignore ├── defines.mk ├── visualize_rrd.py └── README.md /debian/docs: -------------------------------------------------------------------------------- 1 | debian/SOURCE 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | -------------------------------------------------------------------------------- /ui/debian/docs: -------------------------------------------------------------------------------- 1 | debian/SOURCE 2 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /ui/debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /lib/pdm-ui-shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod colors; 2 | -------------------------------------------------------------------------------- /ui/src/sdn/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod evpn; 2 | 3 | mod zone_tree; 4 | pub use zone_tree::ZoneTree; 5 | -------------------------------------------------------------------------------- /ui/css/crisp-yew-style.scss: -------------------------------------------------------------------------------- 1 | @import "../pwt-assets/scss/crisp-yew-style"; 2 | @import "pdm"; 3 | -------------------------------------------------------------------------------- /ui/css/material-yew-style.scss: -------------------------------------------------------------------------------- 1 | @import "../pwt-assets/scss/material-yew-style"; 2 | @import "pdm"; 3 | 4 | -------------------------------------------------------------------------------- /docs/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxmox/proxmox-datacenter-manager/HEAD/docs/images/favicon.ico -------------------------------------------------------------------------------- /docs/images/proxmox-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxmox/proxmox-datacenter-manager/HEAD/docs/images/proxmox-logo.png -------------------------------------------------------------------------------- /server/src/test_support/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(remote_config = "faked")] 2 | pub mod fake_remote; 3 | 4 | #[cfg(test)] 5 | pub mod temp; 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ui/pwt-assets"] 2 | path = ui/pwt-assets 3 | url = git://git.proxmox.com/git/ui/proxmox-yew-widget-toolkit-assets.git 4 | -------------------------------------------------------------------------------- /docs/images/screenshots/custom-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxmox/proxmox-datacenter-manager/HEAD/docs/images/screenshots/custom-view.png -------------------------------------------------------------------------------- /docs/images/screenshots/login-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxmox/proxmox-datacenter-manager/HEAD/docs/images/screenshots/login-window.png -------------------------------------------------------------------------------- /docs/proxmox-datacenter-manager-admin/description.rst: -------------------------------------------------------------------------------- 1 | This tool exposes some of the datacenter managers administrative API on the 2 | command line. 3 | -------------------------------------------------------------------------------- /docs/images/screenshots/remote-node-shell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxmox/proxmox-datacenter-manager/HEAD/docs/images/screenshots/remote-node-shell.png -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [source] 2 | [source.debian-packages] 3 | directory = "/usr/share/cargo/registry" 4 | [source.crates-io] 5 | replace-with = "debian-packages" 6 | -------------------------------------------------------------------------------- /docs/images/screenshots/all-remotes-firewall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxmox/proxmox-datacenter-manager/HEAD/docs/images/screenshots/all-remotes-firewall.png -------------------------------------------------------------------------------- /docs/images/screenshots/all-remotes-updates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxmox/proxmox-datacenter-manager/HEAD/docs/images/screenshots/all-remotes-updates.png -------------------------------------------------------------------------------- /docs/images/screenshots/remote-node-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxmox/proxmox-datacenter-manager/HEAD/docs/images/screenshots/remote-node-overview.png -------------------------------------------------------------------------------- /server/src/bin/proxmox-datacenter-api/tasks/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod logrotate; 2 | 3 | pub mod remote_node_mapping; 4 | pub mod remote_tasks; 5 | pub mod remote_updates; 6 | -------------------------------------------------------------------------------- /debian/proxmox-datacenter-manager-docs.install: -------------------------------------------------------------------------------- 1 | usr/share/doc/proxmox-datacenter-manager/html 2 | usr/share/doc/proxmox-datacenter-manager/proxmox-datacenter-manager.pdf 3 | -------------------------------------------------------------------------------- /docs/images/screenshots/global-overview-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proxmox/proxmox-datacenter-manager/HEAD/docs/images/screenshots/global-overview-dashboard.png -------------------------------------------------------------------------------- /docs/todos.rst: -------------------------------------------------------------------------------- 1 | Documentation Todo List 2 | ======================= 3 | 4 | This is an auto-generated list of the todo references in the documentation. 5 | 6 | .. todolist:: 7 | -------------------------------------------------------------------------------- /debian/proxmox-datacenter-manager-docs.lintian-overrides: -------------------------------------------------------------------------------- 1 | proxmox-datacenter-manager-docs: embedded-javascript-library please use * [usr/share/doc/proxmox-datacenter-manager/html/_static/*.js] 2 | -------------------------------------------------------------------------------- /debian/pdm-enterprise.sources: -------------------------------------------------------------------------------- 1 | Types: deb 2 | URIs: https://enterprise.proxmox.com/debian/pdm 3 | Suites: trixie 4 | Components: pdm-enterprise 5 | Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg 6 | -------------------------------------------------------------------------------- /docs/faq-release-support-table.csv: -------------------------------------------------------------------------------- 1 | Proxmox Datacenter Manager , Debian Version , First Release , Debian EOL , Proxmox Datacenter Manager EOL 2 | Proxmox Datacenter Manager 1 , Debian 13 (Trixie) , 2025-12 , TBA , TBA 3 | -------------------------------------------------------------------------------- /docs/proxmox-datacenter-manager-client/description.rst: -------------------------------------------------------------------------------- 1 | This tool implements a datacenter manager client, i.e. it can connect to any datacenter manager with 2 | the same major version and issue management commands, query status and control remotes and their 3 | resources. 4 | -------------------------------------------------------------------------------- /debian/proxmox-datacenter-manager.lintian-overrides: -------------------------------------------------------------------------------- 1 | package-installs-apt-sources [etc/apt/sources.list.d/pdm-enterprise.sources] 2 | uses-dpkg-database-directly [usr/libexec/proxmox/proxmox-datacenter-api] 3 | uses-dpkg-database-directly [usr/libexec/proxmox/proxmox-datacenter-privileged-api] 4 | -------------------------------------------------------------------------------- /lib/pdm-buildcfg/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pdm-buildcfg" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | description = "macros used for PDM related paths such as configdir and rundir" 7 | build = "build.rs" 8 | 9 | [dependencies] 10 | -------------------------------------------------------------------------------- /services/proxmox-datacenter-manager-daily-update.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Daily Proxmox Datacenter Manager update and maintenance activities 3 | 4 | [Timer] 5 | OnCalendar=*-*-* 1:00 6 | RandomizedDelaySec=5h 7 | Persistent=true 8 | 9 | [Install] 10 | WantedBy=timers.target 11 | -------------------------------------------------------------------------------- /docs/_templates/sidebar-header.html: -------------------------------------------------------------------------------- 1 | 6 |

Proxmox Datacenter Manager

7 |
8 | -------------------------------------------------------------------------------- /lib/pdm-search/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pdm-search" 3 | description = "Simple search query language for the PDM web UI" 4 | homepage = "https://www.proxmox.com" 5 | version = "0.2.0" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | -------------------------------------------------------------------------------- /debian/proxmox-datacenter-manager-client.install: -------------------------------------------------------------------------------- 1 | usr/bin/proxmox-datacenter-manager-client 2 | usr/share/bash-completion/completions/proxmox-datacenter-manager-client.bc 3 | usr/share/man/man1/proxmox-datacenter-manager-client.1 4 | usr/share/zsh/vendor-completions/_proxmox-datacenter-manager-client 5 | -------------------------------------------------------------------------------- /docs/proxmox-datacenter-api/description.rst: -------------------------------------------------------------------------------- 1 | This daemon exposes the whole Proxmox Datacenter Manager API on TCP port 8443 using HTTPS. It runs 2 | as user ``www-data`` and has very limited permissions. Operations requiring more permissions are 3 | forwarded to the local ``proxmox-datacenter-privileged-api`` service. 4 | -------------------------------------------------------------------------------- /server/src/env.rs: -------------------------------------------------------------------------------- 1 | pub fn sanitize_environment_vars() { 2 | std::env::set_var("PATH", "/sbin:/bin:/usr/sbin:/usr/bin"); 3 | // Make %ENV safer - as suggested by https://perldoc.perl.org/perlsec.html 4 | for name in &["IFS", "CDPATH", "ENV", "BASH_ENV"] { 5 | std::env::remove_var(name); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /services/proxmox-datacenter-manager-daily-update.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Daily Proxmox Datacenter Manager update and maintenance activities 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Service] 7 | Type=oneshot 8 | ExecStart=/usr/libexec/proxmox/proxmox-datacenter-manager-daily-update 9 | -------------------------------------------------------------------------------- /lib/pdm-ui-shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pdm-ui-shared" 3 | description = "Proxmox Datacenter Manager shared ui modules" 4 | homepage = "https://www.proxmox.com" 5 | 6 | version.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | -------------------------------------------------------------------------------- /docs/_static/custom.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', (event) => { 2 | let activeSection = document.querySelector("a.current"); 3 | if (activeSection) { 4 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView 5 | activeSection.scrollIntoView({ block: 'center' }); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /server/src/acl.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn init() { 2 | static ACCESS_CONTROL_CONFIG: pdm_api_types::AccessControlConfig = 3 | pdm_api_types::AccessControlConfig; 4 | 5 | proxmox_access_control::init::init(&ACCESS_CONTROL_CONFIG, pdm_buildcfg::configdir!("/access")) 6 | .expect("failed to setup access control config"); 7 | } 8 | -------------------------------------------------------------------------------- /docs/proxmox-datacenter-manager-admin/man1.rst: -------------------------------------------------------------------------------- 1 | ================================ 2 | proxmox-datacenter-manager-admin 3 | ================================ 4 | 5 | Synopsis 6 | ======== 7 | 8 | .. include:: synopsis.rst 9 | 10 | Description 11 | ============ 12 | 13 | .. include:: description.rst 14 | 15 | 16 | .. include:: ../pdm-copyright.rst 17 | -------------------------------------------------------------------------------- /docs/proxmox-datacenter-manager-client/man1.rst: -------------------------------------------------------------------------------- 1 | ================================= 2 | proxmox-datacenter-manager-client 3 | ================================ 4 | 5 | Synopsis 6 | ========== 7 | 8 | .. include:: synopsis.rst 9 | 10 | Description 11 | ============ 12 | 13 | .. include:: description.rst 14 | 15 | 16 | .. include:: ../pdm-copyright.rst 17 | -------------------------------------------------------------------------------- /docs/services.rst: -------------------------------------------------------------------------------- 1 | Service Daemons 2 | --------------- 3 | 4 | ``proxmox-datacenter-api`` 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | .. include:: proxmox-datacenter-api/description.rst 8 | 9 | 10 | ``proxmox-datacenter-privileged-api`` 11 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 12 | 13 | .. include:: proxmox-datacenter-privileged-api/description.rst 14 | 15 | -------------------------------------------------------------------------------- /docs/command-syntax.rst: -------------------------------------------------------------------------------- 1 | Command Syntax 2 | ============== 3 | 4 | ``proxmox-datacenter-manager-admin`` 5 | ------------------------------------ 6 | 7 | .. include:: proxmox-datacenter-manager-admin/synopsis.rst 8 | 9 | 10 | ``proxmox-datacenter-manager-client`` 11 | ------------------------------------- 12 | 13 | .. include:: proxmox-datacenter-manager-client/synopsis.rst 14 | -------------------------------------------------------------------------------- /cli/proxmox-fido2/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proxmox-fido2" 3 | description = "Highlevel API to libfido2 with dynamic loading of its library." 4 | homepage = "https://www.proxmox.com" 5 | version = "1.0.0" 6 | 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | libc.workspace = true 13 | log.workspace = true 14 | -------------------------------------------------------------------------------- /docs/command-line-tools.rst: -------------------------------------------------------------------------------- 1 | Command-line Tools 2 | ------------------ 3 | 4 | ``proxmox-datacenter-manager-client`` 5 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | 7 | .. include:: proxmox-datacenter-manager-client/description.rst 8 | 9 | ``proxmox-datacenter-manager-admin`` 10 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 11 | 12 | .. include:: proxmox-datacenter-manager-admin/description.rst 13 | 14 | -------------------------------------------------------------------------------- /docs/_templates/index-sidebar.html: -------------------------------------------------------------------------------- 1 |

Navigation

2 | {{ toctree(includehidden=theme_sidebar_includehidden, collapse=True, titles_only=True) }} 3 | {% if theme_extra_nav_links %} 4 |
5 |

Links

6 | 11 | {% endif %} 12 | -------------------------------------------------------------------------------- /docs/config/remotes/man5.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | remotes.cfg 3 | =========== 4 | 5 | Description 6 | =========== 7 | 8 | The file ``/etc/proxmox-datacenter-manager/remotes.cfg`` is a configuration 9 | file for Proxmox Datacenter Manager. It contains all Proxmox VE and Proxmox 10 | Backup Server remotes that the PDM instances manages. 11 | 12 | Options 13 | ======= 14 | 15 | .. include:: config.rst 16 | 17 | .. include:: ../../pdm-copyright.rst 18 | -------------------------------------------------------------------------------- /cli/pdmAtoB/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pdmAtoB" 3 | description = "Proxmox Datacenter Upgrade Checks" 4 | homepage = "https://www.proxmox.com" 5 | version.workspace = true 6 | authors.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | exclude.workspace = true 11 | 12 | [dependencies] 13 | anyhow.workspace = true 14 | proxmox-upgrade-checks.workspace = true 15 | pdm-buildcfg.workspace = true 16 | -------------------------------------------------------------------------------- /docs/configuration-files.rst: -------------------------------------------------------------------------------- 1 | Configuration Files 2 | =================== 3 | 4 | The Proxmox Datacenter Manager configuration files are stored at 5 | ``/etc/proxmox-datacenter-manager/`` directory. 6 | 7 | 8 | ``remotes.cfg`` 9 | ~~~~~~~~~~~~~~~ 10 | 11 | 12 | Options 13 | ^^^^^^^ 14 | 15 | .. include:: config/remotes/config.rst 16 | 17 | ``views.cfg`` 18 | ~~~~~~~~~~~~~ 19 | 20 | Options 21 | ^^^^^^^ 22 | 23 | .. include:: config/views/config.rst 24 | -------------------------------------------------------------------------------- /docs/config/views/man5.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | views.cfg 3 | ========= 4 | 5 | Description 6 | =========== 7 | 8 | The file ``/etc/proxmox-datacenter-manager/views.cfg`` is a configuration file 9 | for Proxmox Datacenter Manager and is used to configure the customizable views. 10 | It contains the exclude-include filters and layout definition of each view 11 | 12 | Options 13 | ======= 14 | 15 | .. include:: config.rst 16 | 17 | .. include:: ../../pdm-copyright.rst 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.rs.bk 2 | *.build 3 | *.buildinfo 4 | *.changes 5 | *.deb 6 | *.dsc 7 | *.tar.?z 8 | *~ 9 | .create-shell-completions 10 | /docs/*/synopsis.rst 11 | /docs/_ext/__pycache__/ 12 | /docs/api-viewer/*.css 13 | /docs/api-viewer/*.js 14 | /docs/api-viewer/*/*.png 15 | /docs/config/*/config.rst 16 | /docs/output 17 | /target 18 | /ui/dist 19 | /ui/proxmox-datacenter-manager-ui-[0-9].*/ 20 | /ui/target 21 | Cargo.lock 22 | proxmox-datacenter-manager-[0-9].*/ 23 | -------------------------------------------------------------------------------- /docs/proxmox-datacenter-privileged-api/description.rst: -------------------------------------------------------------------------------- 1 | This daemon exposes the Proxmox Datacenter Manager management API through a restricted UNIX socket 2 | at ``/run/proxmox-datacenter-manager/priv.sock``. 3 | The daemon runs as ``root`` and has permission to do all privileged operations. 4 | 5 | NOTE: The daemon listens to a local UNIX socket address only, so you cannot access it from outside. 6 | The ``proxmox-datacenter-api`` daemon exposes the API to the outside world. 7 | -------------------------------------------------------------------------------- /services/Makefile: -------------------------------------------------------------------------------- 1 | SERVICES := \ 2 | proxmox-datacenter-api.service \ 3 | proxmox-datacenter-privileged-api.service \ 4 | proxmox-datacenter-manager-banner.service \ 5 | proxmox-datacenter-manager-daily-update.service \ 6 | proxmox-datacenter-manager-daily-update.timer \ 7 | 8 | .PHONY: all 9 | all: 10 | 11 | .PHONY: install 12 | install: $(SERVICES) 13 | install -d $(DESTDIR)/usr/lib/systemd/system 14 | install -m 0644 $(SERVICES) $(DESTDIR)/usr/lib/systemd/system 15 | -------------------------------------------------------------------------------- /debian/proxmox-datacenter-manager.prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | #DEBHELPER# 6 | 7 | # modeled after dh_systemd_start output 8 | if [ -d /run/systemd/system ] && [ "$1" = remove ]; then 9 | deb-systemd-invoke stop 'proxmox-datacenter-api.service' \ 10 | 'proxmox-datacenter-manager-banner.service' \ 11 | 'proxmox-datacenter-manager-daily-update.timer' \ 12 | 'proxmox-datacenter-privileged-api.service' \ 13 | >/dev/null || true 14 | fi 15 | -------------------------------------------------------------------------------- /debian/scripts/elf-strip-unused-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | binary=$1 4 | 5 | exec 3< <(ldd -u "$binary" | grep -oP '[^/:]+$') 6 | 7 | patchargs="" 8 | dropped="" 9 | while read -r dep; do 10 | dropped="$dep $dropped" 11 | patchargs="--remove-needed $dep $patchargs" 12 | done <&3 13 | exec 3<&- 14 | 15 | if [[ $dropped == "" ]]; then 16 | exit 0 17 | fi 18 | 19 | echo -e "patchelf '$binary' - removing unused dependencies:\n $dropped" 20 | patchelf $patchargs $binary 21 | -------------------------------------------------------------------------------- /ui/debian/scripts/elf-strip-unused-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | binary=$1 4 | 5 | exec 3< <(ldd -u "$binary" | grep -oP '[^/:]+$') 6 | 7 | patchargs="" 8 | dropped="" 9 | while read -r dep; do 10 | dropped="$dep $dropped" 11 | patchargs="--remove-needed $dep $patchargs" 12 | done <&3 13 | exec 3<&- 14 | 15 | if [[ $dropped == "" ]]; then 16 | exit 0 17 | fi 18 | 19 | echo -e "patchelf '$binary' - removing unused dependencies:\n $dropped" 20 | patchelf $patchargs $binary 21 | -------------------------------------------------------------------------------- /services/proxmox-datacenter-manager-banner.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Proxmox Datacenter Manager Login Banner 3 | ConditionPathExists=/usr/libexec/proxmox/proxmox-datacenter-manager-banner 4 | ConditionPathExists=!/usr/bin/pvebanner 5 | DefaultDependencies=no 6 | After=local-fs.target 7 | Before=console-getty.service 8 | 9 | [Service] 10 | ExecStart=/usr/libexec/proxmox/proxmox-datacenter-manager-banner 11 | Type=oneshot 12 | RemainAfterExit=yes 13 | 14 | [Install] 15 | WantedBy=getty.target 16 | -------------------------------------------------------------------------------- /server/src/api/sdn/mod.rs: -------------------------------------------------------------------------------- 1 | use proxmox_router::{list_subdirs_api_method, Router, SubdirMap}; 2 | use proxmox_sortable_macro::sortable; 3 | 4 | pub mod controllers; 5 | pub mod vnets; 6 | pub mod zones; 7 | 8 | #[sortable] 9 | pub const SUBDIRS: SubdirMap = &sorted!([ 10 | ("controllers", &controllers::ROUTER), 11 | ("vnets", &vnets::ROUTER), 12 | ("zones", &zones::ROUTER), 13 | ]); 14 | 15 | pub const ROUTER: Router = Router::new() 16 | .get(&list_subdirs_api_method!(SUBDIRS)) 17 | .subdirs(SUBDIRS); 18 | -------------------------------------------------------------------------------- /services/proxmox-datacenter-privileged-api.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Proxmox Datacenter Manager privileged API daemon 3 | Wants=network-online.target 4 | After=network.target 5 | 6 | [Service] 7 | Type=notify 8 | ExecStartPre=/usr/libexec/proxmox/proxmox-datacenter-privileged-api setup 9 | ExecStart=/usr/libexec/proxmox/proxmox-datacenter-privileged-api 10 | ExecReload=/bin/kill -HUP $MAINPID 11 | PIDFile=/run/proxmox-datacenter-manager/priv.pid 12 | Restart=on-failure 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /defines.mk: -------------------------------------------------------------------------------- 1 | PREFIX = /usr 2 | BINDIR = $(PREFIX)/bin 3 | SBINDIR = $(PREFIX)/sbin 4 | LIBDIR = $(PREFIX)/lib 5 | LIBEXECDIR = $(PREFIX)/libexec 6 | BASHCOMPDIR = $(PREFIX)/share/bash-completion/completions 7 | ZSHCOMPDIR = $(PREFIX)/share/zsh/vendor-completions 8 | MAN1DIR = $(PREFIX)/share/man/man1 9 | MAN5DIR = $(PREFIX)/share/man/man5 10 | DOCDIR = $(PREFIX)/share/doc/proxmox-datacenter-manager 11 | SYSCONFDIR = /etc 12 | ZSH_COMPL_DEST = $(PREFIX)/share/zsh/vendor-completions 13 | 14 | # For local overrides 15 | -include local.mak 16 | -------------------------------------------------------------------------------- /lib/pdm-buildcfg/build.rs: -------------------------------------------------------------------------------- 1 | // build.rs 2 | use std::env; 3 | use std::process::Command; 4 | 5 | fn main() { 6 | let repoid = match env::var("REPOID") { 7 | Ok(repoid) => repoid, 8 | Err(_) => match Command::new("git").args(["rev-parse", "HEAD"]).output() { 9 | Ok(output) => String::from_utf8(output.stdout).unwrap(), 10 | Err(err) => { 11 | panic!("git rev-parse failed: {}", err); 12 | } 13 | }, 14 | }; 15 | 16 | println!("cargo:rustc-env=REPOID={}", repoid); 17 | } 18 | -------------------------------------------------------------------------------- /services/proxmox-datacenter-api.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Proxmox Datacenter Manager API daemon 3 | Wants=network-online.target 4 | After=network.target 5 | Wants=proxmox-datacenter-privileged-api.service 6 | After=proxmox-datacenter-privileged-api.service 7 | 8 | [Service] 9 | Type=notify 10 | ExecStart=/usr/libexec/proxmox/proxmox-datacenter-api 11 | ExecReload=/bin/kill -HUP $MAINPID 12 | PIDFile=/run/proxmox-datacenter-manager/api.pid 13 | Restart=on-failure 14 | User=www-data 15 | Group=www-data 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /server/src/api/config/access/mod.rs: -------------------------------------------------------------------------------- 1 | use proxmox_router::list_subdirs_api_method; 2 | use proxmox_router::{Router, SubdirMap}; 3 | use proxmox_sortable_macro::sortable; 4 | 5 | mod ad; 6 | mod ldap; 7 | mod openid; 8 | pub mod tfa; 9 | 10 | #[sortable] 11 | const SUBDIRS: SubdirMap = &sorted!([ 12 | ("tfa", &tfa::ROUTER), 13 | ("ldap", &ldap::ROUTER), 14 | ("openid", &openid::ROUTER), 15 | ("ad", &ad::ROUTER), 16 | ]); 17 | 18 | pub const ROUTER: Router = Router::new() 19 | .get(&list_subdirs_api_method!(SUBDIRS)) 20 | .subdirs(SUBDIRS); 21 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Experimental Yew GUI for Proxmox Datacenter Manager 2 | 3 | # Testing 4 | 5 | 1.) start datacenter manager 6 | 7 | sudo ./target/debug/proxmox-datacenter-privileged-api 8 | sudo -u www-data ./target/debug/proxmox-datacenter-api 9 | 10 | 2.) either: start local trunk server 11 | 12 | trunk serve --proxy-backend=https://localhost:8443/api2/ --proxy-insecure 13 | 14 | then test with url: http://localhost:8080 15 | 16 | 2.) or: copy files into pdm js folder: 17 | 18 | make all 19 | make install 20 | 21 | And test with url https://localhost:8443 22 | -------------------------------------------------------------------------------- /docs/api-viewer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Proxmox Datacenter Manager API Documentation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/src/api/config/mod.rs: -------------------------------------------------------------------------------- 1 | use proxmox_router::list_subdirs_api_method; 2 | use proxmox_router::{Router, SubdirMap}; 3 | use proxmox_sortable_macro::sortable; 4 | 5 | pub mod access; 6 | pub mod acme; 7 | pub mod certificate; 8 | pub mod notes; 9 | pub mod views; 10 | 11 | #[sortable] 12 | const SUBDIRS: SubdirMap = &sorted!([ 13 | ("access", &access::ROUTER), 14 | ("acme", &acme::ROUTER), 15 | ("certificate", &certificate::ROUTER), 16 | ("notes", ¬es::ROUTER), 17 | ("views", &views::ROUTER) 18 | ]); 19 | 20 | pub const ROUTER: Router = Router::new() 21 | .get(&list_subdirs_api_method!(SUBDIRS)) 22 | .subdirs(SUBDIRS); 23 | -------------------------------------------------------------------------------- /cli/pdmAtoB/src/main.rs: -------------------------------------------------------------------------------- 1 | use pdm_buildcfg::{PROXMOX_PKG_RELEASE, PROXMOX_PKG_VERSION}; 2 | use proxmox_upgrade_checks::UpgradeCheckerBuilder; 3 | 4 | fn main() -> Result<(), anyhow::Error> { 5 | UpgradeCheckerBuilder::new( 6 | "bookworm", 7 | "trixie", 8 | "proxmox-datacenter-manager", 9 | 0, 10 | 1, 11 | 11, 12 | &format!("{PROXMOX_PKG_VERSION}.{PROXMOX_PKG_RELEASE}"), 13 | ) 14 | .apt_state_file_location(pdm_buildcfg::APT_PKG_STATE_FN) 15 | .add_service_to_checks("proxmox-datacenter-api") 16 | .add_service_to_checks("proxmox-datacenter-privileged-api") 17 | .build() 18 | .run() 19 | } 20 | -------------------------------------------------------------------------------- /debian/proxmox-datacenter-manager-docs.links: -------------------------------------------------------------------------------- 1 | /usr/share/doc/proxmox-datacenter-manager/proxmox-datacenter-manager.pdf /usr/share/doc/proxmox-datacenter-manager/html/proxmox-datacenter-manager.pdf 2 | /usr/share/fonts-font-awesome/ /usr/share/doc/proxmox-datacenter-manager/html/lto-barcode/font-awesome 3 | /usr/share/javascript/extjs /usr/share/doc/proxmox-datacenter-manager/html/api-viewer/extjs 4 | /usr/share/javascript/extjs /usr/share/doc/proxmox-datacenter-manager/html/lto-barcode/extjs 5 | /usr/share/javascript/extjs /usr/share/doc/proxmox-datacenter-manager/html/prune-simulator/extjs 6 | /usr/share/javascript/mathjax /usr/share/doc/proxmox-datacenter-manager/html/_static/mathjax 7 | -------------------------------------------------------------------------------- /ui/css/desktop-yew-style.scss: -------------------------------------------------------------------------------- 1 | @import "../pwt-assets/scss/desktop-yew-style"; 2 | @import "pdm"; 3 | 4 | .proxmox-content-spacer { 5 | padding: var(--pwt-spacer-4); 6 | gap: var(--pwt-spacer-4); 7 | 8 | &.proxmox-content-spacer-with-one-child { 9 | padding: 0; 10 | } 11 | 12 | & > * { 13 | border: 0px !important; 14 | border-top: 1px solid var(--pwt-color-surface) !important; 15 | @include elevation-box-shadow(1); 16 | } 17 | } 18 | 19 | // TODO: move to pwt assets once we're sure about doing it this way or drop, if 20 | // we rewroked layouting and placement of the dropdown picker. 21 | .pwt-dropdown { 22 | max-height: min(45dvh, 500px) !important; 23 | } 24 | -------------------------------------------------------------------------------- /lib/pdm-api-types/src/metric_collection.rs: -------------------------------------------------------------------------------- 1 | //! API types for metric collection. 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use proxmox_schema::api; 6 | 7 | #[api] 8 | #[derive(Clone, Deserialize, Serialize)] 9 | #[serde(rename_all = "kebab-case")] 10 | /// Per-remote collection status. 11 | pub struct MetricCollectionStatus { 12 | /// The remote's name. 13 | pub remote: String, 14 | /// Any error that occured during the last collection attempt. 15 | #[serde(skip_serializing_if = "Option::is_none")] 16 | pub error: Option, 17 | /// Timestamp of last successful collection. 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub last_collection: Option, 20 | } 21 | -------------------------------------------------------------------------------- /docs/proxmox-datacenter-api/man1.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | ====================== 4 | proxmox-datacenter-api 5 | ====================== 6 | 7 | Synopsis 8 | ======== 9 | 10 | This daemon is normally started and managed as ``systemd`` service:: 11 | 12 | systemctl start proxmox-datacenter-api 13 | 14 | systemctl stop proxmox-datacenter-api 15 | 16 | systemctl status proxmox-datacenter-api 17 | 18 | For debugging, you can start the daemon in foreground using:: 19 | 20 | sudo -u www-data -g www-data proxmox-datacenter-api 21 | 22 | .. NOTE:: You need to stop the service before starting the daemon in foreground. 23 | 24 | Description 25 | =========== 26 | 27 | .. include:: description.rst 28 | 29 | .. include:: ../pdm-copyright.rst 30 | -------------------------------------------------------------------------------- /docs/dev/debug-features.md: -------------------------------------------------------------------------------- 1 | # Debug features 2 | 3 | ## Faked remotes 4 | 5 | To enable an alternative implementation for reading remote configurations (`/etc/proxmox-datacenter-manager/remotes.cfg`) 6 | and creating API clients for these remotes, compile the project using: 7 | 8 | ```bash 9 | RUSTFLAGS='--cfg remote_config="faked"' 10 | ``` 11 | 12 | This option is helpful for troubleshooting performance issues, especially when managing a large number of remotes 13 | or resources per remote. 14 | 15 | To use this feature, set the `PDM_FAKED_REMOTE_CONFIG` environment variable to the path of a valid 16 | JSON configuration file. For the expected structure, refer to the `FakeRemoteConfig` struct 17 | in `server/src/test_support/fake_remote.rs`. 18 | -------------------------------------------------------------------------------- /ui/src/remotes/firewall/mod.rs: -------------------------------------------------------------------------------- 1 | mod columns; 2 | mod tree; 3 | mod types; 4 | mod ui_helpers; 5 | 6 | // Re-export public types 7 | pub use tree::FirewallTreeComponent; 8 | 9 | use std::rc::Rc; 10 | use yew::virtual_dom::{VComp, VNode}; 11 | use yew::Properties; 12 | 13 | use proxmox_yew_comp::LoadableComponentMaster; 14 | 15 | #[derive(PartialEq, Properties)] 16 | pub struct FirewallTree {} 17 | 18 | impl FirewallTree { 19 | pub fn new() -> Self { 20 | yew::props!(Self {}) 21 | } 22 | } 23 | 24 | impl From for VNode { 25 | fn from(value: FirewallTree) -> Self { 26 | let comp = 27 | VComp::new::>(Rc::new(value), None); 28 | VNode::from(comp) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/task_utils.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; 2 | 3 | /// Returns an [`Instant`] aligned to a certain boundary. 4 | /// 5 | /// For instance, `next_aligned_instant(60)` will return an [`Instant`] aligned to the next minute 6 | /// boundary. 7 | pub fn next_aligned_instant(seconds: u64) -> Instant { 8 | let now = SystemTime::now(); 9 | let epoch_now = match now.duration_since(UNIX_EPOCH) { 10 | Ok(d) => d, 11 | Err(err) => { 12 | log::error!("task scheduler: computing next aligned instant failed - {err}"); 13 | return Instant::now() + Duration::from_secs(seconds); 14 | } 15 | }; 16 | Instant::now() + Duration::from_secs(seconds - epoch_now.as_secs().rem_euclid(seconds)) 17 | } 18 | -------------------------------------------------------------------------------- /docs/pdm-copyright.rst: -------------------------------------------------------------------------------- 1 | Copyright and Disclaimer 2 | ======================== 3 | 4 | |pdm-copyright| 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, but 12 | WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public 17 | License along with this program. If not, see 18 | http://www.gnu.org/licenses/ 19 | -------------------------------------------------------------------------------- /cli/client/src/upid.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Error}; 2 | 3 | use pdm_api_types::RemoteUpid; 4 | 5 | pub fn parse_for_remote(remote: Option<&str>, upid: &str) -> Result { 6 | if upid.contains('!') { 7 | let upid: RemoteUpid = upid.parse()?; 8 | 9 | if let Some(remote) = remote { 10 | if upid.remote() != remote { 11 | bail!( 12 | "remote in UPID ({:?}) does not match expected remote {remote:?}", 13 | upid.remote() 14 | ); 15 | } 16 | } 17 | 18 | Ok(upid) 19 | } else { 20 | match remote { 21 | Some(remote) => format!("{remote}!{upid}").parse(), 22 | None => bail!("upid without a remote"), 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/src/api/nodes/status.rs: -------------------------------------------------------------------------------- 1 | use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_POWER_MANAGEMENT}; 2 | use proxmox_router::{ApiMethod, Permission, Router}; 3 | 4 | const API_METHOD_GET_STATUS_WITH_ACCESS: ApiMethod = proxmox_node_status::API_METHOD_GET_STATUS 5 | .access( 6 | None, 7 | &Permission::Privilege(&["system", "status"], PRIV_SYS_AUDIT, false), 8 | ); 9 | 10 | const API_METHOD_REBOOT_OR_SHUTDOWN_WITH_ACCESS: ApiMethod = 11 | proxmox_node_status::API_METHOD_REBOOT_OR_SHUTDOWN.access( 12 | None, 13 | &Permission::Privilege(&["system", "status"], PRIV_SYS_POWER_MANAGEMENT, false), 14 | ); 15 | 16 | pub const ROUTER: Router = Router::new() 17 | .get(&API_METHOD_GET_STATUS_WITH_ACCESS) 18 | .post(&API_METHOD_REBOOT_OR_SHUTDOWN_WITH_ACCESS); 19 | -------------------------------------------------------------------------------- /lib/pdm-api-types/src/user.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use proxmox_schema::api; 4 | 5 | #[api] 6 | #[derive(Serialize, Deserialize)] 7 | #[serde(rename_all = "lowercase")] 8 | /// Properties of users which can be unset/deleted. 9 | pub enum DeletableUserProperty { 10 | /// Clear the comment field. 11 | Comment, 12 | /// Reset the enabled state to its default of being enabled. 13 | Enable, 14 | /// Clear the expiration date. 15 | Expire, 16 | /// Clear the first name. 17 | Firstname, 18 | /// Clear the last name. 19 | Lastname, 20 | /// Clear the associated email address. 21 | Email, 22 | } 23 | 24 | serde_plain::derive_display_from_serialize!(DeletableUserProperty); 25 | serde_plain::derive_fromstr_from_deserialize!(DeletableUserProperty); 26 | -------------------------------------------------------------------------------- /ui/images/icon-sdn-vnet.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/proxmox-datacenter-privileged-api/man1.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | ================================= 4 | proxmox-datacenter-privileged-api 5 | ================================= 6 | 7 | Synopsis 8 | ======== 9 | 10 | This daemon is normally started and managed as ``systemd`` service:: 11 | 12 | systemctl start proxmox-datacenter-privileged-api 13 | 14 | systemctl stop proxmox-datacenter-privileged-api 15 | 16 | systemctl status proxmox-datacenter-privileged-api 17 | 18 | For debugging, you can start the daemon in foreground as root user through running:: 19 | 20 | proxmox-datacenter-privileged-api 21 | 22 | .. NOTE:: You need to stop the service before starting the daemon in foreground. 23 | 24 | Description 25 | =========== 26 | 27 | .. include:: description.rst 28 | 29 | .. include:: ../pdm-copyright.rst 30 | -------------------------------------------------------------------------------- /ui/Trunk.toml: -------------------------------------------------------------------------------- 1 | [serve] 2 | addresses = ["127.0.0.1", "::1"] # The address to serve on. 3 | 4 | # FIXME: enable once trunk doesn't errors out if ignored files do not exist -.- 5 | # https://github.com/trunk-rs/trunk/issues/172 6 | #[watch] 7 | #ignore = [ "*.thanks", "*.mbox", "*.deb", "*.changes", "*.build", "*.buildinfo", "proxmox-datacenter-manager-ui-[0-9]*/" ] 8 | 9 | [[hooks]] 10 | stage="build" 11 | command="rust-grass" 12 | command_arguments= ["css/crisp-yew-style.scss", "dist/.stage/crisp-yew-style.css"] 13 | 14 | [[hooks]] 15 | stage="build" 16 | command="rust-grass" 17 | command_arguments= ["css/material-yew-style.scss", "dist/.stage/material-yew-style.css"] 18 | 19 | [[hooks]] 20 | stage="build" 21 | command="rust-grass" 22 | command_arguments= ["css/desktop-yew-style.scss", "dist/.stage/desktop-yew-style.css"] 23 | -------------------------------------------------------------------------------- /lib/pdm-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pdm-client" 3 | description = "Proxmox Datacenter Manager client library crate" 4 | homepage = "https://www.proxmox.com" 5 | 6 | version.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | percent-encoding.workspace = true 13 | serde.workspace = true 14 | serde_json.workspace = true 15 | 16 | pdm-api-types.workspace = true 17 | 18 | proxmox-access-control.workspace = true 19 | proxmox-client.workspace = true 20 | proxmox-rrd-api-types.workspace = true 21 | proxmox-tfa = { workspace = true, features = [ "types" ] } 22 | 23 | pve-api-types = { workspace = true, features = [ "client" ] } 24 | pbs-api-types.workspace = true 25 | 26 | [features] 27 | default = [] 28 | hyper-client = [ "proxmox-client/hyper-client" ] 29 | -------------------------------------------------------------------------------- /cli/admin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proxmox-datacenter-manager-admin" 3 | description = "Local Proxmox Datacenter Manager administration CLI" 4 | homepage = "https://www.proxmox.com" 5 | 6 | version.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | anyhow.workspace = true 13 | serde.workspace = true 14 | serde_json.workspace = true 15 | 16 | proxmox-async.workspace = true 17 | proxmox-log.workspace = true 18 | proxmox-product-config.workspace = true 19 | proxmox-router = { workspace = true, features = [ "cli" ], default-features = false } 20 | proxmox-schema = { workspace = true, features = [ "api-macro" ] } 21 | proxmox-access-control.workspace = true 22 | 23 | pdm-api-types.workspace = true 24 | pdm-config.workspace = true 25 | pdm-buildcfg.workspace = true 26 | server.workspace = true 27 | -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Proxmox Datacenter Manager 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /server/src/api/nodes/report.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use proxmox_router::{Permission, Router}; 4 | use proxmox_schema::api; 5 | 6 | use pdm_api_types::{NODE_SCHEMA, PRIV_SYS_AUDIT}; 7 | 8 | use crate::report; 9 | 10 | #[api( 11 | protected: true, 12 | input: { 13 | properties: { 14 | node: { 15 | schema: NODE_SCHEMA, 16 | }, 17 | }, 18 | }, 19 | returns: { 20 | type: String, 21 | description: "The system report for this PDM node.", 22 | }, 23 | access: { 24 | permission: &Permission::Privilege(&["system", "status"], PRIV_SYS_AUDIT, false), 25 | } 26 | )] 27 | /// Get the system report for this node. 28 | pub fn generate_system_report() -> Result { 29 | Ok(report::generate_report()) 30 | } 31 | 32 | pub const ROUTER: Router = Router::new().get(&API_METHOD_GENERATE_SYSTEM_REPORT); 33 | -------------------------------------------------------------------------------- /ui/src/widget/mod.rs: -------------------------------------------------------------------------------- 1 | mod migrate_window; 2 | pub use migrate_window::MigrateWindow; 3 | 4 | mod pve_node_selector; 5 | pub use pve_node_selector::PveNodeSelector; 6 | 7 | mod pve_network_selector; 8 | pub use pve_network_selector::PveNetworkSelector; 9 | 10 | mod pve_storage_selector; 11 | pub use pve_storage_selector::PveStorageSelector; 12 | 13 | mod pve_migrate_mapping; 14 | pub use pve_migrate_mapping::PveMigrateMap; 15 | 16 | mod pve_realm_selector; 17 | pub use pve_realm_selector::PveRealmSelector; 18 | 19 | mod resource_tree; 20 | pub use resource_tree::{RedrawController, ResourceTree}; 21 | 22 | mod search_box; 23 | pub use search_box::SearchBox; 24 | 25 | mod remote_selector; 26 | pub use remote_selector::RemoteSelector; 27 | 28 | mod remote_endpoint_selector; 29 | 30 | mod view_selector; 31 | pub use view_selector::ViewSelector; 32 | 33 | mod view_filter_selector; 34 | pub use view_filter_selector::ViewFilterSelector; 35 | -------------------------------------------------------------------------------- /ui/src/view_list_context.rs: -------------------------------------------------------------------------------- 1 | use pwt::state::{SharedState, SharedStateObserver}; 2 | use yew::Callback; 3 | 4 | #[derive(PartialEq, Clone)] 5 | /// Provides a context for updating and listening to changes of the list of views 6 | pub struct ViewListContext { 7 | state: SharedState, 8 | } 9 | 10 | impl ViewListContext { 11 | /// Create a new context 12 | pub fn new() -> Self { 13 | Self { 14 | state: SharedState::new(0), 15 | } 16 | } 17 | 18 | /// Add a listener to the view list context 19 | pub fn add_listener( 20 | &self, 21 | cb: impl Into>>, 22 | ) -> SharedStateObserver { 23 | self.state.add_listener(cb) 24 | } 25 | 26 | /// Triggers an update of the view list for the main menu 27 | pub fn update_views(&self) { 28 | let mut state = self.state.write(); 29 | **state = state.saturating_add(1); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/search_provider.rs: -------------------------------------------------------------------------------- 1 | use yew::Callback; 2 | 3 | use pwt::state::{SharedState, SharedStateObserver}; 4 | 5 | use pdm_search::Search; 6 | 7 | #[derive(Clone, PartialEq)] 8 | pub struct SearchProvider { 9 | state: SharedState, 10 | } 11 | 12 | impl SearchProvider { 13 | pub fn new() -> Self { 14 | Self { 15 | state: SharedState::new("".into()), 16 | } 17 | } 18 | 19 | pub fn add_listener( 20 | &self, 21 | cb: impl Into>>, 22 | ) -> SharedStateObserver { 23 | self.state.add_listener(cb) 24 | } 25 | 26 | pub fn search(&self, search_term: Search) { 27 | **self.state.write() = search_term.to_string(); 28 | } 29 | } 30 | 31 | pub fn get_search_provider(ctx: &yew::Context) -> Option { 32 | let (provider, _context_listener) = ctx.link().context(Callback::from(|_| {}))?; 33 | 34 | Some(provider) 35 | } 36 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://git.proxmox.com/?p=proxmox-datacenter-manager.git;a=summary 3 | 4 | Files: 5 | * 6 | Copyright: 2023 - 2024 Proxmox Server Solutions GmbH 7 | License: AGPL-3.0-or-later 8 | This program is free software: you can redistribute it and/or modify it under 9 | the terms of the GNU Affero General Public License as published by the Free 10 | Software Foundation, either version 3 of the License, or (at your option) any 11 | later version. 12 | . 13 | This program is distributed in the hope that it will be useful, but WITHOUT 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 16 | details. 17 | . 18 | You should have received a copy of the GNU Affero General Public License along 19 | with this program. If not, see . 20 | -------------------------------------------------------------------------------- /lib/pdm-api-types/src/proxy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Error}; 2 | use http::Uri; 3 | 4 | use proxmox_schema::{ApiStringFormat, Schema, StringSchema}; 5 | 6 | fn verify_proxy_url(http_proxy: &str) -> Result<(), Error> { 7 | let proxy_uri: Uri = http_proxy.parse()?; 8 | if proxy_uri.authority().is_none() { 9 | bail!("missing proxy authority"); 10 | }; 11 | 12 | match proxy_uri.scheme_str() { 13 | Some("http") => { /* Ok */ } 14 | Some(scheme) => bail!("unsupported proxy scheme '{}'", scheme), 15 | None => { /* assume HTTP */ } 16 | } 17 | 18 | Ok(()) 19 | } 20 | 21 | pub const HTTP_PROXY_SCHEMA: Schema = 22 | StringSchema::new("HTTP proxy configuration [http://][:port]") 23 | .format(&ApiStringFormat::VerifyFn(|s| { 24 | verify_proxy_url(s)?; 25 | Ok(()) 26 | })) 27 | .min_length(1) 28 | .max_length(128) 29 | .type_text("[http://][:port]") 30 | .schema(); 31 | -------------------------------------------------------------------------------- /lib/pdm-config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pdm-config" 3 | version = "0.1.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | description = "Configuration file management for PDM" 7 | 8 | [dependencies] 9 | anyhow.workspace = true 10 | log.workspace = true 11 | nix.workspace = true 12 | once_cell.workspace = true 13 | openssl.workspace = true 14 | serde.workspace = true 15 | 16 | proxmox-config-digest = { workspace = true, features = [ "openssl" ] } 17 | proxmox-http = { workspace = true, features = [ "http-helpers" ] } 18 | proxmox-ldap = { workspace = true, features = [ "types" ]} 19 | proxmox-product-config.workspace = true 20 | proxmox-schema.workspace = true 21 | proxmox-section-config.workspace = true 22 | proxmox-shared-memory.workspace = true 23 | proxmox-simple-config.workspace = true 24 | proxmox-sys = { workspace = true, features = [ "acl", "crypt", "timer" ] } 25 | proxmox-acme-api.workspace = true 26 | pdm-api-types.workspace = true 27 | pdm-buildcfg.workspace = true 28 | -------------------------------------------------------------------------------- /ui/debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Source: https://git.proxmox.com/?p=proxmox-datacenter-manager.git;a=summary 3 | 4 | Files: 5 | * 6 | Copyright: 2023 - 2024 Proxmox Server Solutions GmbH 7 | License: AGPL-3.0-or-later 8 | This program is free software: you can redistribute it and/or modify it under 9 | the terms of the GNU Affero General Public License as published by the Free 10 | Software Foundation, either version 3 of the License, or (at your option) any 11 | later version. 12 | . 13 | This program is distributed in the hope that it will be useful, but WITHOUT 14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 | FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 16 | details. 17 | . 18 | You should have received a copy of the GNU Affero General Public License along 19 | with this program. If not, see . 20 | -------------------------------------------------------------------------------- /ui/images/icon-cd-drive.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /server/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Common API crate for PDM. 2 | 3 | pub mod acl; 4 | pub mod api; 5 | pub mod auth; 6 | pub mod context; 7 | pub mod env; 8 | pub mod jobstate; 9 | pub mod metric_collection; 10 | pub mod parallel_fetcher; 11 | pub mod remote_cache; 12 | pub mod remote_tasks; 13 | pub mod remote_updates; 14 | pub mod report; 15 | pub mod resource_cache; 16 | pub mod task_utils; 17 | pub mod views; 18 | 19 | pub mod connection; 20 | pub mod pbs_client; 21 | pub mod sdn_client; 22 | 23 | #[cfg(any(remote_config = "faked", test))] 24 | pub mod test_support; 25 | 26 | use anyhow::Error; 27 | use serde_json::Value; 28 | 29 | pub(crate) async fn reload_api_certificate() -> Result<(), Error> { 30 | let proxy_pid = proxmox_rest_server::read_pid(pdm_buildcfg::PDM_API_PID_FN)?; 31 | let sock = proxmox_daemon::command_socket::path_from_pid(proxy_pid); 32 | let _: Value = 33 | proxmox_daemon::command_socket::send_raw(sock, "{\"command\":\"reload-certificate\"}\n") 34 | .await?; 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /cli/completions/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | 4 | # bash completion 5 | %.bc: 6 | @echo "creating bash completion definition for $*" 7 | @printf '# $* bash completion\n\n' >$@.tmp 8 | @printf "complete -C '$* bashcomplete' $*\n" >>$@.tmp 9 | mv $@.tmp $@ 10 | 11 | # ZSH completion 12 | _%: 13 | @echo "creating ZSH completion definition for $*" 14 | @printf '#compdef _$*() $*\n\n' >$@.tmp 15 | @printf 'function _$*() {\n' >>$@.tmp 16 | @printf ' local cwords line point cmd curr prev\n' >>$@.tmp 17 | @printf ' cwords=$${#words[@]}\n' >>$@.tmp 18 | @printf ' line=$$words\n' >>$@.tmp 19 | @printf ' point=$${#line}\n' >>$@.tmp 20 | @printf ' cmd=$${words[1]}\n' >>$@.tmp 21 | @printf ' curr=$${words[cwords]}\n' >>$@.tmp 22 | @printf ' prev=$${words[cwords-1]}\n' >>$@.tmp 23 | @printf ' compadd -- $$(COMP_CWORD="$$cwords" COMP_LINE="$$line" COMP_POINT="$$point" \\n' >>$@.tmp 24 | @printf ' $* bashcomplete "$$cmd" "$$curr" "$$prev")\n' >>$@.tmp 25 | @printf '}\n' >>$@.tmp 26 | mv $@.tmp $@ 27 | 28 | clean: 29 | rm -f _* *.bc 30 | -------------------------------------------------------------------------------- /lib/pdm-api-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pdm-api-types" 3 | version = "1.0.0" 4 | authors.workspace = true 5 | edition.workspace = true 6 | description = "general API type helpers for PDM" 7 | 8 | [dependencies] 9 | anyhow.workspace = true 10 | const_format.workspace = true 11 | http.workspace = true 12 | regex.workspace = true 13 | serde.workspace = true 14 | serde_plain.workspace = true 15 | 16 | proxmox-acme-api.workspace = true 17 | proxmox-access-control.workspace = true 18 | proxmox-auth-api = { workspace = true, features = ["api-types"] } 19 | proxmox-apt-api-types.workspace = true 20 | proxmox-lang.workspace = true 21 | proxmox-config-digest.workspace = true 22 | proxmox-schema = { workspace = true, features = ["api-macro"] } 23 | proxmox-section-config.workspace = true 24 | proxmox-dns-api.workspace = true 25 | proxmox-time.workspace = true 26 | proxmox-serde.workspace = true 27 | proxmox-subscription = { workspace = true, features = ["api-types"], default-features = false } 28 | 29 | pbs-api-types = { workspace = true } 30 | pve-api-types = { workspace = true } 31 | -------------------------------------------------------------------------------- /docs/epilog.rst: -------------------------------------------------------------------------------- 1 | .. Epilog (included at top of each file) 2 | 3 | We use this file to define external links and common replacement 4 | patterns. 5 | 6 | .. |WEBSITE| replace:: https://www.proxmox.com 7 | .. |DOWNLOADS| replace:: https://www.proxmox.com/downloads 8 | 9 | .. _Proxmox: https://www.proxmox.com 10 | .. _Proxmox Community Forum: https://forum.proxmox.com 11 | .. _Proxmox Virtual Environment: https://www.proxmox.com/proxmox-ve 12 | .. _Proxmox Datacenter Manager: https://www.proxmox.com/proxmox-datacenter-manager 13 | .. _Proxmox Backup: https://pbs.proxmox.com/wiki/index.php/Main_Page 14 | .. _PDM Development List: https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel 15 | .. _reStructuredText: https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html 16 | .. _Rust: https://www.rust-lang.org/ 17 | 18 | .. _AGPL3: https://www.gnu.org/licenses/agpl-3.0.en.html 19 | .. _Debian: https://www.debian.org 20 | .. _Debian Administrator's Handbook: https://debian-handbook.info/ 21 | 22 | .. _LVM: https://en.wikipedia.org/wiki/Logical_Volume_Manager_(Linux) 23 | .. _ZFS: https://en.wikipedia.org/wiki/ZFS 24 | -------------------------------------------------------------------------------- /cli/client/src/time.rs: -------------------------------------------------------------------------------- 1 | //! Time formatting functions. 2 | 3 | use std::ffi::CStr; 4 | use std::io; 5 | 6 | use anyhow::{bail, format_err, Error}; 7 | 8 | pub fn format_epoch(epoch: i64) -> Result { 9 | let mut ts: libc::tm = unsafe { std::mem::zeroed() }; 10 | let mut buf = [0u8; 64]; 11 | let epoch = epoch as libc::time_t; 12 | let len = unsafe { 13 | if libc::localtime_r(&epoch, &mut ts).is_null() { 14 | return Err(io::Error::last_os_error().into()); 15 | } 16 | 17 | libc::strftime( 18 | buf.as_mut_ptr() as *mut libc::c_char, 19 | buf.len(), 20 | c"%c".as_ptr(), 21 | &ts, 22 | ) 23 | }; 24 | 25 | if len == 0 || len > buf.len() { 26 | bail!("failed to format time"); 27 | } 28 | 29 | let text = CStr::from_bytes_with_nul(&buf[..(len + 1)]) 30 | .map_err(|_| format_err!("formatted time string is not valid"))?; 31 | 32 | Ok(text.to_string_lossy().to_string()) 33 | } 34 | 35 | pub fn format_epoch_lossy(epoch: i64) -> String { 36 | format_epoch(epoch).unwrap_or_else(|_| format!("{epoch}")) 37 | } 38 | -------------------------------------------------------------------------------- /server/src/resource_cache.rs: -------------------------------------------------------------------------------- 1 | use std::pin::pin; 2 | 3 | use anyhow::Error; 4 | 5 | use crate::task_utils; 6 | 7 | // This is the interval we update the cache independent of any API / UI activity, but depending on 8 | // the max-age from API calls the caches can get updated more frequently. 9 | const METRIC_POLL_INTERVALL: u64 = 15 * 60; // once every 15 minutes 10 | 11 | /// Start the resource caching. 12 | pub fn start_task() { 13 | tokio::spawn(async move { 14 | let task_scheduler = pin!(resource_caching_task()); 15 | let abort_future = pin!(proxmox_daemon::shutdown_future()); 16 | futures::future::select(task_scheduler, abort_future).await; 17 | }); 18 | } 19 | 20 | // FIXME: handle many remotes more intelligently? 21 | async fn resource_caching_task() -> Result<(), Error> { 22 | loop { 23 | if let Err(err) = 24 | crate::api::resources::get_resources_impl(METRIC_POLL_INTERVALL, None, None, None, None) 25 | .await 26 | { 27 | log::error!("could not update resource cache: {err}"); 28 | } 29 | 30 | let delay_target = task_utils::next_aligned_instant(METRIC_POLL_INTERVALL + 10); 31 | tokio::time::sleep_until(tokio::time::Instant::from_std(delay_target)).await; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/pdm-config/src/views.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use proxmox_product_config::{open_api_lockfile, replace_config, ApiLockGuard}; 4 | use proxmox_section_config::typed::{ApiSectionDataEntry, SectionConfigData}; 5 | 6 | use pdm_api_types::{views::ViewConfigEntry, ConfigDigest}; 7 | 8 | use pdm_buildcfg::configdir; 9 | 10 | const VIEW_CFG_FILENAME: &str = configdir!("/views.cfg"); 11 | const VIEW_FILTER_CFG_LOCKFILE: &str = configdir!("/.views.lock"); 12 | 13 | /// Get the `views.cfg` config file contents. 14 | pub fn config() -> Result<(SectionConfigData, ConfigDigest), Error> { 15 | let content = 16 | proxmox_sys::fs::file_read_optional_string(VIEW_CFG_FILENAME)?.unwrap_or_default(); 17 | 18 | let digest = openssl::sha::sha256(content.as_bytes()); 19 | 20 | let data = ViewConfigEntry::parse_section_config(VIEW_CFG_FILENAME, &content)?; 21 | Ok((data, digest.into())) 22 | } 23 | 24 | /// Get exclusive lock 25 | pub fn lock_config() -> Result { 26 | open_api_lockfile(VIEW_FILTER_CFG_LOCKFILE, None, true) 27 | } 28 | 29 | pub fn save_config(config: &SectionConfigData) -> Result<(), Error> { 30 | let raw = ViewConfigEntry::write_section_config(VIEW_CFG_FILENAME, config)?; 31 | replace_config(VIEW_CFG_FILENAME, raw.as_bytes())?; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /server/src/api/nodes/journal.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use serde_json::Value; 3 | 4 | use proxmox_router::{ApiMethod, Permission, Router, RpcEnvironment}; 5 | use proxmox_schema::api; 6 | 7 | use proxmox_syslog_api::{dump_journal, JournalFilter}; 8 | 9 | use pdm_api_types::{NODE_SCHEMA, PRIV_SYS_AUDIT}; 10 | 11 | #[api( 12 | protected: true, 13 | input: { 14 | properties: { 15 | node: { 16 | schema: NODE_SCHEMA, 17 | }, 18 | filter: { 19 | type: JournalFilter, 20 | flatten: true, 21 | }, 22 | }, 23 | }, 24 | returns: { 25 | type: Array, 26 | description: "Returns a list of journal entries.", 27 | items: { 28 | type: String, 29 | description: "Line text.", 30 | }, 31 | }, 32 | access: { 33 | permission: &Permission::Privilege(&["system", "log"], PRIV_SYS_AUDIT, false), 34 | }, 35 | )] 36 | /// Read syslog entries. 37 | #[allow(clippy::too_many_arguments)] 38 | fn get_journal( 39 | filter: JournalFilter, 40 | _param: Value, 41 | _info: &ApiMethod, 42 | _rpcenv: &mut dyn RpcEnvironment, 43 | ) -> Result, Error> { 44 | dump_journal(filter) 45 | } 46 | 47 | pub const ROUTER: Router = Router::new().get(&API_METHOD_GET_JOURNAL); 48 | -------------------------------------------------------------------------------- /docs/sysadmin.rst: -------------------------------------------------------------------------------- 1 | .. _sysadmin_host_administration: 2 | 3 | Host System Administration 4 | ========================== 5 | 6 | Proxmox Datacenter Manager is based on the famous Debian_ Linux distribution. This means that you 7 | have access to the entire range of Debian packages, and that the base system is well documented. The 8 | `Debian Administrator's Handbook`_ is available online, and provides a comprehensive introduction to 9 | the Debian operating system. 10 | 11 | A standard Proxmox Datacenter Manager installation uses the default repositories from Debian, so you get bug 12 | fixes and security updates through that channel. In addition, we provide our own package repository 13 | to roll out all Proxmox related packages. This includes updates to some Debian packages when 14 | necessary. 15 | 16 | We also deliver a specially optimized Linux kernel, based on the Ubuntu kernel. This kernel includes 17 | drivers for ZFS_. 18 | 19 | The following sections will explain things which are different on Proxmox Datacenter Manager, or 20 | tasks which are commonly used on Proxmox Datacenter Manager. For other topics, please refer to the 21 | standard Debian documentation. 22 | 23 | 24 | .. include:: local-zfs.rst 25 | 26 | .. include:: system-booting.rst 27 | 28 | .. include:: certificate-management.rst 29 | 30 | .. include:: services.rst 31 | 32 | .. include:: command-line-tools.rst 33 | -------------------------------------------------------------------------------- /ui/src/load_result.rs: -------------------------------------------------------------------------------- 1 | /// Helper wrapper to factor out some common api loading behavior 2 | pub struct LoadResult { 3 | pub data: Option, 4 | pub error: Option, 5 | } 6 | 7 | impl LoadResult { 8 | /// Creates a new empty result that contains no data or error. 9 | pub fn new() -> Self { 10 | Self { 11 | data: None, 12 | error: None, 13 | } 14 | } 15 | 16 | /// Update the current value with the given result 17 | /// 18 | /// On `Ok`, the previous error will be deleted. 19 | /// On `Err`, the previous valid date is kept. 20 | pub fn update(&mut self, result: Result) { 21 | match result { 22 | Ok(data) => { 23 | self.error = None; 24 | self.data = Some(data); 25 | } 26 | Err(err) => { 27 | self.error = Some(err); 28 | } 29 | } 30 | } 31 | 32 | /// If any of data or err has any value 33 | pub fn has_data(&self) -> bool { 34 | self.data.is_some() || self.error.is_some() 35 | } 36 | 37 | /// Clears both data and the error from the result. 38 | pub fn clear(&mut self) { 39 | self.data = None; 40 | self.error = None; 41 | } 42 | } 43 | 44 | impl Default for LoadResult { 45 | fn default() -> Self { 46 | Self::new() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/pdm-api-types/src/pbs.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use proxmox_schema::api; 4 | 5 | #[api] 6 | #[derive(Debug, Deserialize, Serialize, PartialEq)] 7 | #[serde(rename_all = "kebab-case")] 8 | /// Whether a task is still running. 9 | pub enum IsRunning { 10 | /// Task is running. 11 | Running, 12 | /// Task is not running. 13 | Stopped, 14 | } 15 | 16 | // TODO: The pbs code should expose this via pbs-api-types! 17 | #[api] 18 | /// Status if a task. 19 | #[derive(Debug, Deserialize, Serialize)] 20 | pub struct TaskStatus { 21 | /// Exit status, if available. 22 | pub exitstatus: Option, 23 | 24 | /// Task id. 25 | pub id: Option, 26 | 27 | /// Node the task is running on. 28 | pub node: String, 29 | 30 | /// The Unix PID 31 | pub pid: i64, 32 | 33 | /// The task start time (Epoch) 34 | pub pstart: i64, 35 | 36 | /// The task's start time. 37 | pub starttime: i64, 38 | 39 | pub status: IsRunning, 40 | 41 | /// The task type. 42 | #[serde(rename = "type")] 43 | pub ty: String, 44 | 45 | /// The task's UPID. 46 | pub upid: String, 47 | 48 | /// The authenticated entity who started the task. 49 | pub user: String, 50 | } 51 | 52 | impl TaskStatus { 53 | /// Checks if the task is currently running. 54 | pub fn is_running(&self) -> bool { 55 | self.status == IsRunning::Running 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /server/src/api/nodes/syslog.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use serde_json::Value; 3 | 4 | use proxmox_router::{ApiMethod, Permission, Router, RpcEnvironment}; 5 | use proxmox_schema::api; 6 | 7 | use proxmox_syslog_api::{dump_syslog, SyslogFilter, SyslogLine}; 8 | 9 | use pdm_api_types::{NODE_SCHEMA, PRIV_SYS_AUDIT}; 10 | 11 | #[api( 12 | protected: true, 13 | input: { 14 | properties: { 15 | node: { 16 | schema: NODE_SCHEMA, 17 | }, 18 | filter: { 19 | type: SyslogFilter, 20 | flatten: true, 21 | }, 22 | }, 23 | }, 24 | returns: { 25 | type: Array, 26 | description: "Returns a list of syslog entries.", 27 | items: { 28 | type: SyslogLine, 29 | } 30 | }, 31 | access: { 32 | permission: &Permission::Privilege(&["system", "log"], PRIV_SYS_AUDIT, false), 33 | }, 34 | )] 35 | /// Read syslog entries. 36 | fn get_syslog( 37 | filter: SyslogFilter, 38 | _info: &ApiMethod, 39 | rpcenv: &mut dyn RpcEnvironment, 40 | ) -> Result, Error> { 41 | //filter.service = filter.service.map(crate::api2::node::services::real_service_name); 42 | 43 | let (count, lines) = dump_syslog(filter)?; 44 | 45 | rpcenv["total"] = Value::from(count); 46 | 47 | Ok(lines) 48 | } 49 | 50 | pub const ROUTER: Router = Router::new().get(&API_METHOD_GET_SYSLOG); 51 | -------------------------------------------------------------------------------- /visualize_rrd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # deps: python3-matplotlib python3-pandas python3-requests 3 | # 4 | # Usage example: 5 | # ``` 6 | # export PDM_USERNAME="root@pam" 7 | # export PDM_PASSWORD="" 8 | # export PDM_URL="https://172.30.0.4:8443" 9 | # 10 | # ./visualize_rrd.py pve/remotes//qemu/100 cpu-current 11 | # ./visualize_rrd.py pbs/remotes//datastore/ disk-used 12 | # ``` 13 | 14 | import os 15 | import sys 16 | 17 | import pandas as pd 18 | import matplotlib.pyplot as plt 19 | import requests 20 | 21 | for env_var in ["PDM_URL", "PDM_USERNAME", "PDM_PASSWORD"]: 22 | if not os.environ.get(env_var): 23 | raise Exception(f"{env_var} not set") 24 | 25 | url = os.environ["PDM_URL"] 26 | user = os.environ["PDM_USERNAME"] 27 | password = os.environ["PDM_PASSWORD"] 28 | 29 | query = sys.argv[1] 30 | rows = sys.argv[2:] 31 | 32 | r = requests.post( 33 | f"{url}/api2/json/access/ticket", 34 | verify=False, 35 | data={ 36 | "username": user, 37 | "password": password 38 | } 39 | ) 40 | 41 | data = r.json()['data'] 42 | csrf = data['CSRFPreventionToken'] 43 | ticket = data['ticket'] 44 | 45 | r = requests.get( 46 | f"{url}/api2/json/{query}/rrddata", 47 | params={"cf": "AVERAGE", "timeframe": "hour"}, 48 | cookies={"PDMAuthCookie": ticket}, 49 | verify=False 50 | ) 51 | 52 | data = r.json() 53 | df = pd.DataFrame(data['data']) 54 | df.plot(x='time', y=rows) 55 | plt.show() 56 | -------------------------------------------------------------------------------- /debian/proxmox-datacenter-manager.install: -------------------------------------------------------------------------------- 1 | debian/pdm-enterprise.sources etc/apt/sources.list.d/ 2 | usr/lib/systemd/system/proxmox-datacenter-api.service 3 | usr/lib/systemd/system/proxmox-datacenter-manager-banner.service 4 | usr/lib/systemd/system/proxmox-datacenter-manager-daily-update.service 5 | usr/lib/systemd/system/proxmox-datacenter-manager-daily-update.timer 6 | usr/lib/systemd/system/proxmox-datacenter-privileged-api.service 7 | usr/libexec/proxmox/proxmox-datacenter-api 8 | usr/libexec/proxmox/proxmox-datacenter-manager-banner 9 | usr/libexec/proxmox/proxmox-datacenter-manager-daily-update 10 | usr/libexec/proxmox/proxmox-datacenter-privileged-api 11 | usr/sbin/pdmAtoB 12 | usr/sbin/proxmox-datacenter-manager-admin 13 | usr/share/bash-completion/completions/pdmAtoB.bc 14 | usr/share/bash-completion/completions/proxmox-datacenter-api.bc 15 | usr/share/bash-completion/completions/proxmox-datacenter-manager-admin.bc 16 | usr/share/bash-completion/completions/proxmox-datacenter-privileged-api.bc 17 | usr/share/man/man1/proxmox-datacenter-api.1 18 | usr/share/man/man1/proxmox-datacenter-manager-admin.1 19 | usr/share/man/man1/proxmox-datacenter-privileged-api.1 20 | usr/share/man/man5/remotes.cfg.5 21 | usr/share/man/man5/views.cfg.5 22 | usr/share/zsh/vendor-completions/_pdmAtoB 23 | usr/share/zsh/vendor-completions/_proxmox-datacenter-api 24 | usr/share/zsh/vendor-completions/_proxmox-datacenter-manager-admin 25 | usr/share/zsh/vendor-completions/_proxmox-datacenter-privileged-api 26 | -------------------------------------------------------------------------------- /lib/pdm-api-types/src/translation.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use proxmox_schema::api; 4 | 5 | /// All available languages in Proxmox. Taken from proxmox-i18n repository. 6 | /// pt_BR, zh_CN, and zh_TW use the same case in the translation files. 7 | // TODO: auto-generate from available translations 8 | #[api] 9 | #[derive(Serialize, Deserialize)] 10 | #[serde(rename_all = "lowercase")] 11 | pub enum Translation { 12 | /// Arabic 13 | Ar, 14 | /// Catalan 15 | Ca, 16 | /// Danish 17 | Da, 18 | /// German 19 | De, 20 | /// English 21 | En, 22 | /// Spanish 23 | Es, 24 | /// Euskera 25 | Eu, 26 | /// Persian (Farsi) 27 | Fa, 28 | /// French 29 | Fr, 30 | /// Galician 31 | Gl, 32 | /// Hebrew 33 | He, 34 | /// Hungarian 35 | Hu, 36 | /// Italian 37 | It, 38 | /// Japanese 39 | Ja, 40 | /// Korean 41 | Kr, 42 | /// Norwegian (Bokmal) 43 | Nb, 44 | /// Dutch 45 | Nl, 46 | /// Norwegian (Nynorsk) 47 | Nn, 48 | /// Polish 49 | Pl, 50 | /// Portuguese (Brazil) 51 | #[serde(rename = "pt_BR")] 52 | PtBr, 53 | /// Russian 54 | Ru, 55 | /// Slovenian 56 | Sl, 57 | /// Swedish 58 | Sv, 59 | /// Turkish 60 | Tr, 61 | /// Chinese (simplified) 62 | #[serde(rename = "zh_CN")] 63 | ZhCn, 64 | /// Chinese (traditional) 65 | #[serde(rename = "zh_TW")] 66 | ZhTw, 67 | } 68 | -------------------------------------------------------------------------------- /server/src/api/nodes/time.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use serde_json::Value; 3 | 4 | use proxmox_router::{Permission, Router}; 5 | use proxmox_schema::api; 6 | 7 | use pdm_api_types::{NODE_SCHEMA, PRIV_SYS_MODIFY, TIME_ZONE_SCHEMA}; 8 | use proxmox_time_api::ServerTimeInfo; 9 | 10 | #[api( 11 | input: { 12 | properties: { 13 | node: { 14 | schema: NODE_SCHEMA, 15 | }, 16 | }, 17 | }, 18 | returns: { 19 | type: ServerTimeInfo, 20 | }, 21 | access: { 22 | permission: &Permission::Anybody, 23 | }, 24 | )] 25 | /// Read server time and time zone settings. 26 | fn get_time(_param: Value) -> Result { 27 | proxmox_time_api::get_server_time_info() 28 | } 29 | 30 | #[api( 31 | protected: true, 32 | reload_timezone: true, 33 | input: { 34 | properties: { 35 | node: { 36 | schema: NODE_SCHEMA, 37 | }, 38 | timezone: { 39 | schema: TIME_ZONE_SCHEMA, 40 | }, 41 | }, 42 | }, 43 | access: { 44 | permission: &Permission::Privilege(&["system", "time"], PRIV_SYS_MODIFY, false), 45 | }, 46 | )] 47 | /// Set time zone 48 | fn set_timezone(timezone: String, _param: Value) -> Result<(), Error> { 49 | proxmox_time_api::set_timezone(timezone) 50 | } 51 | 52 | pub const ROUTER: Router = Router::new() 53 | .get(&API_METHOD_GET_TIME) 54 | .put(&API_METHOD_SET_TIMEZONE); 55 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Proxmox Backup documentation master file 2 | 3 | Welcome to the Proxmox Datacenter Manager documentation! 4 | ======================================================== 5 | | |pdm-copyright| 6 | | Version |version| -- |today| 7 | 8 | Permission is granted to copy, distribute and/or modify this document under the 9 | terms of the GNU Free Documentation License, Version 1.3 or any later version 10 | published by the Free Software Foundation; with no Invariant Sections, no 11 | Front-Cover Texts, and no Back-Cover Texts. A copy of the license is included 12 | in the section entitled "GNU Free Documentation License". 13 | 14 | 15 | .. only:: html 16 | 17 | A `PDF` version of the documentation is `also available here <./proxmox-datacenter-manager.pdf>`_ 18 | 19 | .. toctree:: 20 | :maxdepth: 3 21 | :caption: Table of Contents 22 | 23 | introduction.rst 24 | installation.rst 25 | web-ui.rst 26 | sdn-integration.rst 27 | remotes.rst 28 | views.rst 29 | access-control.rst 30 | sysadmin.rst 31 | faq.rst 32 | 33 | .. raw:: latex 34 | 35 | \appendix 36 | 37 | .. toctree:: 38 | :maxdepth: 2 39 | :caption: Appendix 40 | 41 | command-syntax.rst 42 | configuration-files.rst 43 | roadmap.rst 44 | markdown-primer.rst 45 | GFDL.rst 46 | 47 | .. only:: html and devbuild 48 | 49 | .. toctree:: 50 | :maxdepth: 2 51 | :hidden: 52 | :caption: Developer Appendix 53 | 54 | todos.rst 55 | 56 | 57 | .. # * :ref:`genindex` 58 | -------------------------------------------------------------------------------- /server/src/bin/proxmox-datacenter-api/tasks/remote_updates.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use server::{remote_updates, task_utils}; 4 | 5 | const REFRESH_TIME: u64 = 6 * 3600; 6 | 7 | /// Start the remote task fetching task 8 | pub fn start_task() -> Result<(), Error> { 9 | tokio::spawn(async move { 10 | let task_scheduler = std::pin::pin!(RemoteUpdateRefreshTask {}.run()); 11 | let abort_future = std::pin::pin!(proxmox_daemon::shutdown_future()); 12 | futures::future::select(task_scheduler, abort_future).await; 13 | }); 14 | 15 | Ok(()) 16 | } 17 | 18 | struct RemoteUpdateRefreshTask {} 19 | 20 | impl RemoteUpdateRefreshTask { 21 | async fn run(self) { 22 | loop { 23 | self.refresh().await; 24 | self.wait_for_refresh().await; 25 | } 26 | } 27 | 28 | async fn refresh(&self) { 29 | if let Err(err) = self.do_refresh().await { 30 | log::error!("could not refresh remote update cache: {err:#}"); 31 | } 32 | } 33 | 34 | async fn do_refresh(&self) -> Result<(), Error> { 35 | let (config, _digest) = tokio::task::spawn_blocking(pdm_config::remotes::config).await??; 36 | remote_updates::refresh_update_summary_cache(config.into_iter().map(|(_, r)| r).collect()) 37 | .await 38 | } 39 | 40 | async fn wait_for_refresh(&self) { 41 | let instant = task_utils::next_aligned_instant(REFRESH_TIME); 42 | tokio::time::sleep_until(instant.into()).await; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /server/src/context.rs: -------------------------------------------------------------------------------- 1 | //! Module to setup the API server's global runtime context. 2 | //! 3 | //! Make sure to call `init` *once* when starting up the API server. 4 | 5 | use anyhow::Error; 6 | 7 | use crate::connection; 8 | 9 | /// Dependency-inject production remote-config implementation and remote client factory 10 | #[allow(dead_code)] 11 | fn default_remote_setup() { 12 | pdm_config::remotes::init(Box::new(pdm_config::remotes::DefaultRemoteConfig)); 13 | connection::init(Box::new(connection::DefaultClientFactory)); 14 | } 15 | 16 | /// Dependency-inject concrete implementations needed at runtime. 17 | pub fn init() -> Result<(), Error> { 18 | #[cfg(remote_config = "faked")] 19 | { 20 | use anyhow::bail; 21 | 22 | use crate::test_support::fake_remote; 23 | 24 | match std::env::var("PDM_FAKED_REMOTE_CONFIG") { 25 | Ok(path) => { 26 | log::info!("using fake remotes from {path:?}"); 27 | let config = fake_remote::FakeRemoteConfig::from_json_config(&path)?; 28 | pdm_config::remotes::init(Box::new(config.clone())); 29 | connection::init(Box::new(fake_remote::FakeClientFactory { config })); 30 | } 31 | Err(_) => { 32 | bail!("compiled with remote_config = 'faked', but PDM_FAKED_REMOTE_CONFIG not set") 33 | } 34 | } 35 | } 36 | #[cfg(not(remote_config = "faked"))] 37 | { 38 | default_remote_setup(); 39 | } 40 | 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /lib/pdm-config/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{format_err, Error}; 2 | use nix::unistd::{Gid, Group, Uid, User}; 3 | pub use pdm_buildcfg::{BACKUP_GROUP_NAME, BACKUP_USER_NAME}; 4 | 5 | pub mod certificate_config; 6 | pub mod domains; 7 | pub mod node; 8 | pub mod remotes; 9 | pub mod setup; 10 | pub mod views; 11 | 12 | mod config_version_cache; 13 | pub use config_version_cache::ConfigVersionCache; 14 | 15 | /// Return User info for the main system user (``getpwnam_r(3)``) 16 | pub fn api_user() -> Result { 17 | if cfg!(test) { 18 | Ok(User::from_uid(Uid::current())?.expect("current user does not exist")) 19 | } else { 20 | User::from_name(BACKUP_USER_NAME)? 21 | .ok_or_else(|| format_err!("Unable to lookup '{}' user.", BACKUP_USER_NAME)) 22 | } 23 | } 24 | 25 | /// Return Group info for the main system group (``getgrnam(3)``) 26 | pub fn api_group() -> Result { 27 | if cfg!(test) { 28 | Ok(Group::from_gid(Gid::current())?.expect("current group does not exist")) 29 | } else { 30 | Group::from_name(BACKUP_GROUP_NAME)? 31 | .ok_or_else(|| format_err!("Unable to lookup '{}' group.", BACKUP_GROUP_NAME)) 32 | } 33 | } 34 | 35 | pub fn priv_user() -> Result { 36 | if cfg!(test) { 37 | Ok(User::from_uid(Uid::current())?.expect("current user does not exist")) 38 | } else { 39 | User::from_name("root")?.ok_or_else(|| format_err!("Unable to lookup superuser.")) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/src/api/nodes/rrddata.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use proxmox_rrd_api_types::{RrdMode, RrdTimeframe}; 3 | 4 | use proxmox_router::Router; 5 | use proxmox_schema::api; 6 | 7 | use pdm_api_types::rrddata::PdmNodeDatapoint; 8 | 9 | use crate::api::rrd_common::{self, DataPoint}; 10 | 11 | impl DataPoint for PdmNodeDatapoint { 12 | fn new(time: u64) -> Self { 13 | Self { 14 | time, 15 | ..Default::default() 16 | } 17 | } 18 | 19 | fn fields() -> &'static [&'static str] { 20 | &["metric-collection-total-time"] 21 | } 22 | 23 | fn set_field(&mut self, name: &str, value: f64) { 24 | if name == "metric-collection-total-time" { 25 | self.total_time = Some(value); 26 | } 27 | } 28 | } 29 | 30 | #[api( 31 | input: { 32 | properties: { 33 | timeframe: { 34 | type: RrdTimeframe, 35 | }, 36 | cf: { 37 | type: RrdMode, 38 | }, 39 | }, 40 | }, 41 | returns: { 42 | type: Array, 43 | description: "An array of RRD data points.", 44 | items: { 45 | type: PdmNodeDatapoint, 46 | } 47 | } 48 | )] 49 | /// Read RRD data for this PDM node. 50 | fn get_node_rrddata(timeframe: RrdTimeframe, cf: RrdMode) -> Result, Error> { 51 | let base = "nodes/localhost"; 52 | rrd_common::create_datapoints_from_rrd(base, timeframe, cf) 53 | } 54 | 55 | pub const ROUTER: Router = Router::new().get(&API_METHOD_GET_NODE_RRDDATA); 56 | -------------------------------------------------------------------------------- /server/src/api/metric_collection.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use proxmox_router::{Router, SubdirMap}; 4 | use proxmox_schema::api; 5 | use proxmox_sortable_macro::sortable; 6 | 7 | use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, MetricCollectionStatus}; 8 | 9 | use crate::metric_collection; 10 | 11 | pub const ROUTER: Router = Router::new().subdirs(SUBDIRS); 12 | 13 | #[sortable] 14 | const SUBDIRS: SubdirMap = &sorted!([ 15 | ( 16 | "trigger", 17 | &Router::new().post(&API_METHOD_TRIGGER_METRIC_COLLECTION) 18 | ), 19 | ( 20 | "status", 21 | &Router::new().get(&API_METHOD_GET_METRIC_COLLECTION_STATUS) 22 | ), 23 | ]); 24 | 25 | #[api( 26 | input: { 27 | properties: { 28 | remote: { 29 | schema: REMOTE_ID_SCHEMA, 30 | optional: true, 31 | }, 32 | }, 33 | }, 34 | )] 35 | /// Trigger metric collection for a provided remote or for all remotes if no remote is passed. 36 | pub async fn trigger_metric_collection(remote: Option) -> Result<(), Error> { 37 | crate::metric_collection::trigger_metric_collection(remote, false).await?; 38 | 39 | Ok(()) 40 | } 41 | 42 | #[api( 43 | returns: { 44 | type: Array, 45 | description: "A list of metric collection statuses.", 46 | items: { 47 | type: MetricCollectionStatus, 48 | } 49 | } 50 | )] 51 | /// Read metric collection status. 52 | fn get_metric_collection_status() -> Result, Error> { 53 | metric_collection::get_status() 54 | } 55 | -------------------------------------------------------------------------------- /ui/src/widget/view_selector.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use pwt::prelude::*; 4 | use pwt::state::Store; 5 | use pwt::widget::form::Combobox; 6 | use pwt_macros::{builder, widget}; 7 | 8 | use pdm_api_types::views::ViewConfig; 9 | 10 | #[widget(comp=ViewSelectorComp, @input)] 11 | #[derive(Clone, Properties, PartialEq)] 12 | #[builder] 13 | pub struct ViewSelector { 14 | store: Store, 15 | } 16 | 17 | impl ViewSelector { 18 | pub fn new(store: Store) -> Self { 19 | yew::props!(Self { store }) 20 | } 21 | } 22 | 23 | #[doc(hidden)] 24 | pub struct ViewSelectorComp {} 25 | 26 | impl Component for ViewSelectorComp { 27 | type Message = (); 28 | type Properties = ViewSelector; 29 | 30 | fn create(_ctx: &Context) -> Self { 31 | Self {} 32 | } 33 | 34 | fn view(&self, ctx: &Context) -> Html { 35 | let mut list = vec!["__dashboard__".into()]; 36 | let store = &ctx.props().store; 37 | for item in store.read().data().iter() { 38 | list.push(item.id.clone().into()); 39 | } 40 | Combobox::new() 41 | .items(Rc::new(list)) 42 | .with_input_props(&ctx.props().input_props) 43 | .on_change(|_| {}) 44 | .render_value({ 45 | move |value: &AttrValue| { 46 | if value == "__dashboard__" { 47 | html! {{tr!("Dashboard")}} 48 | } else { 49 | html! {{value}} 50 | } 51 | } 52 | }) 53 | .into() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server/src/auth/csrf.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::OnceLock; 3 | 4 | use anyhow::Error; 5 | 6 | use proxmox_sys::fs::file_get_contents; 7 | use proxmox_sys::fs::{replace_file, CreateOptions}; 8 | 9 | use proxmox_auth_api::HMACKey; 10 | 11 | use pdm_buildcfg::configdir; 12 | 13 | pub fn csrf_secret() -> &'static HMACKey { 14 | static SECRET: OnceLock = OnceLock::new(); 15 | 16 | SECRET.get_or_init(|| { 17 | let bytes = file_get_contents(configdir!("/auth/csrf.key")).unwrap(); 18 | std::str::from_utf8(&bytes) 19 | .map_err(anyhow::Error::new) 20 | .and_then(HMACKey::from_base64) 21 | // legacy fall back to load legacy csrf secrets 22 | // TODO: remove once we move away from legacy token verification 23 | .unwrap_or_else(|_| { 24 | let key_as_b64 = proxmox_base64::encode_no_pad(bytes); 25 | HMACKey::from_base64(&key_as_b64).unwrap() 26 | }) 27 | }) 28 | } 29 | 30 | pub fn generate_csrf_key() -> Result<(), Error> { 31 | let path = PathBuf::from(configdir!("/auth/csrf.key")); 32 | 33 | if path.exists() { 34 | return Ok(()); 35 | } 36 | 37 | let key = HMACKey::generate()?.to_base64()?; 38 | 39 | use nix::sys::stat::Mode; 40 | 41 | let api_user = pdm_config::api_user()?; 42 | 43 | replace_file( 44 | &path, 45 | key.as_bytes(), 46 | CreateOptions::new() 47 | .perm(Mode::from_bits_truncate(0o0640)) 48 | .owner(nix::unistd::ROOT) 49 | .group(api_user.gid), 50 | true, 51 | )?; 52 | 53 | Ok(()) 54 | } 55 | -------------------------------------------------------------------------------- /server/src/auth/certs.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{format_err, Error}; 2 | use std::path::PathBuf; 3 | 4 | use proxmox_dns_api::read_etc_resolv_conf; 5 | 6 | use pdm_buildcfg::configdir; 7 | 8 | pub const API_KEY_FN: &str = configdir!("/auth/api.key"); 9 | pub const API_CERT_FN: &str = configdir!("/auth/api.pem"); 10 | 11 | /// Update self signed node certificate. 12 | pub fn update_self_signed_cert(force: bool) -> Result<(), Error> { 13 | let key_path = PathBuf::from(API_KEY_FN); 14 | let cert_path = PathBuf::from(API_CERT_FN); 15 | 16 | if key_path.exists() && cert_path.exists() && !force { 17 | return Ok(()); 18 | } 19 | 20 | let resolv_conf = read_etc_resolv_conf(None)?.config; 21 | 22 | let (priv_key, cert) = proxmox_acme_api::create_self_signed_cert( 23 | "Proxmox Datacenter Manager", 24 | proxmox_sys::nodename(), 25 | resolv_conf.search.as_deref(), 26 | )?; 27 | 28 | let cert_pem = cert.to_pem()?; 29 | let priv_pem = priv_key.private_key_to_pem_pkcs8()?; 30 | 31 | set_api_certificate(&cert_pem, &priv_pem)?; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub(crate) fn set_api_certificate(cert_pem: &[u8], key_pem: &[u8]) -> Result<(), Error> { 37 | let key_path = PathBuf::from(API_KEY_FN); 38 | let cert_path = PathBuf::from(API_CERT_FN); 39 | 40 | proxmox_product_config::replace_privileged_config(key_path, key_pem) 41 | .map_err(|err| format_err!("error writing certificate private key - {}", err))?; 42 | proxmox_product_config::replace_privileged_config(cert_path, cert_pem) 43 | .map_err(|err| format_err!("error writing certificate file - {}", err))?; 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /server/src/test_support/temp.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use anyhow::Error; 4 | 5 | use proxmox_sys::fs::CreateOptions; 6 | 7 | /// Temporary file that be cleaned up when dropped. 8 | pub struct NamedTempFile { 9 | path: PathBuf, 10 | } 11 | 12 | impl NamedTempFile { 13 | /// Create a new temporary file. 14 | /// 15 | /// The file will be created with the passed [`CreateOptions`]. 16 | pub fn new(options: CreateOptions) -> Result { 17 | let base = std::env::temp_dir().join("test"); 18 | let (_, path) = proxmox_sys::fs::make_tmp_file(base, options)?; 19 | 20 | Ok(Self { path }) 21 | } 22 | 23 | /// Return the [`Path`] to the temporary file. 24 | pub fn path(&self) -> &Path { 25 | &self.path 26 | } 27 | } 28 | 29 | impl Drop for NamedTempFile { 30 | fn drop(&mut self) { 31 | let _ = std::fs::remove_file(&self.path); 32 | } 33 | } 34 | 35 | /// Temporary directory that is cleaned up when dropped. 36 | pub struct NamedTempDir { 37 | path: PathBuf, 38 | } 39 | 40 | impl NamedTempDir { 41 | /// Create a new temporary directory. 42 | /// 43 | /// The directory will be created with `0o700` permissions. 44 | pub fn new() -> Result { 45 | let path = proxmox_sys::fs::make_tmp_dir("/tmp", None)?; 46 | 47 | Ok(Self { path }) 48 | } 49 | 50 | /// Return the [`Path`] to the temporary directory. 51 | pub fn path(&self) -> &Path { 52 | &self.path 53 | } 54 | } 55 | 56 | impl Drop for NamedTempDir { 57 | fn drop(&mut self) { 58 | let _ = std::fs::remove_dir_all(&self.path); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/src/api/pve/storage.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap}; 4 | use proxmox_schema::api; 5 | use proxmox_sortable_macro::sortable; 6 | 7 | use pdm_api_types::remotes::REMOTE_ID_SCHEMA; 8 | use pdm_api_types::{NODE_SCHEMA, PRIV_RESOURCE_AUDIT, PVE_STORAGE_ID_SCHEMA}; 9 | 10 | use super::connect_to_remote; 11 | 12 | pub const ROUTER: Router = Router::new() 13 | .get(&list_subdirs_api_method!(STORAGE_SUBDIR)) 14 | .subdirs(STORAGE_SUBDIR); 15 | 16 | #[sortable] 17 | const STORAGE_SUBDIR: SubdirMap = &sorted!([ 18 | ("rrddata", &super::rrddata::STORAGE_RRD_ROUTER), 19 | ("status", &Router::new().get(&API_METHOD_GET_STATUS)), 20 | ]); 21 | 22 | #[api( 23 | input: { 24 | properties: { 25 | remote: { schema: REMOTE_ID_SCHEMA }, 26 | node: { schema: NODE_SCHEMA, }, 27 | storage: { schema: PVE_STORAGE_ID_SCHEMA, }, 28 | }, 29 | }, 30 | returns: { type: pve_api_types::StorageStatus }, 31 | access: { 32 | permission: &Permission::Privilege(&["resource", "{remote}", "storage", "{storage}"], PRIV_RESOURCE_AUDIT, false), 33 | }, 34 | )] 35 | /// Get the status of a qemu VM from a remote. If a node is provided, the VM must be on that 36 | /// node, otherwise the node is determined automatically. 37 | pub async fn get_status( 38 | remote: String, 39 | node: String, 40 | storage: String, 41 | ) -> Result { 42 | let (remotes, _) = pdm_config::remotes::config()?; 43 | 44 | let pve = connect_to_remote(&remotes, &remote)?; 45 | 46 | Ok(pve.storage_status(&node, &storage).await?) 47 | } 48 | -------------------------------------------------------------------------------- /ui/images/icon-memory.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /ui/src/dashboard/mod.rs: -------------------------------------------------------------------------------- 1 | use pwt::css; 2 | use pwt::prelude::*; 3 | use pwt::widget::{Column, Fa, Row}; 4 | 5 | mod top_entities; 6 | pub use top_entities::create_top_entities_panel; 7 | 8 | mod subscription_info; 9 | pub use subscription_info::create_subscription_panel; 10 | 11 | mod subscriptions_list; 12 | pub use subscriptions_list::SubscriptionsList; 13 | 14 | mod remote_panel; 15 | pub use remote_panel::create_remote_panel; 16 | 17 | mod guest_panel; 18 | pub use guest_panel::create_guest_panel; 19 | 20 | mod node_status_panel; 21 | use node_status_panel::create_node_panel; 22 | 23 | mod sdn_zone_panel; 24 | pub use sdn_zone_panel::create_sdn_panel; 25 | 26 | mod status_row; 27 | pub use status_row::DashboardStatusRow; 28 | 29 | mod filtered_tasks; 30 | 31 | mod pbs_datastores_panel; 32 | pub use pbs_datastores_panel::create_pbs_datastores_panel; 33 | 34 | mod tasks; 35 | pub use tasks::create_task_summary_panel; 36 | 37 | pub mod view; 38 | 39 | mod refresh_config_edit; 40 | pub use refresh_config_edit::create_refresh_config_edit_window; 41 | 42 | mod resource_tree; 43 | pub use resource_tree::create_resource_tree; 44 | 45 | fn loading_column() -> Column { 46 | Column::new() 47 | .padding(4) 48 | .class(css::FlexFit) 49 | .class(css::JustifyContent::Center) 50 | .class(css::AlignItems::Center) 51 | .with_child(html! {}) 52 | } 53 | 54 | /// Create a consistent title component for the given title and icon 55 | fn create_title_with_icon(icon: &str, title: String) -> Html { 56 | Row::new() 57 | .class(css::AlignItems::Center) 58 | .gap(2) 59 | .with_child(Fa::new(icon)) 60 | .with_child(title) 61 | .into() 62 | } 63 | -------------------------------------------------------------------------------- /ui/debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | DH_VERBOSE = 1 5 | 6 | include /usr/share/dpkg/pkg-info.mk 7 | 8 | export BUILD_MODE=release 9 | 10 | CARGO=/usr/share/cargo/bin/cargo 11 | 12 | export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS 13 | export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE 14 | export CARGO_HOME = $(CURDIR)/debian/cargo_home 15 | 16 | export DEB_CARGO_CRATE=proxmox-datacenter-manager-ui_$(DEB_VERSION_UPSTREAM) 17 | export DEB_CARGO_PACKAGE=proxmox-datacenter-manager-ui 18 | 19 | %: 20 | dh $@ 21 | 22 | override_dh_auto_configure: 23 | @perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \ 24 | die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml 25 | $(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system 26 | echo "\nlto=\"fat\"" >> debian/cargo_home/config.toml 27 | echo "\nopt-level=\"s\"" >> debian/cargo_home/config.toml 28 | echo "\ncodegen-units=1" >> debian/cargo_home/config.toml 29 | # patch cargo_home config to use lld with wasm, otherwise the build fails 30 | echo "\n[target.wasm32-unknown-unknown]" >> debian/cargo_home/config.toml 31 | cat debian/cargo_home/config.toml | sed "s/linker=[^']\+/linker=rust-lld/" | grep "^rustflags = " >> debian/cargo_home/config.toml 32 | dh_auto_configure 33 | 34 | override_dh_strip: 35 | dh_strip 36 | for exe in $$(find \ 37 | debian/*/usr \ 38 | -executable -type f); do \ 39 | debian/scripts/elf-strip-unused-dependencies.sh "$$exe" || true; \ 40 | done 41 | 42 | override_dh_missing: 43 | dh_missing --fail-missing 44 | -------------------------------------------------------------------------------- /server/src/api/nodes/mod.rs: -------------------------------------------------------------------------------- 1 | //! Server/Node Configuration and Administration 2 | 3 | use proxmox_router::{list_subdirs_api_method, Router, SubdirMap}; 4 | use proxmox_sortable_macro::sortable; 5 | 6 | pub mod apt; 7 | pub mod certificates; 8 | pub mod config; 9 | pub mod dns; 10 | pub mod journal; 11 | pub mod network; 12 | pub mod report; 13 | pub mod rrddata; 14 | pub mod sdn; 15 | pub mod status; 16 | pub mod subscription; 17 | pub mod syslog; 18 | pub mod tasks; 19 | pub mod termproxy; 20 | pub mod time; 21 | pub mod vncwebsocket; 22 | 23 | use anyhow::Error; 24 | use serde_json::{json, Value}; 25 | 26 | use proxmox_schema::api; 27 | 28 | #[api] 29 | /// List Nodes (only for compatibility) 30 | pub fn list_nodes() -> Result { 31 | Ok(json!([ { "node": proxmox_sys::nodename().to_string() } ])) 32 | } 33 | 34 | pub const ROUTER: Router = Router::new() 35 | .get(&API_METHOD_LIST_NODES) 36 | .match_all("node", &ITEM_ROUTER); 37 | 38 | pub const ITEM_ROUTER: Router = Router::new() 39 | .get(&list_subdirs_api_method!(SUBDIRS)) 40 | .subdirs(SUBDIRS); 41 | 42 | #[sortable] 43 | pub const SUBDIRS: SubdirMap = &sorted!([ 44 | ("apt", &apt::ROUTER), 45 | ("certificates", &certificates::ROUTER), 46 | ("config", &config::ROUTER), 47 | ("dns", &dns::ROUTER), 48 | ("journal", &journal::ROUTER), 49 | ("network", &network::ROUTER), 50 | ("report", &report::ROUTER), 51 | ("rrdata", &rrddata::ROUTER), 52 | ("sdn", &sdn::ROUTER), 53 | ("subscription", &subscription::ROUTER), 54 | ("status", &status::ROUTER), 55 | ("syslog", &syslog::ROUTER), 56 | ("tasks", &tasks::ROUTER), 57 | ("termproxy", &termproxy::ROUTER), 58 | ("time", &time::ROUTER), 59 | ("vncwebsocket", &vncwebsocket::ROUTER), 60 | ]); 61 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | DH_VERBOSE = 1 5 | 6 | include /usr/share/dpkg/pkg-info.mk 7 | include /usr/share/rustc/architecture.mk 8 | 9 | export BUILD_MODE=release 10 | 11 | CARGO=/usr/share/cargo/bin/cargo 12 | 13 | export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS 14 | export DEB_HOST_RUST_TYPE DEB_HOST_GNU_TYPE 15 | export CARGO_HOME = $(CURDIR)/debian/cargo_home 16 | 17 | export DEB_CARGO_CRATE=proxmox-datacenter-manager_$(DEB_VERSION_UPSTREAM) 18 | export DEB_CARGO_PACKAGE=proxmox-datacenter-manager 19 | 20 | export DEB_VERSION DEB_VERSION_UPSTREAM 21 | 22 | %: 23 | dh $@ 24 | 25 | override_dh_auto_configure: 26 | @perl -ne 'if (/^version\s*=\s*"(\d+(?:\.\d+)+)"/) { my $$v_cargo = $$1; my $$v_deb = "$(DEB_VERSION_UPSTREAM)"; \ 27 | die "ERROR: d/changelog <-> Cargo.toml version mismatch: $$v_cargo != $$v_deb\n" if $$v_cargo ne $$v_deb; exit(0); }' Cargo.toml 28 | $(CARGO) prepare-debian $(CURDIR)/debian/cargo_registry --link-from-system 29 | dh_auto_configure 30 | 31 | override_dh_strip: 32 | dh_strip 33 | for exe in $$(find \ 34 | debian/proxmox-datacenter-manager/usr \ 35 | debian/proxmox-datacenter-manager-client/usr \ 36 | -executable -type f); do \ 37 | debian/scripts/elf-strip-unused-dependencies.sh "$$exe" || true; \ 38 | done 39 | 40 | override_dh_installsystemd: 41 | dh_installsystemd -pproxmox-datacenter-manager proxmox-datacenter-manager-daily-update.timer 42 | # note: we start/try-reload-restart services manually in postinst 43 | dh_installsystemd --no-start --no-restart-after-upgrade --no-stop-on-upgrade 44 | 45 | override_dh_missing: 46 | dh_missing --fail-missing 47 | 48 | override_dh_compress: 49 | dh_compress -X.pdf 50 | -------------------------------------------------------------------------------- /docs/system-requirements.rst: -------------------------------------------------------------------------------- 1 | System Requirements 2 | ------------------- 3 | 4 | We recommend using high quality server hardware when running Proxmox Datacenter Manager in 5 | production. While no managed remote or resource depends on Proxmox Datacenter Manager to run, you 6 | might find that Proxmox Datacenter Manager will become a convenient and critical tool in your 7 | operations fast enough. 8 | 9 | .. _minimum_system_requirements: 10 | 11 | Minimum Server Requirements, for Evaluation 12 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 13 | 14 | These minimum requirements are for evaluation purposes only and should not be used in production. 15 | 16 | * CPU: 64bit (*x86-64* or *AMD64*), 1+ Cores 17 | 18 | * Memory (RAM): 1 GiB RAM 19 | 20 | * Hard drive: more than 10 GB of space. 21 | 22 | * Network card (NIC) 23 | 24 | 25 | Recommended Server System Requirements 26 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 27 | 28 | * CPU: Modern AMD or Intel 64-bit based CPU, with at least 2 cores 29 | 30 | * Memory: minimum 4 GiB for the OS. 31 | 32 | * OS storage: 33 | 34 | * 40 GB, or more, free storage space 35 | * Use a hardware RAID with battery protected write cache (*BBU*) or a redundant ZFS setup (ZFS is 36 | not compatible with a hardware RAID controller). 37 | 38 | * Redundant Multi-GBit/s network interface cards (NICs) 39 | 40 | 41 | 42 | Supported Web Browsers for Accessing the Web Interface 43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 44 | 45 | To access the server's web-based user interface, we recommend using one of the following browsers: 46 | 47 | * Firefox, a release from the current year, or the latest Extended Support Release 48 | * Chrome, a release from the current year 49 | * Microsoft's currently supported version of Edge 50 | * Safari, a release from the current year 51 | -------------------------------------------------------------------------------- /ui/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ NodeName }} - Proxmox Datacenter Manager 12 | 13 | 18 | 26 | 27 | 28 | 29 | 30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ui/images/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /debian/proxmox-datacenter-manager.postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | #DEBHELPER# 6 | 7 | case "$1" in 8 | configure) 9 | # modeled after dh_systemd_start output 10 | systemctl --system daemon-reload >/dev/null || true 11 | if [ -n "$2" ]; then 12 | _dh_action=try-reload-or-restart 13 | else 14 | _dh_action=start 15 | fi 16 | deb-systemd-invoke $_dh_action 'proxmox-datacenter-api.service' 'proxmox-datacenter-privileged-api.service' >/dev/null || true 17 | 18 | # For migrations or upgrade steps that can safely be automated. 19 | # NOTE: check if '/proxmox_install_mode' exist to avoid doing something if in the ISO installer environment. 20 | 21 | cfgdir="/etc/proxmox-datacenter-manager" 22 | 23 | if test -n "$2" && dpkg --compare-versions "$2" 'lt' '1.0~'; then 24 | if test -f "$cfgdir/ldap_passwords.json"; then 25 | echo "migrating legacy 'ldap_passwords.json' location to 'access/ldap-passwords.json'" 26 | mv "$cfgdir/ldap_passwords.json" "$cfgdir/access/ldap-passwords.json" || \ 27 | echo "moving '$cfgdir/ldap_passwords.json' to '$cfgdir/access/ldap-passwords.json' failed with code $?" 28 | fi 29 | fi 30 | 31 | BETA_SOURCES="/etc/apt/sources.list.d/pdm-test.sources" 32 | if test -f "$BETA_SOURCES" && dpkg --compare-versions "$2" 'lt' '1.0.1' && dpkg --compare-versions "$2" 'gt' '0.0~~'; then 33 | printf "\nNOTE: Remove the pdm-test repository, which was added during the beta phase.\nYou can (re-)add repositories on the web UI (Administration -> Repositories)\n\n" 34 | rm -v "$BETA_SOURCES" || true 35 | fi 36 | 37 | ;; 38 | 39 | abort-upgrade|abort-remove|abort-deconfigure) 40 | ;; 41 | 42 | *) 43 | echo "postinst called with unknown argument \`$1'" >&2 44 | exit 1 45 | ;; 46 | esac 47 | 48 | exit 0 49 | -------------------------------------------------------------------------------- /server/src/auth/key.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::sync::OnceLock; 3 | 4 | use anyhow::Error; 5 | 6 | use proxmox_auth_api::{PrivateKey, PublicKey}; 7 | use proxmox_sys::fs::file_get_contents; 8 | use proxmox_sys::fs::{replace_file, CreateOptions}; 9 | 10 | use pdm_buildcfg::configdir; 11 | 12 | pub fn public_auth_key() -> &'static PublicKey { 13 | static KEY: OnceLock = OnceLock::new(); 14 | KEY.get_or_init(|| { 15 | let pem = file_get_contents(configdir!("/auth/authkey.pub")).unwrap(); 16 | PublicKey::from_pem(&pem).unwrap() 17 | }) 18 | } 19 | 20 | pub fn private_auth_key() -> &'static PrivateKey { 21 | static KEY: OnceLock = OnceLock::new(); 22 | 23 | KEY.get_or_init(|| { 24 | let pem = file_get_contents(configdir!("/auth/authkey.key")).unwrap(); 25 | PrivateKey::from_pem(&pem).unwrap() 26 | }) 27 | } 28 | 29 | pub fn generate_auth_key() -> Result<(), Error> { 30 | let priv_path = PathBuf::from(configdir!("/auth/authkey.key")); 31 | 32 | let mut public_path = priv_path.clone(); 33 | public_path.set_extension("pub"); 34 | 35 | if priv_path.exists() && public_path.exists() { 36 | return Ok(()); 37 | } 38 | 39 | let key = proxmox_auth_api::PrivateKey::generate_ec()?; 40 | 41 | use nix::sys::stat::Mode; 42 | 43 | replace_file( 44 | &priv_path, 45 | &key.private_key_to_pem()?, 46 | CreateOptions::new().perm(Mode::from_bits_truncate(0o0600)), 47 | true, 48 | )?; 49 | 50 | let api_user = pdm_config::api_user()?; 51 | 52 | replace_file( 53 | &public_path, 54 | &key.public_key_to_pem()?, 55 | CreateOptions::new() 56 | .perm(Mode::from_bits_truncate(0o0640)) 57 | .owner(nix::unistd::ROOT) 58 | .group(api_user.gid), 59 | true, 60 | )?; 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /ui/src/dashboard/resource_tree.rs: -------------------------------------------------------------------------------- 1 | use pwt::css; 2 | use pwt::prelude::*; 3 | use pwt::props::{ContainerBuilder, WidgetBuilder, WidgetStyleBuilder}; 4 | use pwt::widget::form::Field; 5 | use pwt::widget::Column; 6 | use pwt::widget::Panel; 7 | use pwt::widget::Row; 8 | use pwt::widget::Toolbar; 9 | 10 | use crate::widget::RedrawController; 11 | use crate::widget::ResourceTree; 12 | 13 | #[derive(Properties, Clone, PartialEq)] 14 | struct ResourceTreeWithSearchProps { 15 | redraw_controller: RedrawController, 16 | } 17 | 18 | #[function_component] 19 | fn ResourceTreeWithSearch(props: &ResourceTreeWithSearchProps) -> Html { 20 | let search = use_state(String::new); 21 | 22 | Column::new() 23 | .class(css::FlexFit) 24 | .with_child( 25 | Toolbar::new() 26 | .with_child(tr!("Search")) 27 | .with_child(Field::new().on_change({ 28 | let search = search.clone(); 29 | move |value| search.set(value) 30 | })), 31 | ) 32 | .with_child( 33 | // use another flex layout with base width to work around the data tables dynamic 34 | // column size that does not decrease 35 | Row::new().class(css::FlexFit).with_child( 36 | ResourceTree::new() 37 | .redraw_controller(props.redraw_controller.clone()) 38 | .search_term(search.to_string()) 39 | .flex(1.0) 40 | .width(250) 41 | .height(500) 42 | .class(css::FlexFit), 43 | ), 44 | ) 45 | .into() 46 | } 47 | 48 | pub fn create_resource_tree(redraw_controller: RedrawController) -> Panel { 49 | Panel::new() 50 | .class(css::FlexFit) 51 | .title(tr!("Resources")) 52 | .with_child(html! {}) 53 | } 54 | -------------------------------------------------------------------------------- /server/src/api/pbs/node.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap}; 4 | use proxmox_schema::api; 5 | use proxmox_sortable_macro::sortable; 6 | 7 | use pdm_api_types::remotes::REMOTE_ID_SCHEMA; 8 | use pdm_api_types::PRIV_RESOURCE_AUDIT; 9 | 10 | use pbs_api_types::NODE_SCHEMA; 11 | 12 | use crate::connection; 13 | use crate::pbs_client::get_remote; 14 | 15 | pub const ROUTER: Router = Router::new() 16 | .get(&list_subdirs_api_method!(SUBDIRS)) 17 | .subdirs(SUBDIRS); 18 | 19 | #[sortable] 20 | const SUBDIRS: SubdirMap = &sorted!([ 21 | ("apt", &crate::api::remote_updates::APT_ROUTER), 22 | ( 23 | "subscription", 24 | &Router::new().get(&API_METHOD_GET_SUBSCRIPTION) 25 | ), 26 | ( 27 | "termproxy", 28 | &Router::new().post(&crate::api::remote_shell::API_METHOD_SHELL_TICKET) 29 | ), 30 | ( 31 | "vncwebsocket", 32 | &Router::new().upgrade(&crate::api::remote_shell::API_METHOD_SHELL_WEBSOCKET) 33 | ), 34 | ]); 35 | 36 | #[api( 37 | input: { 38 | properties: { 39 | remote: { schema: REMOTE_ID_SCHEMA }, 40 | node: { schema: NODE_SCHEMA }, // not used, always localhost 41 | }, 42 | }, 43 | access: { 44 | permission: &Permission::Privilege(&["resource", "{remote}"], PRIV_RESOURCE_AUDIT, false), 45 | description: "The user needs to have at least the `Resource.Audit` privilege on `/resource/{remote}`." 46 | }, 47 | returns: { type: proxmox_subscription::SubscriptionInfo } 48 | )] 49 | /// Get subscription for the PBS remote 50 | async fn get_subscription(remote: String) -> Result { 51 | let (remotes, _) = pdm_config::remotes::config()?; 52 | let remote = get_remote(&remotes, &remote)?; 53 | Ok(connection::make_pbs_client(remote)? 54 | .get_subscription() 55 | .await?) 56 | } 57 | -------------------------------------------------------------------------------- /cli/admin/src/support_status.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{json, Value}; 2 | 3 | use proxmox_router::cli::{ 4 | format_and_print_result_full, get_output_format, CliCommand, CliCommandMap, 5 | CommandLineInterface, 6 | }; 7 | use proxmox_router::{ApiHandler, RpcEnvironment}; 8 | use proxmox_schema::api; 9 | 10 | pub fn cli() -> CommandLineInterface { 11 | CliCommandMap::new() 12 | .insert("get", CliCommand::new(&API_METHOD_SUPPORT_STATUS)) 13 | .insert("update", CliCommand::new(&API_METHOD_UPDATE_SUPPORT_STATUS)) 14 | .into() 15 | } 16 | 17 | #[api] 18 | /// Get the support status information. 19 | async fn support_status( 20 | param: Value, 21 | rpcenv: &mut dyn RpcEnvironment, 22 | ) -> Result<(), anyhow::Error> { 23 | let info = &server::api::nodes::subscription::API_METHOD_GET_SUBSCRIPTION; 24 | 25 | let mut data = match info.handler { 26 | ApiHandler::Async(handler) => (handler)(json!({}), info, rpcenv).await?, 27 | _ => unreachable!(), 28 | }; 29 | 30 | let output_format = get_output_format(¶m); 31 | 32 | format_and_print_result_full( 33 | &mut data, 34 | &info.returns, 35 | &output_format, 36 | &Default::default(), 37 | ); 38 | Ok(()) 39 | } 40 | 41 | #[api] 42 | /// Update the support status information. 43 | async fn update_support_status( 44 | param: Value, 45 | rpcenv: &mut dyn RpcEnvironment, 46 | ) -> Result<(), anyhow::Error> { 47 | let info = &server::api::nodes::subscription::API_METHOD_CHECK_SUBSCRIPTION; 48 | 49 | let mut data = match info.handler { 50 | ApiHandler::Async(handler) => (handler)(json!({}), info, rpcenv).await?, 51 | _ => unreachable!(), 52 | }; 53 | 54 | let output_format = get_output_format(¶m); 55 | 56 | format_and_print_result_full( 57 | &mut data, 58 | &info.returns, 59 | &output_format, 60 | &Default::default(), 61 | ); 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /cli/client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proxmox-datacenter-manager-client" 3 | description = "Proxmox Datacenter Manager command line client" 4 | homepage = "https://www.proxmox.com" 5 | 6 | version.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | anyhow.workspace = true 13 | hex.workspace = true 14 | http.workspace = true 15 | libc.workspace = true 16 | log.workspace = true 17 | nix.workspace = true 18 | once_cell.workspace = true 19 | openssl.workspace = true 20 | serde.workspace = true 21 | serde_cbor.workspace = true 22 | serde_json.workspace = true 23 | serde_plain.workspace = true 24 | tokio = { workspace = true, features = [ "time" ] } 25 | webauthn-rs-core.workspace = true 26 | xdg.workspace = true 27 | 28 | pbs-api-types.workspace = true 29 | 30 | pdm-api-types.workspace = true 31 | pdm-buildcfg.workspace = true 32 | pdm-client.workspace = true 33 | pdm-ui-shared.workspace = true 34 | 35 | proxmox-access-control.workspace = true 36 | proxmox-async.workspace = true 37 | proxmox-base64.workspace = true 38 | proxmox-client = { workspace = true, features = [ "hyper-client", "webauthn" ] } 39 | proxmox-config-digest.workspace = true 40 | proxmox-fido2.workspace = true 41 | proxmox-human-byte.workspace = true 42 | proxmox-log.workspace = true 43 | proxmox-login.workspace = true 44 | proxmox-router = { workspace = true, features = [ "cli" ], default-features = false } 45 | proxmox-rrd-api-types.workspace = true 46 | proxmox-schema = { workspace = true, features = [ "api-macro" ] } 47 | proxmox-section-config.workspace = true 48 | proxmox-sys.workspace = true 49 | proxmox-time.workspace = true 50 | proxmox-tfa = { workspace = true, features = [ "types" ] } 51 | 52 | # for the pve API types: 53 | pve-api-types.workspace = true 54 | 55 | # when root wants to log into localhost, we just create a ticket directly 56 | proxmox-auth-api = { workspace = true, features = [ "api", "ticket" ] } 57 | -------------------------------------------------------------------------------- /lib/pdm-api-types/src/node_config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use proxmox_schema::{api, Updater}; 4 | 5 | use crate::{ 6 | Translation, EMAIL_SCHEMA, HTTP_PROXY_SCHEMA, OPENSSL_CIPHERS_TLS_1_2_SCHEMA, 7 | OPENSSL_CIPHERS_TLS_1_3_SCHEMA, 8 | }; 9 | 10 | #[api( 11 | properties: { 12 | "http-proxy": { 13 | schema: HTTP_PROXY_SCHEMA, 14 | optional: true, 15 | }, 16 | "email-from": { 17 | schema: EMAIL_SCHEMA, 18 | optional: true, 19 | }, 20 | "ciphers-tls-1.3": { 21 | schema: OPENSSL_CIPHERS_TLS_1_3_SCHEMA, 22 | optional: true, 23 | }, 24 | "ciphers-tls-1.2": { 25 | schema: OPENSSL_CIPHERS_TLS_1_2_SCHEMA, 26 | optional: true, 27 | }, 28 | "default-lang" : { 29 | schema: Translation::API_SCHEMA, 30 | optional: true, 31 | }, 32 | }, 33 | )] 34 | #[derive(Deserialize, Serialize, Updater)] 35 | #[serde(rename_all = "kebab-case")] 36 | /// Node specific configuration. 37 | pub struct NodeConfig { 38 | #[serde(skip_serializing_if = "Option::is_none")] 39 | pub http_proxy: Option, 40 | 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub email_from: Option, 43 | 44 | /// List of TLS ciphers for TLS 1.3 that will be used by the proxy. (Proxy has to be restarted for changes to take effect) 45 | #[serde(skip_serializing_if = "Option::is_none", rename = "ciphers-tls-1.3")] 46 | pub ciphers_tls_1_3: Option, 47 | 48 | /// List of TLS ciphers for TLS <= 1.2 that will be used by the proxy. (Proxy has to be restarted for changes to take effect) 49 | #[serde(skip_serializing_if = "Option::is_none", rename = "ciphers-tls-1.2")] 50 | pub ciphers_tls_1_2: Option, 51 | 52 | /// Default language used in the GUI 53 | #[serde(skip_serializing_if = "Option::is_none")] 54 | pub default_lang: Option, 55 | } 56 | -------------------------------------------------------------------------------- /ui/images/icon-cpu.svg: -------------------------------------------------------------------------------- 1 | 2 | 10 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /ui/src/sdn/evpn/mod.rs: -------------------------------------------------------------------------------- 1 | mod evpn_panel; 2 | pub use evpn_panel::EvpnPanel; 3 | 4 | mod remote_tree; 5 | pub use remote_tree::RemoteTree; 6 | 7 | mod vrf_tree; 8 | pub use vrf_tree::VrfTree; 9 | 10 | mod add_vnet; 11 | pub use add_vnet::AddVnetWindow; 12 | 13 | mod add_zone; 14 | pub use add_zone::AddZoneWindow; 15 | 16 | mod zone_status; 17 | pub use zone_status::ZoneStatusTable; 18 | 19 | mod vnet_status; 20 | pub use vnet_status::VnetStatusTable; 21 | 22 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] 23 | pub struct EvpnRouteTarget { 24 | asn: u32, 25 | vni: u32, 26 | } 27 | 28 | impl std::str::FromStr for EvpnRouteTarget { 29 | type Err = anyhow::Error; 30 | 31 | fn from_str(value: &str) -> Result { 32 | if let Some((asn, vni)) = value.split_once(':') { 33 | return Ok(Self { 34 | asn: asn.parse()?, 35 | vni: vni.parse()?, 36 | }); 37 | } 38 | 39 | anyhow::bail!("could not parse EVPN route target!") 40 | } 41 | } 42 | 43 | impl std::fmt::Display for EvpnRouteTarget { 44 | fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { 45 | write!(f, "{}:{}", self.asn, self.vni) 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone, PartialEq, Default)] 50 | #[repr(transparent)] 51 | pub struct NodeList(Vec); 52 | 53 | impl std::ops::Deref for NodeList { 54 | type Target = Vec; 55 | 56 | fn deref(&self) -> &Self::Target { 57 | &self.0 58 | } 59 | } 60 | 61 | impl std::str::FromStr for NodeList { 62 | type Err = anyhow::Error; 63 | 64 | fn from_str(value: &str) -> Result { 65 | if value.is_empty() { 66 | anyhow::bail!("node list cannot be an empty string"); 67 | } 68 | 69 | Ok(Self(value.split(",").map(String::from).collect())) 70 | } 71 | } 72 | 73 | impl FromIterator for NodeList { 74 | fn from_iter>(iter: I) -> Self { 75 | Self(iter.into_iter().collect()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/pdm-config/src/node.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use openssl::ssl::{SslAcceptor, SslMethod}; 3 | 4 | use proxmox_schema::ApiType; 5 | 6 | use proxmox_http::ProxyConfig; 7 | 8 | use pdm_api_types::ConfigDigest; 9 | 10 | use pdm_buildcfg::configdir; 11 | use proxmox_product_config::{open_api_lockfile, replace_config, ApiLockGuard}; 12 | 13 | use pdm_api_types::NodeConfig; 14 | 15 | const CONF_FILE: &str = configdir!("/node.cfg"); 16 | const LOCK_FILE: &str = configdir!("/.node.lck"); 17 | 18 | pub fn lock() -> Result { 19 | open_api_lockfile(LOCK_FILE, None, true) 20 | } 21 | 22 | /// Read the Node Config. 23 | pub fn config() -> Result<(NodeConfig, ConfigDigest), Error> { 24 | let content = proxmox_sys::fs::file_read_optional_string(CONF_FILE)?.unwrap_or_default(); 25 | 26 | let digest = openssl::sha::sha256(content.as_bytes()); 27 | let data: NodeConfig = proxmox_simple_config::from_str(&content, &NodeConfig::API_SCHEMA)?; 28 | 29 | Ok((data, digest.into())) 30 | } 31 | 32 | /// Write the Node Config, requires the write lock to be held. 33 | pub fn save_config(config: &NodeConfig) -> Result<(), Error> { 34 | validate_node_config(config)?; 35 | 36 | let raw = proxmox_simple_config::to_bytes(config, &NodeConfig::API_SCHEMA)?; 37 | replace_config(CONF_FILE, &raw) 38 | } 39 | 40 | /// Returns the parsed ProxyConfig 41 | pub fn get_http_proxy_config(config: &NodeConfig) -> Option { 42 | if let Some(http_proxy) = &config.http_proxy { 43 | ProxyConfig::parse_proxy_url(http_proxy).ok() 44 | } else { 45 | None 46 | } 47 | } 48 | 49 | // Validate the configuration. 50 | fn validate_node_config(config: &NodeConfig) -> Result<(), Error> { 51 | let mut dummy_acceptor = SslAcceptor::mozilla_intermediate_v5(SslMethod::tls()).unwrap(); 52 | if let Some(ciphers) = config.ciphers_tls_1_3.as_deref() { 53 | dummy_acceptor.set_ciphersuites(ciphers)?; 54 | } 55 | if let Some(ciphers) = config.ciphers_tls_1_2.as_deref() { 56 | dummy_acceptor.set_cipher_list(ciphers)?; 57 | } 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /server/src/bin/docgen.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Error}; 2 | 3 | use proxmox_schema::format::dump_enum_properties; 4 | use proxmox_schema::ApiType; 5 | use proxmox_section_config::{dump_section_config, typed::ApiSectionDataEntry}; 6 | 7 | use pdm_api_types::PRIVILEGES; 8 | 9 | use server::api; 10 | 11 | fn get_args() -> (String, Vec) { 12 | let mut args = std::env::args(); 13 | let prefix = args.next().unwrap(); 14 | let prefix = prefix.rsplit('/').next().unwrap().to_string(); // without path 15 | let args: Vec = args.collect(); 16 | 17 | (prefix, args) 18 | } 19 | 20 | fn main() -> Result<(), Error> { 21 | let (_prefix, args) = get_args(); 22 | 23 | if args.is_empty() { 24 | bail!("missing arguments"); 25 | } 26 | 27 | for arg in args.iter() { 28 | let text = match arg.as_ref() { 29 | "apidata.js" => generate_api_tree(), 30 | "domains.cfg" => dump_section_config(&pdm_config::domains::CONFIG), 31 | //TODO: needs pub changes in proxmox-access-control 32 | //"user.cfg" => dump_section_config(&proxmox_access_control::user::CONFIG) 33 | "remotes.cfg" => dump_section_config(pdm_api_types::remotes::Remote::section_config()), 34 | "views.cfg" => { 35 | dump_section_config(pdm_api_types::views::ViewConfigEntry::section_config()) 36 | } 37 | "config::acl::Role" => dump_enum_properties(&pdm_api_types::Role::API_SCHEMA)?, 38 | _ => bail!("docgen: got unknown type"), 39 | }; 40 | println!("{}", text); 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | fn generate_api_tree() -> String { 47 | let mut tree = Vec::new(); 48 | 49 | let mut data = proxmox_docgen::generate_api_tree(&api::ROUTER, ".", PRIVILEGES); 50 | data["path"] = "/".into(); 51 | // hack: add invisible space to sort as first entry 52 | data["text"] = "​Management API (HTTP)".into(); 53 | tree.push(data); 54 | 55 | format!( 56 | "var apiSchema = {};", 57 | serde_json::to_string_pretty(&tree).unwrap() 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /server/src/api/config/notes.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | 3 | use proxmox_config_digest::ConfigDigest; 4 | use proxmox_router::{Permission, Router, RpcEnvironment}; 5 | use proxmox_schema::api; 6 | 7 | use pdm_api_types::{PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}; 8 | use pdm_buildcfg::configdir; 9 | 10 | pub const ROUTER: Router = Router::new() 11 | .get(&API_METHOD_GET_NOTES) 12 | .put(&API_METHOD_UPDATE_NOTES); 13 | 14 | pub const NOTES_FILENAME: &str = configdir!("/notes.md"); 15 | 16 | fn read_notes() -> Result<(String, ConfigDigest), Error> { 17 | let notes = proxmox_sys::fs::file_read_optional_string(NOTES_FILENAME)?.unwrap_or_default(); 18 | let digest = openssl::sha::sha256(notes.as_bytes()); 19 | 20 | Ok((notes, digest.into())) 21 | } 22 | 23 | #[api( 24 | access: { 25 | permission: &Permission::Privilege(&["system"], PRIV_SYS_AUDIT, false), 26 | }, 27 | returns: { 28 | description: "Notes, utf8 encoded markdown file.", 29 | type: String, 30 | }, 31 | protected: true, 32 | )] 33 | /// Get notes. 34 | pub fn get_notes(rpcenv: &mut dyn RpcEnvironment) -> Result { 35 | let (notes, digest) = read_notes()?; 36 | rpcenv["digest"] = digest.to_hex().into(); 37 | Ok(notes) 38 | } 39 | 40 | #[api( 41 | input: { 42 | properties: { 43 | notes: { 44 | description: "New notes text.", 45 | type: String, 46 | }, 47 | digest: { 48 | type: ConfigDigest, 49 | optional: true, 50 | }, 51 | }, 52 | }, 53 | access: { 54 | // fixme: maybe we want something else here ... 55 | permission: &Permission::Privilege(&["system", "notes"], PRIV_SYS_MODIFY, false), 56 | } 57 | )] 58 | /// Update Notes 59 | pub fn update_notes(notes: String, digest: Option) -> Result<(), Error> { 60 | if digest.is_some() { 61 | let (_old, expected_digest) = read_notes()?; 62 | expected_digest.detect_modification(digest.as_ref())?; 63 | } 64 | proxmox_product_config::replace_config(NOTES_FILENAME, notes.as_bytes()) 65 | } 66 | -------------------------------------------------------------------------------- /server/src/bin/proxmox-datacenter-manager-banner.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{format_err, Error}; 2 | 3 | use std::fmt::Write; 4 | use std::fs; 5 | use std::net::ToSocketAddrs; 6 | use std::os::unix::prelude::OsStrExt; 7 | 8 | use nix::sys::utsname::uname; 9 | 10 | fn nodename() -> Result { 11 | let uname = uname().map_err(|err| format_err!("uname() failed - {err}"))?; // save on stack to avoid to_owned() allocation below 12 | std::str::from_utf8(uname.nodename().as_bytes())? 13 | .split('.') 14 | .next() 15 | .ok_or_else(|| format_err!("Failed to split FQDN to get hostname")) 16 | .map(|s| s.to_owned()) 17 | } 18 | 19 | fn main() { 20 | let nodename = match nodename() { 21 | Ok(value) => value, 22 | Err(err) => { 23 | eprintln!("Failed to retrieve hostname: {err}"); 24 | "INVALID".to_string() 25 | } 26 | }; 27 | 28 | let addr = format!("{nodename}:8443"); 29 | 30 | let mut banner = format!( 31 | " 32 | {:-<78} 33 | 34 | Welcome to the Proxmox Datacenter Manager. Please use your web browser to 35 | configure this server - connect to: 36 | 37 | ", 38 | "" 39 | ); 40 | 41 | let msg = match addr.to_socket_addrs() { 42 | Ok(saddrs) => { 43 | let saddrs: Vec<_> = saddrs 44 | .filter_map(|s| match !s.ip().is_loopback() { 45 | true => Some(format!(" https://{s}/")), 46 | false => None, 47 | }) 48 | .collect(); 49 | 50 | if !saddrs.is_empty() { 51 | saddrs.join("\n") 52 | } else { 53 | format!("hostname '{nodename}' does not resolve to any non-loopback address",) 54 | } 55 | } 56 | Err(e) => format!("could not resolve hostname '{nodename}': {e}"), 57 | }; 58 | banner += &msg; 59 | 60 | // unwrap will never fail for write!: 61 | // https://github.com/rust-lang/rust/blob/1.39.0/src/liballoc/string.rs#L2318-L2331 62 | write!(&mut banner, "\n\n{:-<78}\n\n", "").unwrap(); 63 | 64 | fs::write("/etc/issue", banner.as_bytes()).expect("Unable to write banner to issue file"); 65 | } 66 | -------------------------------------------------------------------------------- /ui/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pdm-ui" 3 | version = "1.0.1" 4 | edition = "2021" 5 | license = "AGPL-3" 6 | repository = "https://git.proxmox.com/?p=proxmox-datacenter-manager.git" 7 | 8 | [workspace] 9 | resolver = "2" 10 | 11 | [dependencies] 12 | anyhow = "1.0" 13 | futures = "0.3" 14 | gloo-net = "0.4" 15 | gloo-timers = "0.3" 16 | gloo-utils = "0.2" 17 | http = "1" 18 | js-sys = "0.3.69" 19 | log = "0.4.6" 20 | percent-encoding = "2.1" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_json = "1.0" 23 | wasm-bindgen = "0.2.92" 24 | wasm-bindgen-futures = "0.4" 25 | wasm-logger = "0.2" 26 | web-sys = { version = "0.3", features = ["Location", "DataTransfer"] } 27 | yew = { version = "0.21", features = ["csr"] } 28 | yew-router = { version = "0.18" } 29 | 30 | pwt = "0.7.1" 31 | pwt-macros = "0.5" 32 | 33 | proxmox-yew-comp = { version = "0.8.1", features = ["apt", "dns", "network", "rrd"] } 34 | 35 | proxmox-access-control = { version = "1.1", features = []} 36 | proxmox-acme-api = "1" 37 | proxmox-deb-version = "0.1" 38 | proxmox-client = "1" 39 | proxmox-human-byte = "1" 40 | proxmox-login = "1" 41 | proxmox-schema = "5" 42 | proxmox-subscription = { version = "1.0.1", features = ["api-types"], default-features = false } 43 | proxmox-rrd-api-types = "1" 44 | proxmox-node-status = "1" 45 | pbs-api-types = { version = "1.0.3", features = [ "enum-fallback" ] } 46 | 47 | pdm-api-types = { version = "1.0", path = "../lib/pdm-api-types" } 48 | pdm-client = { version = "1.0", path = "../lib/pdm-client" } 49 | 50 | pdm-ui-shared = { version = "1.0", path = "../lib/pdm-ui-shared" } 51 | 52 | pdm-search = { version = "0.2", path = "../lib/pdm-search" } 53 | 54 | [patch.crates-io] 55 | # proxmox-client = { path = "../../proxmox/proxmox-client" } 56 | # proxmox-human-byte = { path = "../../proxmox/proxmox-human-byte" } 57 | # proxmox-login = { path = "../../proxmox/proxmox-login" } 58 | # proxmox-rrd-api-types = { path = "../../proxmox/proxmox-rrd-api-types" } 59 | # proxmox-schema = { path = "../../proxmox/proxmox-schema" } 60 | # proxmox-yew-comp = { path = "../../proxmox-yew-comp" } 61 | # pwt = { path = "../../proxmox-yew-widget-toolkit" } 62 | # pwt-macros = { path = "../../proxmox-yew-widget-toolkit/pwt-macros" } 63 | -------------------------------------------------------------------------------- /docs/views.rst: -------------------------------------------------------------------------------- 1 | .. _views: 2 | 3 | Views 4 | ===== 5 | 6 | Views allow you to add an interactive view on a selected set of resources. 7 | 8 | Resource Selection 9 | ------------------ 10 | 11 | The resource selection is controlled by an include-exclude filter system. 12 | 13 | You define what resources to consider for including which then get passed through an exclude list to 14 | single specific types out again. 15 | 16 | This way you can, for example, easily configure to include all virtual machine resources, but then 17 | exclude any such VM that resides on a specific remote. 18 | 19 | Filter Types 20 | ^^^^^^^^^^^^ 21 | 22 | .. todo auto-generate below list 23 | 24 | The following lists of filter types are available to be used in include or exclude lists. 25 | 26 | - The `resource-type` filter allows you to filter by a specific resource type. 27 | The following types are available: 28 | 29 | - `datastore`: A Proxmox Backup Server datastore. 30 | - `lxc`: A LXC container. 31 | - `node`: A Proxmox VE or Proxmox Backup Server node. 32 | - `qemu`: A QEMU virtual machine. 33 | - `sdn-zone`: A SDN zone. 34 | - `storage`: A Proxmox VE storage 35 | 36 | - The `resource-pool` filter allows you to include or exclude only resources that are located in a 37 | specific resource pool-name. 38 | - The `tag` filter allows you to filter resources that are tagged with a specific tag-name. 39 | - The `remote` filter allows you to filter resources located on a specific remote. 40 | - The `resource-id` filter allows you to filter resources with a specific ID. 41 | 42 | 43 | Each filter can be prefixed with an optional `:` prefix. Currently there is only 44 | the `exact` matching behavior available. This behavior is the default if no prefix is provided. 45 | 46 | 47 | Customizable Dashboard 48 | ---------------------- 49 | 50 | You can create customizable dashboards for a views from a set of pre-defined widgets. 51 | Only resources matching your include minus the ones matching your exclude filters will be displayed 52 | in these widgets. 53 | 54 | 55 | Access Control 56 | -------------- 57 | 58 | You can grant permissions on specific views. With such a permission the user can operate on the 59 | view and all its selected resources. 60 | -------------------------------------------------------------------------------- /cli/client/src/metric_collection.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use pdm_api_types::remotes::REMOTE_ID_SCHEMA; 3 | use proxmox_router::cli::{ 4 | format_and_print_result, CliCommand, CliCommandMap, CommandLineInterface, OutputFormat, 5 | }; 6 | use proxmox_schema::api; 7 | 8 | use crate::{client, env}; 9 | 10 | pub fn cli() -> CommandLineInterface { 11 | CliCommandMap::new() 12 | .insert( 13 | "trigger", 14 | CliCommand::new(&API_METHOD_TRIGGER_METRIC_COLLECTION), 15 | ) 16 | .insert( 17 | "status", 18 | CliCommand::new(&API_METHOD_METRIC_COLLECTION_STATUS), 19 | ) 20 | .into() 21 | } 22 | 23 | #[api( 24 | input: { 25 | properties: { 26 | remote: { 27 | schema: REMOTE_ID_SCHEMA, 28 | optional: true, 29 | }, 30 | } 31 | } 32 | )] 33 | /// Trigger metric collection. If a remote is passed, only this remote will be collected, otherwise 34 | /// all. 35 | async fn trigger_metric_collection(remote: Option) -> Result<(), Error> { 36 | client()? 37 | .trigger_metric_collection(remote.as_deref()) 38 | .await?; 39 | Ok(()) 40 | } 41 | 42 | #[api] 43 | /// Show metric collection status. 44 | async fn metric_collection_status() -> Result<(), Error> { 45 | let result = client()?.get_metric_collection_status().await?; 46 | 47 | let output_format = env().format_args.output_format; 48 | if output_format == OutputFormat::Text { 49 | for remote_status in result { 50 | let timestamp = if let Some(last_collection) = remote_status.last_collection { 51 | proxmox_time::strftime_local("%a, %d %b %Y %T %z", last_collection)? 52 | } else { 53 | "never".into() 54 | }; 55 | 56 | let status = if let Some(err) = &remote_status.error { 57 | err 58 | } else { 59 | "ok" 60 | }; 61 | 62 | println!("{}: {status}", remote_status.remote); 63 | println!(" last successful: {timestamp}"); 64 | println!(); 65 | } 66 | } else { 67 | format_and_print_result(&result, &output_format.to_string()); 68 | } 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /server/src/api/nodes/dns.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use serde_json::Value; 3 | 4 | use proxmox_router::{ApiMethod, Permission, Router, RpcEnvironment}; 5 | use proxmox_schema::api; 6 | 7 | use pdm_api_types::{ConfigDigest, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY}; 8 | 9 | use proxmox_dns_api::{DeletableResolvConfProperty, ResolvConf, ResolvConfWithDigest}; 10 | 11 | #[api( 12 | protected: true, 13 | input: { 14 | description: "Update DNS settings.", 15 | properties: { 16 | node: { 17 | schema: NODE_SCHEMA, 18 | }, 19 | update: { 20 | type: ResolvConf, 21 | flatten: true, 22 | }, 23 | delete: { 24 | description: "List of properties to delete.", 25 | type: Array, 26 | optional: true, 27 | items: { 28 | type: DeletableResolvConfProperty, 29 | } 30 | }, 31 | digest: { 32 | type: ConfigDigest, 33 | optional: true, 34 | }, 35 | }, 36 | }, 37 | access: { 38 | permission: &Permission::Privilege(&["system", "network", "dns"], PRIV_SYS_MODIFY, false), 39 | } 40 | )] 41 | /// Update DNS settings 42 | pub fn update_dns( 43 | update: ResolvConf, 44 | delete: Option>, 45 | digest: Option, 46 | ) -> Result<(), Error> { 47 | proxmox_dns_api::update_dns(update, delete, digest) 48 | } 49 | 50 | #[api( 51 | input: { 52 | properties: { 53 | node: { 54 | schema: NODE_SCHEMA, 55 | }, 56 | }, 57 | }, 58 | returns: { 59 | type: ResolvConfWithDigest, 60 | }, 61 | access: { 62 | permission: &Permission::Privilege(&["system", "network", "dns"], PRIV_SYS_AUDIT, false), 63 | } 64 | )] 65 | /// Read DNS settings. 66 | pub fn get_dns( 67 | _param: Value, 68 | _info: &ApiMethod, 69 | _rpcenv: &mut dyn RpcEnvironment, 70 | ) -> Result { 71 | proxmox_dns_api::read_etc_resolv_conf(None) 72 | } 73 | 74 | pub const ROUTER: Router = Router::new() 75 | .get(&API_METHOD_GET_DNS) 76 | .put(&API_METHOD_UPDATE_DNS); 77 | -------------------------------------------------------------------------------- /ui/src/remotes/mod.rs: -------------------------------------------------------------------------------- 1 | mod wizard_page_connect; 2 | use wizard_page_connect::WizardPageConnect; 3 | 4 | mod wizard_page_nodes; 5 | use wizard_page_nodes::WizardPageNodes; 6 | 7 | mod wizard_page_summary; 8 | pub use wizard_page_summary::WizardPageSummary; 9 | 10 | mod wizard_page_info; 11 | pub use wizard_page_info::WizardPageInfo; 12 | 13 | mod add_wizard; 14 | pub use add_wizard::AddWizard; 15 | 16 | mod node_url_list; 17 | pub use node_url_list::NodeUrlList; 18 | 19 | mod edit_remote; 20 | 21 | mod config; 22 | pub use config::{create_remote, RemoteConfigPanel}; 23 | 24 | mod tasks; 25 | pub use tasks::RemoteTaskList; 26 | 27 | mod updates; 28 | pub use updates::UpdateTree; 29 | 30 | mod firewall; 31 | pub use firewall::FirewallTree; 32 | 33 | use yew::{function_component, Html}; 34 | 35 | use pwt::prelude::*; 36 | use pwt::{ 37 | props::StorageLocation, 38 | state::NavigationContainer, 39 | widget::{MiniScrollMode, TabBarItem, TabPanel}, 40 | }; 41 | 42 | #[function_component(RemotesPanel)] 43 | pub fn system_configuration() -> Html { 44 | let panel = TabPanel::new() 45 | .state_id(StorageLocation::session("RemotesPanelState")) 46 | .class(pwt::css::FlexFit) 47 | .router(true) 48 | .scroll_mode(MiniScrollMode::Arrow) 49 | .with_item_builder( 50 | TabBarItem::new() 51 | .key("configuration") 52 | .label(tr!("Configuration")) 53 | .icon_class("fa fa-cogs"), 54 | |_| RemoteConfigPanel::new().into(), 55 | ) 56 | .with_item_builder( 57 | TabBarItem::new() 58 | .key("tasks") 59 | .label(tr!("Tasks")) 60 | .icon_class("fa fa-book"), 61 | |_| RemoteTaskList::new().into(), 62 | ) 63 | .with_item_builder( 64 | TabBarItem::new() 65 | .key("updates") 66 | .label(tr!("Updates")) 67 | .icon_class("fa fa-refresh"), 68 | |_| UpdateTree::new().into(), 69 | ) 70 | .with_item_builder( 71 | TabBarItem::new() 72 | .key("firewall") 73 | .label(tr!("Firewall")) 74 | .icon_class("fa fa-shield"), 75 | |_| FirewallTree::new().into(), 76 | ); 77 | 78 | NavigationContainer::new().with_child(panel).into() 79 | } 80 | -------------------------------------------------------------------------------- /ui/src/configuration/permission_path_selector.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use yew::html::IntoPropValue; 4 | 5 | use pwt::prelude::*; 6 | use pwt::widget::form::Combobox; 7 | 8 | use pwt_macros::{builder, widget}; 9 | 10 | static PREDEFINED_PATHS: &[&str] = &[ 11 | "/", 12 | "/access", 13 | "/access/acl", 14 | "/access/users", 15 | "/resource", 16 | "/system", 17 | "/system/certificates", 18 | "/system/disks", 19 | "/system/log", 20 | "/system/network", 21 | "/system/network/dns", 22 | "/system/network/interfaces", 23 | "/system/notifications", 24 | "/system/services", 25 | "/system/status", 26 | "/system/tasks", 27 | "/system/time", 28 | "/view", 29 | ]; 30 | 31 | #[widget(comp=PdmPermissionPathSelector, @input, @element)] 32 | #[derive(Clone, PartialEq, Properties)] 33 | #[builder] 34 | pub struct PermissionPathSelector { 35 | /// Default value 36 | #[builder(IntoPropValue, into_prop_value)] 37 | #[prop_or_default] 38 | default: Option, 39 | } 40 | 41 | impl PermissionPathSelector { 42 | pub(super) fn new() -> Self { 43 | yew::props!(Self {}) 44 | } 45 | } 46 | 47 | enum Msg {} 48 | 49 | struct PdmPermissionPathSelector { 50 | items: Rc>, 51 | } 52 | 53 | impl PdmPermissionPathSelector {} 54 | 55 | impl Component for PdmPermissionPathSelector { 56 | type Message = Msg; 57 | type Properties = PermissionPathSelector; 58 | 59 | fn create(_ctx: &Context) -> Self { 60 | // TODO: fetch resources & remotes from the backend to improve the pre-defined selection of 61 | // acl paths 62 | Self { 63 | items: Rc::new( 64 | PREDEFINED_PATHS 65 | .iter() 66 | .map(|i| AttrValue::from(*i)) 67 | .collect(), 68 | ), 69 | } 70 | } 71 | 72 | fn update(&mut self, _ctx: &Context, _msg: Self::Message) -> bool { 73 | false 74 | } 75 | 76 | fn view(&self, ctx: &Context) -> Html { 77 | let props = ctx.props(); 78 | Combobox::new() 79 | .with_std_props(&props.std_props) 80 | .with_input_props(&props.input_props) 81 | .default(props.default.clone()) 82 | .items(Rc::clone(&self.items)) 83 | .editable(true) 84 | .into() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ui/src/pbs/datastore.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use yew::{ 4 | virtual_dom::{VComp, VNode}, 5 | Component, Properties, 6 | }; 7 | 8 | use pwt::{css::FlexFit, props::WidgetBuilder, tr, widget::TabBarItem}; 9 | 10 | use pbs_api_types::DataStoreConfig; 11 | 12 | use crate::pbs::SnapshotList; 13 | 14 | mod overview; 15 | use overview::DataStoreOverview; 16 | 17 | #[derive(Properties, PartialEq)] 18 | pub struct DatastorePanel { 19 | remote: String, 20 | config: DataStoreConfig, 21 | } 22 | 23 | impl DatastorePanel { 24 | pub fn new(remote: String, config: DataStoreConfig) -> Self { 25 | yew::props!(Self { remote, config }) 26 | } 27 | } 28 | 29 | impl From for VNode { 30 | fn from(val: DatastorePanel) -> Self { 31 | VComp::new::(Rc::new(val), None).into() 32 | } 33 | } 34 | 35 | #[doc(hidden)] 36 | struct DatastorePanelComp {} 37 | 38 | impl Component for DatastorePanelComp { 39 | type Message = (); 40 | type Properties = DatastorePanel; 41 | 42 | fn create(_ctx: &yew::Context) -> Self { 43 | Self {} 44 | } 45 | 46 | fn view(&self, ctx: &yew::Context) -> yew::Html { 47 | let props = ctx.props(); 48 | pwt::widget::TabPanel::new() 49 | .router(true) 50 | .class(FlexFit) 51 | .title(tr!("Datastore {0}", props.config.name)) 52 | .with_item_builder( 53 | TabBarItem::new() 54 | .key("overview") 55 | .label(tr!("Overview")) 56 | .icon_class("fa fa-tachometer"), 57 | { 58 | let remote = props.remote.clone(); 59 | let config = props.config.clone(); 60 | move |_| DataStoreOverview::new(remote.clone(), config.clone()).into() 61 | }, 62 | ) 63 | .with_item_builder( 64 | TabBarItem::new() 65 | .key("content") 66 | .label(tr!("Content")) 67 | .icon_class("fa fa-th"), 68 | { 69 | let remote = props.remote.clone(); 70 | let name = props.config.name.clone(); 71 | move |_| SnapshotList::new(remote.clone(), name.clone()).into() 72 | }, 73 | ) 74 | .into() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /ui/images/icon-sdn.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 12 | 13 | sdn 14 | 15 | 16 | a 17 | 18 | 19 | 20 | 21 | b 22 | 23 | 24 | 25 | 26 | a--b 27 | 28 | 29 | 30 | 31 | d 32 | 33 | 34 | 35 | 36 | a--d 37 | 38 | 39 | 40 | 41 | c 42 | 43 | 44 | 45 | 46 | a--c 47 | 48 | 49 | 50 | 51 | e 52 | 53 | 54 | 55 | 56 | a--e 57 | 58 | 59 | 60 | 61 | b--d 62 | 63 | 64 | 65 | 66 | c--d 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /ui/src/certificates.rs: -------------------------------------------------------------------------------- 1 | use pwt::prelude::*; 2 | use pwt::props::StorageLocation; 3 | use pwt::state::NavigationContainer; 4 | use pwt::widget::{Container, MiniScrollMode, TabBarItem, TabPanel}; 5 | 6 | use proxmox_yew_comp::acme::{ 7 | AcmeAccountsPanel, AcmeDomainsPanel, AcmePluginsPanel, CertificateList, 8 | }; 9 | 10 | #[function_component(CertificatesPanel)] 11 | pub fn certificates_panel() -> Html { 12 | let panel = TabPanel::new() 13 | .state_id(StorageLocation::session("CertificatesState")) 14 | .class(pwt::css::FlexFit) 15 | //.title("Certificates") 16 | .router(true) 17 | .scroll_mode(MiniScrollMode::Arrow) 18 | .with_item_builder( 19 | TabBarItem::new() 20 | .key("certificate_List") 21 | .label(tr!("Certificates")), 22 | |_| { 23 | Container::new() 24 | .class("pwt-content-spacer") 25 | .class(pwt::css::FlexFit) 26 | .with_child(CertificateList::new()) 27 | .into() 28 | }, 29 | ) 30 | .with_item_builder( 31 | TabBarItem::new() 32 | .key("acme_domains") 33 | // TRANSLATORS: ACME Stands for Automatic Certificate Management Environment 34 | .label(tr!("ACME Domains")), 35 | |_| { 36 | Container::new() 37 | .class("pwt-content-spacer") 38 | .class(pwt::css::FlexFit) 39 | .with_child(AcmeDomainsPanel::new().url("/config/certificate")) 40 | .into() 41 | }, 42 | ) 43 | .with_item_builder( 44 | TabBarItem::new() 45 | .key("acme_accounts") 46 | .label(tr!("ACME Accounts")), 47 | |_| { 48 | Container::new() 49 | .class("pwt-content-spacer") 50 | .class(pwt::css::FlexFit) 51 | .with_child(AcmeAccountsPanel::new()) 52 | .into() 53 | }, 54 | ) 55 | .with_item_builder( 56 | TabBarItem::new() 57 | .key("acme_plugins") 58 | .label(tr!("Challenge Plugins")), 59 | |_| { 60 | Container::new() 61 | .class("pwt-content-spacer") 62 | .class(pwt::css::FlexFit) 63 | .with_child(AcmePluginsPanel::new()) 64 | .into() 65 | }, 66 | ); 67 | 68 | NavigationContainer::new().with_child(panel).into() 69 | } 70 | -------------------------------------------------------------------------------- /docs/remotes.rst: -------------------------------------------------------------------------------- 1 | Remotes 2 | ======= 3 | 4 | Proxmox Datacenter Manager allows you to add arbitrary Proxmox VE nodes or clusters and Proxmox 5 | Backup Server instances as remotes. This allows for a structured, unified overview of every host, 6 | VM, container, and datastore across different locations. 7 | 8 | Resource Operation 9 | ------------------ 10 | 11 | Through the Proxmox Datacenter Manager, administrators can manage the lifecycle of virtual workloads 12 | at scale. Supported operations include starting, stopping, and rebooting guests across the inventory 13 | without the need to log in to individual nodes. 14 | 15 | Additionally, the platform supports live migration of guests. This capability extends to migrations 16 | between independent clusters, facilitating load balancing and planned maintenance while maintaining 17 | high availability. 18 | 19 | Data Collection 20 | --------------- 21 | 22 | Collecting data like RRD metrics, worker task status, logs, and other operational information is a 23 | primary function of Proxmox Datacenter Manager. The system aggregates metrics to provide insight 24 | into usage, performance, and infrastructure growth. 25 | 26 | This allows for introspection into the server fleet, providing a central overview but also allowing 27 | you to explore specific remotes or resources. Dashboards and RRD graphs visualize this data to 28 | assist in detecting trends, optimizing resource allocation, and planning future capacity. 29 | 30 | Proxmox VE Remote 31 | ----------------- 32 | 33 | Proxmox VE remotes integrate virtualization clusters and independent nodes into the central 34 | management view. Once added, the interface displays the hierarchy of hosts, virtual machines, 35 | containers, and storage resources, searchable via the central interface. 36 | 37 | Specific management capabilities available for Proxmox VE remotes include: 38 | 39 | * **Update Management**: A centralized panel provides an overview of available updates across the 40 | infrastructure and allows for the rollout of patches directly from the Datacenter Manager 41 | interface. 42 | * **SDN Capabilities**: Administrators can configure EVPN zones and VNets across multiple remotes to 43 | manage network overlays and administrative tasks. 44 | 45 | Proxmox Backup Server Remote 46 | ---------------------------- 47 | 48 | Proxmox Backup Server instances can be managed as remotes to oversee backup infrastructure alongside 49 | virtualization hosts. The interface provides a consolidated overview of different datastores, 50 | displaying content and storage utilization. 51 | 52 | Metrics from Proxmox Backup Server remotes are integrated directly into the central dashboard 53 | widgets, including RRD graphs for performance and usage monitoring. 54 | -------------------------------------------------------------------------------- /docs/api-viewer/Makefile: -------------------------------------------------------------------------------- 1 | DOCDIR = /usr/share/doc/proxmox-datacenter-manager 2 | 3 | API_VIEWER_SOURCES= \ 4 | index.html \ 5 | apidoc.css \ 6 | apidoc-dark.css \ 7 | apidoc.js \ 8 | 9 | API_VIEWER_JS_FILES := \ 10 | apidata.js \ 11 | /usr/share/javascript/extjs/ext-all.js \ 12 | /usr/share/javascript/proxmox-widget-toolkit-dev/APIViewer.js 13 | 14 | API_VIEWER_CSS_FILES := \ 15 | /usr/share/javascript/extjs/theme-crisp/resources/theme-crisp-all_1.css \ 16 | /usr/share/javascript/extjs/theme-crisp/resources/theme-crisp-all_2.css \ 17 | /usr/share/javascript/proxmox-widget-toolkit/css/ext6-pmx.css \ 18 | 19 | API_VIEWER_DARK_CSS_FILES := /usr/share/javascript/proxmox-widget-toolkit/themes/theme-proxmox-dark.css 20 | 21 | IMAGES := \ 22 | dd/drop-no.png \ 23 | grid/col-move-bottom.png \ 24 | grid/col-move-top.png \ 25 | grid/columns.png \ 26 | grid/group-collapse.png \ 27 | grid/group-expand.png \ 28 | grid/hd-pop.png \ 29 | grid/hmenu-asc.png \ 30 | grid/hmenu-desc.png \ 31 | grid/sort_asc.png \ 32 | grid/sort_desc.png \ 33 | menu/default-checked.png \ 34 | menu/default-menu-parent.png \ 35 | menu/default-unchecked.png \ 36 | tools/tool-sprites.png \ 37 | tree/elbow-end-minus.png \ 38 | tree/elbow-end-plus.png \ 39 | tree/elbow-end.png \ 40 | tree/elbow-line.png \ 41 | tree/elbow-minus.png \ 42 | tree/elbow-plus.png \ 43 | tree/elbow.png \ 44 | tree/folder-open.png \ 45 | tree/folder.png \ 46 | tree/leaf.png \ 47 | 48 | IMAGES_DIR := grid tools tree dd menu col 49 | 50 | API_VIEWER_IMAGES_FILES := $(addprefix /usr/share/javascript/extjs/theme-crisp/resources/images/, $(IMAGES)) 51 | 52 | ifeq ($(BUILD_MODE), release) 53 | COMPILEDIR := ../../target/release 54 | else 55 | COMPILEDIR := ../../target/debug 56 | endif 57 | 58 | all: 59 | 60 | .PHONY: install 61 | install: $(API_VIEWER_SOURCES) 62 | install -dm 0755 $(DESTDIR)$(DOCDIR)/html/api-viewer 63 | install -m 0644 $(API_VIEWER_SOURCES) $(DESTDIR)$(DOCDIR)/html/api-viewer 64 | install -dm 0755 $(addprefix $(DESTDIR)$(DOCDIR)/html/api-viewer/images/, $(IMAGES_DIR)) 65 | install -m 0644 $(API_VIEWER_IMAGES_FILES) $(DESTDIR)$(DOCDIR)/html/api-viewer/images/ 66 | cd $(DESTDIR)$(DOCDIR)/html/api-viewer/images; for f in $(IMAGES); do \ 67 | mv $$(basename $$f) $$f; \ 68 | done 69 | 70 | apidata.js: $(COMPILEDIR)/docgen 71 | $(COMPILEDIR)/docgen apidata.js >$@ 72 | 73 | apidoc.js: $(API_VIEWER_JS_FILES) 74 | cat $(API_VIEWER_JS_FILES) >$@.tmp 75 | mv $@.tmp $@ 76 | 77 | apidoc.css: $(API_VIEWER_CSS_FILES) 78 | cat $(API_VIEWER_CSS_FILES) >$@.tmp 79 | mv $@.tmp $@ 80 | 81 | apidoc-dark.css: $(API_VIEWER_DARK_CSS_FILES) 82 | cat $(API_VIEWER_DARK_CSS_FILES) >$@.tmp 83 | mv $@.tmp $@ 84 | 85 | .PHONY: clean 86 | clean: 87 | rm -f apidata.js apidoc.js apidoc.css apidoc-dark.css *.tmp 88 | -------------------------------------------------------------------------------- /ui/src/pve/remote/mod.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use proxmox_yew_comp::NotesView; 4 | use yew::virtual_dom::{VComp, VNode}; 5 | 6 | use pwt::css::{AlignItems, ColorScheme}; 7 | use pwt::prelude::*; 8 | use pwt::props::{ContainerBuilder, WidgetBuilder}; 9 | use pwt::widget::{Fa, Row, TabBarItem, TabPanel}; 10 | 11 | use crate::remotes::RemoteTaskList; 12 | 13 | #[derive(Clone, Debug, Eq, PartialEq, Properties)] 14 | pub struct PveRemotePanel { 15 | /// The remote to show 16 | pub remote: String, 17 | } 18 | 19 | impl PveRemotePanel { 20 | pub fn new(remote: String) -> Self { 21 | yew::props!(Self { remote }) 22 | } 23 | } 24 | 25 | impl From for VNode { 26 | fn from(val: PveRemotePanel) -> Self { 27 | VComp::new::(Rc::new(val), None).into() 28 | } 29 | } 30 | 31 | struct PveRemotePanelComp; 32 | 33 | impl yew::Component for PveRemotePanelComp { 34 | type Message = (); 35 | type Properties = PveRemotePanel; 36 | 37 | fn create(_ctx: &yew::Context) -> Self { 38 | Self 39 | } 40 | 41 | fn view(&self, ctx: &yew::Context) -> yew::Html { 42 | let props = ctx.props(); 43 | 44 | let title: Html = Row::new() 45 | .gap(2) 46 | .class(AlignItems::Baseline) 47 | .with_child(Fa::new("building")) 48 | .with_child(tr! {"Remote '{0}'", props.remote}) 49 | .into(); 50 | 51 | TabPanel::new() 52 | .router(true) 53 | .class(pwt::css::FlexFit) 54 | .title(title) 55 | .class(ColorScheme::Neutral) 56 | .with_item_builder( 57 | TabBarItem::new() 58 | .key("tasks_view") 59 | .label(tr!("Remote Tasks")) 60 | .icon_class("fa fa-list"), 61 | { 62 | let remote = props.remote.clone(); 63 | move |_| RemoteTaskList::new().remote(remote.clone()).into() 64 | }, 65 | ) 66 | .with_item_builder( 67 | TabBarItem::new() 68 | .key("notes_view") 69 | .label(tr!("Notes")) 70 | .icon_class("fa fa-sticky-note-o"), 71 | { 72 | let remote = props.remote.clone(); 73 | move |_| { 74 | NotesView::edit_property( 75 | format!("/pve/remotes/{remote}/options"), 76 | "description", 77 | ) 78 | .on_submit(None) 79 | .into() 80 | } 81 | }, 82 | ) 83 | .into() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | include ../defines.mk 2 | 3 | GENERATED_SYNOPSIS := \ 4 | proxmox-datacenter-manager-admin/synopsis.rst \ 5 | proxmox-datacenter-manager-client/synopsis.rst \ 6 | config/remotes/config.rst \ 7 | config/views/config.rst \ 8 | 9 | MAN1_PAGES := \ 10 | proxmox-datacenter-manager-admin.1 \ 11 | proxmox-datacenter-manager-client.1 \ 12 | proxmox-datacenter-api.1 \ 13 | proxmox-datacenter-privileged-api.1 \ 14 | 15 | MAN5_PAGES := \ 16 | remotes.cfg.5 \ 17 | views.cfg.5 \ 18 | 19 | # Sphinx documentation setup 20 | SPHINXOPTS = 21 | SPHINXBUILD = sphinx-build 22 | BUILDDIR = output 23 | 24 | ifeq ($(BUILD_MODE), release) 25 | COMPILEDIR := ../target/release 26 | SPHINXOPTS += -t release 27 | else 28 | COMPILEDIR := ../target/debug 29 | SPHINXOPTS += -t devbuild 30 | endif 31 | 32 | # Sphinx internal variables. 33 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(SPHINXOPTS) . 34 | 35 | all: $(MAN1_PAGES) $(MAN5_PAGES) 36 | 37 | config/%/config.rst: $(COMPILEDIR)/docgen 38 | $(COMPILEDIR)/docgen $*.cfg >$@ 39 | 40 | %/synopsis.rst: $(COMPILEDIR)/% 41 | $< printdoc > $@ 42 | 43 | $(MAN1_PAGES) $(MAN5_PAGES): man-pages 44 | 45 | .PHONY: man-pages 46 | man-pages: $(GENERATED_SYNOPSIS) 47 | $(SPHINXBUILD) $(SPHINXOPTS) -b man ./ $(BUILDDIR)/man 48 | 49 | .PHONY: html 50 | html: $(GENERATED_SYNOPSIS) images/proxmox-logo.svg _static/custom.css _static/custom.css conf.py 51 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 52 | install -m 0644 _static/custom.js _static/custom.css images/proxmox-logo.svg $(BUILDDIR)/html/_static/ 53 | install -dm 0755 $(BUILDDIR)/html/api-viewer 54 | $(MAKE) -C api-viewer DESTDIR=../ DOCDIR=$(BUILDDIR) install 55 | @echo 56 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 57 | 58 | .PHONY: latexpdf 59 | latexpdf: $(GENERATED_SYNOPSIS) 60 | @echo "Requires python3-sphinx, texlive-xetex, xindy and texlive-fonts-extra" 61 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 62 | @echo "Running LaTeX files through xelatex..." 63 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 64 | @echo "xelatex finished; the PDF files are in $(BUILDDIR)/latex." 65 | 66 | clean: 67 | $(MAKE) -C api-viewer clean 68 | rm -r -f *~ *.1 $(BUILDDIR) $(GENERATED_SYNOPSIS) 69 | 70 | install_manual_pages: man-pages 71 | install -dm755 $(DESTDIR)$(MAN1DIR) 72 | for i in $(MAN1_PAGES); do install -m755 $(BUILDDIR)/man/$$i $(DESTDIR)$(MAN1DIR)/ ; done 73 | install -dm755 $(DESTDIR)$(MAN5DIR) 74 | for i in $(MAN5_PAGES); do install -m755 $(BUILDDIR)/man/$$i $(DESTDIR)$(MAN5DIR)/ ; done 75 | 76 | install_html: html 77 | install -dm755 $(DESTDIR)$(DOCDIR) 78 | rsync -a $(BUILDDIR)/html $(DESTDIR)$(DOCDIR) 79 | 80 | install_pdf: latexpdf 81 | install -dm755 $(DESTDIR)$(DOCDIR) 82 | install -m 0644 output/latex/ProxmoxDatacenterManager.pdf $(DESTDIR)$(DOCDIR)/proxmox-datacenter-manager.pdf 83 | 84 | ifneq ($(filter nodoc,$(DEB_BUILD_PROFILES)),) 85 | 86 | install: install_manual_pages 87 | 88 | else 89 | 90 | install: install_manual_pages install_html install_pdf 91 | 92 | endif 93 | -------------------------------------------------------------------------------- /lib/pdm-config/src/setup.rs: -------------------------------------------------------------------------------- 1 | //! Setup methods. 2 | 3 | //use std::os::fd::OwnedFd; 4 | use std::os::unix::io::{AsRawFd, FromRawFd, OwnedFd}; 5 | 6 | use anyhow::{bail, format_err, Context as _, Error}; 7 | use nix::fcntl::OFlag; 8 | use nix::sys::stat::Mode; 9 | use nix::unistd::{Gid, Uid}; 10 | 11 | use pdm_buildcfg::configdir; 12 | use proxmox_sys::fs::CreateOptions; 13 | 14 | pub fn create_configdir() -> Result<(), Error> { 15 | let api_user = crate::api_user()?; 16 | 17 | let cfgdir = pdm_buildcfg::CONFIGDIR; 18 | mkdir_perms(cfgdir, api_user.uid, api_user.gid, 0o1770)?; 19 | mkdir_perms(configdir!("/auth"), nix::unistd::ROOT, api_user.gid, 0o750)?; 20 | mkdir_perms( 21 | configdir!("/access"), 22 | nix::unistd::ROOT, 23 | api_user.gid, 24 | 0o750, 25 | )?; 26 | 27 | Ok(()) 28 | } 29 | 30 | pub fn mkdir_perms(dir: &str, uid: Uid, gid: Gid, mode: u32) -> Result<(), Error> { 31 | let nix_mode = Mode::from_bits(mode).expect("bad mode bits for nix crate"); 32 | match nix::unistd::mkdir(dir, nix_mode) { 33 | Ok(()) => (), 34 | Err(nix::errno::Errno::EEXIST) => { 35 | CreateOptions::new() 36 | .owner(uid) 37 | .group(gid) 38 | .perm(nix_mode) 39 | .check(dir)?; 40 | 41 | check_permissions(dir, uid, gid, mode) 42 | .map_err(|err| format_err!("unexpected permissions directory '{dir}': {err}"))?; 43 | return Ok(()); 44 | } 45 | Err(err) => bail!("unable to create directory '{dir}' - {err}",), 46 | } 47 | 48 | let fd = nix::fcntl::open(dir, OFlag::O_DIRECTORY, Mode::empty()) 49 | .map(|fd| unsafe { OwnedFd::from_raw_fd(fd) }) 50 | .map_err(|err| format_err!("unable to open created directory '{dir}' - {err}"))?; 51 | // umask defaults to 022 so make sure the mode is fully honowed: 52 | nix::sys::stat::fchmod(fd.as_raw_fd(), nix_mode) 53 | .map_err(|err| format_err!("unable to set mode for directory '{dir}' - {err}"))?; 54 | nix::unistd::fchown(fd.as_raw_fd(), Some(uid), Some(gid)) 55 | .map_err(|err| format_err!("unable to set ownership directory '{dir}' - {err}"))?; 56 | 57 | Ok(()) 58 | } 59 | 60 | fn check_permissions(dir: &str, uid: Uid, gid: Gid, mode: u32) -> Result<(), Error> { 61 | let uid = uid.as_raw(); 62 | let gid = gid.as_raw(); 63 | 64 | let nix::sys::stat::FileStat { 65 | st_uid, 66 | st_gid, 67 | st_mode, 68 | .. 69 | } = nix::sys::stat::stat(dir).with_context(|| format!("failed to stat {dir:?}"))?; 70 | 71 | if st_uid != uid { 72 | log::error!("bad owner on {dir:?} ({st_uid} != {uid})"); 73 | } 74 | if st_gid != gid { 75 | log::error!("bad group on {dir:?} ({st_gid} != {gid})"); 76 | } 77 | let perms = st_mode & !nix::sys::stat::SFlag::S_IFMT.bits(); 78 | if perms != mode { 79 | log::error!("bad permissions on {dir:?} (0o{perms:o} != 0o{mode:o})"); 80 | } 81 | 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /ui/images/proxmox_logo_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ui/images/proxmox_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/dev/pdm-permission-system.md: -------------------------------------------------------------------------------- 1 | # ACL-Object path & Privileges 2 | 3 | 4 | ## `/system/{network,updates,disks,...}` 5 | 6 | For basic PDM system management. 7 | 8 | Privileges: 9 | 10 | - System.Audit 11 | - System.Modify 12 | 13 | ## `/resource/{remote-id}/{resource-type=guest,storage}/{resource-id}` 14 | 15 | To see, manage or modify specific resources. Keep resource-types rather minimal for now, e.g., no 16 | SDN or the node (host) for now, require the rest. 17 | 18 | - Resource.Audit -> read-only 19 | - Resource.Manage -> Migrate, Start, Stop, ... 20 | - Resource.Modify -> Change config or state of resource 21 | - Resource.Migrate -> Remote Migration 22 | - Resource.Delete -> Delete guests 23 | 24 | In the future we might extend this to something like: 25 | 26 | - Resource.Guest.Modify -> limited to guest related API calls and parameters on privilege level 27 | - Resource.Storage.Modify -> limited to storage related API calls and parameters on privilege level 28 | - Resource.User.Modify (once we integrated user and access control management of remotes, something 29 | for the mid/long-term future) 30 | 31 | The no-subtype ones, e.g. Resource.Modify, are seen as super-set of the per-resource type one. 32 | Should only be really evaluated after public feedback about the beta. 33 | 34 | ## `/access/{user,realm,acl}` 35 | 36 | To see or modify specific resources. Keep resource-types rather minimal for now, e.g., no 37 | SDN or the node (host) for now, require the rest. 38 | 39 | - Access.Audit -> read-only 40 | - Access.Modify -> Change config or state of resource 41 | 42 | We could also create sub-types to provide more flexibility, like: 43 | - Access.ACL.Modify 44 | - Access.User.Modify 45 | 46 | The biggest value from having a separate ACL and User modification privilege would be the ability to 47 | ensure on role-level that a user cannot give themselves more permissions. 48 | 49 | While that would speak for having this from the beginning, it's not a must from a technical POV, it 50 | could be still added later on, as it's an extension. 51 | 52 | # Roles 53 | 54 | - Administrator -> all, ideally only to allow permission modifications by default. 55 | - Auditor 56 | - SystemAdministrator 57 | - SystemAuditor 58 | - ResourceAdministrator 59 | - ResourceAuditor 60 | - AccessAuditor 61 | - ... can be extended in the future. 62 | 63 | ## Use cases to Support (TODO: is this still relevant?) 64 | 65 | - Simplest, one or more admin working on equal terms and using PDM to manage resources owned by the 66 | same entity (e.g., company) 67 | They want a simple way to add one API-token per Proxmox product to PDM 68 | - More complex admin hierarchy, or (support) staff involved, where some need to manage parts of 69 | their Proxmox infra, and some need to only audit part of the Proxmox infra, possibly on partially 70 | overlapping hosts sets. 71 | Flexible groups are required, some way to distinguish between admin/audit user while not blowing 72 | up complexity of different credentials to add for each Proxmox project 73 | 74 | IOW., we want to have a somewhat flexible system while not blowing out (potential) complexity out of 75 | proportions. 76 | -------------------------------------------------------------------------------- /ui/src/widget/remote_endpoint_selector.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use wasm_bindgen::UnwrapThrowExt; 4 | use yew::{ 5 | html::{IntoEventCallback, IntoPropValue}, 6 | AttrValue, Callback, Component, Properties, 7 | }; 8 | 9 | use pwt::{ 10 | props::{FieldBuilder, WidgetBuilder}, 11 | widget::form::Combobox, 12 | }; 13 | use pwt_macros::{builder, widget}; 14 | 15 | use crate::RemoteList; 16 | 17 | #[widget(comp=PdmEndpointSelector, @input)] 18 | #[derive(Clone, Properties, PartialEq)] 19 | #[builder] 20 | pub struct EndpointSelector { 21 | /// The default value 22 | #[builder(IntoPropValue, into_prop_value)] 23 | #[prop_or_default] 24 | pub default: Option, 25 | 26 | /// Change callback 27 | #[builder_cb(IntoEventCallback, into_event_callback, String)] 28 | #[prop_or_default] 29 | pub on_change: Option>, 30 | 31 | /// The remote to list Endpoints from 32 | #[builder(IntoPropValue, into_prop_value)] 33 | #[prop_or_default] 34 | pub remote: AttrValue, 35 | } 36 | 37 | impl EndpointSelector { 38 | pub fn new(remote: AttrValue) -> Self { 39 | yew::props!(Self { remote }) 40 | } 41 | } 42 | 43 | pub struct PdmEndpointSelector { 44 | endpoints: Rc>, 45 | } 46 | 47 | impl PdmEndpointSelector { 48 | fn update_endpoint_list(&mut self, ctx: &yew::Context) { 49 | let (remotes, _): (RemoteList, _) = ctx 50 | .link() 51 | .context(ctx.link().callback(|_| ())) 52 | .unwrap_throw(); 53 | 54 | let remote_id = ctx.props().remote.as_str(); 55 | 56 | for remote in remotes.iter() { 57 | if remote.id != remote_id { 58 | continue; 59 | } 60 | 61 | let endpoints = remote 62 | .nodes 63 | .iter() 64 | .map(|endpoint| AttrValue::from(endpoint.hostname.clone())) 65 | .collect(); 66 | self.endpoints = Rc::new(endpoints); 67 | break; 68 | } 69 | } 70 | } 71 | 72 | impl Component for PdmEndpointSelector { 73 | type Message = (); 74 | type Properties = EndpointSelector; 75 | 76 | fn create(ctx: &yew::Context) -> Self { 77 | let mut this = Self { 78 | endpoints: Rc::new(Vec::new()), 79 | }; 80 | 81 | this.update_endpoint_list(ctx); 82 | this 83 | } 84 | 85 | fn changed(&mut self, ctx: &yew::Context, old_props: &Self::Properties) -> bool { 86 | if ctx.props().remote != old_props.remote { 87 | self.update_endpoint_list(ctx); 88 | } 89 | true 90 | } 91 | 92 | fn view(&self, ctx: &yew::Context) -> yew::Html { 93 | let props = ctx.props(); 94 | Combobox::new() 95 | .with_std_props(&props.std_props) 96 | .with_input_props(&props.input_props) 97 | .on_change(props.on_change.clone()) 98 | .default(props.default.clone()) 99 | .items(self.endpoints.clone()) 100 | .into() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /ui/debian/control: -------------------------------------------------------------------------------- 1 | Source: proxmox-datacenter-manager-ui 2 | Section: admin 3 | Priority: optional 4 | Build-Depends: debhelper-compat (= 13), 5 | dh-cargo (>= 25), 6 | cargo:native, 7 | esbuild, 8 | fonts-font-awesome, 9 | librust-anyhow-1+default-dev, 10 | librust-futures-0.3+default-dev, 11 | librust-gloo-net-0.4+default-dev, 12 | librust-gloo-timers-0.3+default-dev, 13 | librust-gloo-utils-0.2+default-dev, 14 | librust-http-1+default-dev (>=1.2~~), 15 | librust-js-sys-0.3+default-dev (>= 0.3.69-~~), 16 | librust-log-0.4+default-dev (>= 0.4.6-~~), 17 | librust-pbs-api-types-1+default-dev (>= 1.0.3~~), 18 | librust-percent-encoding-2+default-dev (>= 2.1-~~), 19 | librust-proxmox-acme-api-1+default-dev, 20 | librust-proxmox-client-1+default-dev, 21 | librust-proxmox-deb-version-0.1-dev, 22 | librust-proxmox-human-byte-1+default-dev (>= 0.1.3-~~), 23 | librust-proxmox-login-1+default-dev, 24 | librust-proxmox-rrd-api-types-1+default-dev, 25 | librust-proxmox-schema-5+default-dev, 26 | librust-proxmox-yew-comp-0.8+apt-dev (>= 0.8.1-~~), 27 | librust-proxmox-yew-comp-0.8+default-dev (>= 0.8.1-~~), 28 | librust-proxmox-yew-comp-0.8+dns-dev (>= 0.8.1-~~), 29 | librust-proxmox-yew-comp-0.8+network-dev (>= 0.8.1-~~), 30 | librust-proxmox-yew-comp-0.8+rrd-dev (>= 0.8.1-~~), 31 | librust-pwt-0.7+default-dev, 32 | librust-pwt-macros-0.5+default-dev, 33 | librust-serde-1+default-dev, 34 | librust-serde-1+derive-dev, 35 | librust-serde-json-1+default-dev, 36 | librust-wasm-bindgen-0.2+default-dev (>= 0.2.92-~~), 37 | librust-wasm-bindgen-futures-0.4+default-dev, 38 | librust-wasm-logger-0.2+default-dev, 39 | librust-web-sys-0.3+default-dev, 40 | librust-web-sys-0.3+location-dev, 41 | librust-yew-0.21+csr-dev, 42 | librust-yew-0.21+default-dev, 43 | librust-yew-router-0.18+default-dev, 44 | proxmox-wasm-builder, 45 | rust-grass, 46 | rust-llvm, 47 | Maintainer: Proxmox Support Team 48 | Standards-Version: 4.6.1 49 | Vcs-Git: git://git.proxmox.com/git/proxmox-datacenter-manager.git 50 | Vcs-Browser: https://git.proxmox.com/?p=proxmox-datacenter-manager.git;a=summary 51 | Homepage: https://www.proxmox.com 52 | Rules-Requires-Root: no 53 | 54 | Package: proxmox-datacenter-manager-ui 55 | Architecture: any 56 | Multi-Arch: allowed 57 | Depends: fonts-font-awesome, 58 | pdm-i18n, 59 | pve-xtermjs, 60 | ${misc:Depends}, 61 | ${shlibs:Depends}, 62 | Recommends: proxmox-datacenter-manager, 63 | Description: Web UI to for the Proxmox Datacenter Manager 64 | This package provides the web UI of the Proxmox Datacenter Manager (PDM) 65 | which allows one to add multiple Proxmox VE and Proxmox Backup Server 66 | remotes and to manage these remotes from a central UI. 67 | -------------------------------------------------------------------------------- /server/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | //! Common API endpoints 2 | 3 | use anyhow::{bail, Error}; 4 | use pdm_api_types::{remotes::RemoteType, RemoteUpid}; 5 | use serde_json::{json, Value}; 6 | 7 | use proxmox_router::{list_subdirs_api_method, Permission, Router, SubdirMap}; 8 | use proxmox_schema::api; 9 | use proxmox_sortable_macro::sortable; 10 | 11 | pub mod access; 12 | pub mod config; 13 | pub mod metric_collection; 14 | pub mod nodes; 15 | pub mod pbs; 16 | pub mod pve; 17 | pub mod remote_shell; 18 | pub mod remote_tasks; 19 | pub mod remote_updates; 20 | pub mod remotes; 21 | pub mod resources; 22 | mod rrd_common; 23 | pub mod sdn; 24 | 25 | #[sortable] 26 | const SUBDIRS: SubdirMap = &sorted!([ 27 | ("access", &access::ROUTER), 28 | ("config", &config::ROUTER), 29 | ("ping", &Router::new().get(&API_METHOD_PING)), 30 | ("pve", &pve::ROUTER), 31 | ("pbs", &pbs::ROUTER), 32 | ("remotes", &remotes::ROUTER), 33 | ("resources", &resources::ROUTER), 34 | ("nodes", &nodes::ROUTER), 35 | ("sdn", &sdn::ROUTER), 36 | ("version", &Router::new().get(&API_METHOD_VERSION)), 37 | ]); 38 | 39 | pub const ROUTER: Router = Router::new() 40 | .get(&list_subdirs_api_method!(SUBDIRS)) 41 | .subdirs(SUBDIRS); 42 | 43 | #[api( 44 | access: { 45 | description: "Anyone can access this, just a cheap check if the API daemon is online.", 46 | permission: &Permission::World, 47 | }, 48 | returns: { 49 | type: String, 50 | description: "The string \"pong\"." 51 | } 52 | )] 53 | /// A simple ping method. returns "pong" 54 | fn ping() -> Result { 55 | Ok("pong".to_string()) 56 | } 57 | 58 | #[api( 59 | access: { 60 | description: "Any valid user can access this.", 61 | permission: &Permission::Anybody, 62 | }, 63 | returns: { 64 | type: Object, 65 | description: "Version information.", 66 | properties: { 67 | version: { 68 | type: String, 69 | description: "The version string." 70 | }, 71 | release: { 72 | type: String, 73 | description: "The package release.", 74 | }, 75 | repoid: { 76 | type: String, 77 | description: "The repoid." 78 | } 79 | } 80 | } 81 | )] 82 | /// Return the program's version/release info 83 | fn version() -> Result { 84 | Ok(json!({ 85 | "version": pdm_buildcfg::PROXMOX_PKG_VERSION, 86 | "release": pdm_buildcfg::PROXMOX_PKG_RELEASE, 87 | "repoid": pdm_buildcfg::PROXMOX_PKG_REPOID 88 | })) 89 | } 90 | 91 | /// Check a [`RemoteUpid`] matches the expected remote name and type. 92 | pub(crate) fn verify_upid( 93 | remote: &str, 94 | remote_type: RemoteType, 95 | upid: &RemoteUpid, 96 | ) -> Result<(), Error> { 97 | if upid.remote() != remote { 98 | bail!( 99 | "remote '{remote}' does not match remote in upid ('{}')", 100 | upid.remote() 101 | ); 102 | } 103 | if upid.remote_type() != remote_type { 104 | bail!("upid does not belong to a {remote_type} remote"); 105 | } 106 | 107 | Ok(()) 108 | } 109 | -------------------------------------------------------------------------------- /server/src/api/rrd_common.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::BTreeMap, time::Duration}; 2 | 3 | use anyhow::{bail, Error}; 4 | 5 | use proxmox_rrd_api_types::{RrdMode, RrdTimeframe}; 6 | 7 | use crate::metric_collection::{self, rrd_cache}; 8 | 9 | /// Trait common to all RRD-stored metric objects (nodes, datastores, qemu, lxc, etc.) 10 | pub trait DataPoint { 11 | /// Create a new data point with a given timestamp 12 | fn new(time: u64) -> Self; 13 | /// Returns the names of the underlying (stringly typed) fields in the RRD 14 | fn fields() -> &'static [&'static str]; 15 | /// Set a member by its field identifier 16 | fn set_field(&mut self, name: &str, value: f64); 17 | } 18 | 19 | pub fn create_datapoints_from_rrd( 20 | basedir: &str, 21 | timeframe: RrdTimeframe, 22 | mode: RrdMode, 23 | ) -> Result, Error> { 24 | let mut timemap = BTreeMap::new(); 25 | let mut last_resolution = None; 26 | 27 | let cache = rrd_cache::get_cache(); 28 | 29 | for name in T::fields() { 30 | let (start, resolution, data) = match cache.extract_data(basedir, name, timeframe, mode)? { 31 | Some(data) => data.into(), 32 | None => continue, 33 | }; 34 | 35 | if let Some(expected_resolution) = last_resolution { 36 | if resolution != expected_resolution { 37 | bail!("got unexpected RRD resolution ({resolution} != {expected_resolution})",); 38 | } 39 | } else { 40 | last_resolution = Some(resolution); 41 | } 42 | 43 | let mut t = start; 44 | 45 | for value in data { 46 | let entry = timemap.entry(t).or_insert_with(|| T::new(t)); 47 | if let Some(value) = value { 48 | entry.set_field(name, value); 49 | } 50 | 51 | t += resolution; 52 | } 53 | } 54 | 55 | Ok(timemap.into_values().collect()) 56 | } 57 | 58 | /// Get RRD datapoints for a given remote/RRD path. 59 | /// 60 | /// If `timeframe` is set to [`RrdTimeframe::Hour`], then this function will trigger 61 | /// metric collection for this remote and wait for its completion, up to a timeout of five 62 | /// seconds. If the timeout is exceeded, we simply go ahead and return what is in the database at 63 | /// the moment, which might have a gap for the last couple minutes. 64 | pub async fn get_rrd_datapoints( 65 | remote: String, 66 | basepath: String, 67 | timeframe: RrdTimeframe, 68 | mode: RrdMode, 69 | ) -> Result, Error> { 70 | const WAIT_FOR_NEWEST_METRIC_TIMEOUT: Duration = Duration::from_secs(5); 71 | 72 | if timeframe == RrdTimeframe::Hour { 73 | // Let's wait for a limited time for the most recent metrics. If the connection to the remote 74 | // is super slow or if the metric collection tasks currently busy with collecting other 75 | // metrics, we just return the data we already have, not the newest one. 76 | let _ = tokio::time::timeout(WAIT_FOR_NEWEST_METRIC_TIMEOUT, async { 77 | metric_collection::trigger_metric_collection(Some(remote), true).await 78 | }) 79 | .await; 80 | } 81 | 82 | tokio::task::spawn_blocking(move || create_datapoints_from_rrd(&basepath, timeframe, mode)) 83 | .await? 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Proxmox Datacenter Manager 2 | 3 | A stand-alone API + GUI product with the following main features for multiple instances of Proxmox 4 | VE and Proxmox Backup Server in one central place. 5 | 6 | ## Feature Overview 7 | 8 | - Connect & display an arbitrary amount of independent nodes or clusters ("Remotes") 9 | - View the status and load of all resources, which includes nodes, virtual guests, storages, 10 | datastores and so on. Proxmox Datacenter Manager provides a dashboard that tries to present 11 | information such that potential problematic outliers can be found easily. 12 | - Customizable dashboards ("views") showing a configurable subset of resources 13 | - Basic management of the guest resources 14 | - Resource graphs 15 | - Basic power management (start, reboot, shutdown) 16 | - Remote shell for Proxmox VE and Proxmox Backup Server remotes 17 | - Global overview over available system updates for managed remotes 18 | - Firewall overview for all managed remotes 19 | - Basic SDN overview for all managed remotes 20 | - Remote migration of virtual guests between different datacenters 21 | Advertising use of ZFS & Ceph backed replication for quicker transfer on actual migration 22 | - View configuration health state (subscription, APT repositories, pending updates, ...) 23 | - User management / access control 24 | - Users/API token 25 | - Support for LDAP and Active Directory 26 | - Support for OpenID Connect 27 | - Support for complex Two-Factor Authentication 28 | - ACME/Let's Encrypt 29 | 30 | - A non-exhaustive list of features planned for future releases is: 31 | - Management of more configuration (e.g. backup jobs, notification policies, package repos, HA) 32 | - Active-standby architecture for standby instances of PDM to avoid single point of failure. 33 | - Integration of other projects, like Proxmox Mail Gateway, and potentially also Proxmox Offline Mirror. 34 | - Off-site replication copies of guest for manual recovery on DC failure (not HA!) 35 | - ... to be determined from user feedback and feature requests. 36 | 37 | ## Technology Overview 38 | 39 | ### Backend 40 | - Implemented in the Rust programming language, reusing code from Proxmox Backup Server where possible 41 | - A for Proxmox projects standard dual-stack of API daemons. One as main API daemon running as 42 | unprivileged users and one privileged daemon running as root. Contrary to other projects the 43 | privileged daemon exclusively listens on a file based UNIX socket, thus restricting attack surface 44 | even further. 45 | - The backend listens on port 8443 (TLS only) 46 | - The code for the backend server is located in the `server/` directory. 47 | 48 | ### Frontend 49 | 50 | - The Web UI communicates with the backend server via a JSON-based REST API. 51 | - The UI is implemented in Rust, using [Yew](https://yew.rs/) and the 52 | [proxmox-yew-widget-toolkit](https://git.proxmox.com/?p=ui/proxmox-yew-widget-toolkit.git;a=summary). 53 | The Rust code is compiled to WebAssembly. 54 | - The code for the UI is located in the `ui/` directory. 55 | 56 | ### CLI tools 57 | 58 | There are two CLI tools to manage Proxmox Datacenter Manager. 59 | - `proxmox-datacenter-manager-client`: client using the PDM API, can be used to 60 | control local or remote PDM instances 61 | - `proxmox-datacenter-manager-admin`: root-only, local administration tool 62 | 63 | Their implementation can be found in `cli/admin` and `cli/client`, respectively. 64 | 65 | 66 | ## Documentation 67 | 68 | Documentation (user-facing as well as developer-facing) can be found in `docs/`. 69 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _install_pdm: 2 | 3 | Installation 4 | ============ 5 | 6 | `Proxmox Datacenter Manager`_ can either be installed with a graphical 7 | installer or on top of Debian_ from the provided package repository. 8 | 9 | .. include:: system-requirements.rst 10 | 11 | .. include:: installation-media.rst 12 | 13 | Using our provided disk image (ISO file) is the recommended installation 14 | method, as it includes a convenient installer, a complete Debian system as well 15 | as all necessary packages for the Proxmox Datacenter Manager. 16 | 17 | Once you have created an :ref:`installation_medium`, the booted :ref:`installer 18 | ` will guide you through the setup process. It will help 19 | you to partition your disks, apply basic settings such as the language, time 20 | zone and network configuration, and finally install all required packages 21 | within minutes. 22 | 23 | As an alternative to the interactive installer, advanced users may wish to 24 | install Proxmox Datacenter Manager :ref:`unattended `. 25 | 26 | With sufficient Debian knowledge, you can also install Proxmox Datacenter 27 | Manager :ref:`on top of Debian ` yourself. 28 | 29 | .. todo 30 | .. include:: using-the-installer.rst 31 | 32 | .. _install_pdm_unattended: 33 | 34 | Install Proxmox Datacenter Manager Unattended 35 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 36 | 37 | It is possible to install Proxmox Datacenter Manager automatically in an 38 | unattended manner. This enables you to fully automate the setup process on 39 | bare-metal. Once the installation is complete and the host has booted up, 40 | automation tools like Ansible can be used to further configure the installation. 41 | 42 | The necessary options for the installer must be provided in an answer file. 43 | This file allows the use of filter rules to determine which disks and network 44 | cards should be used. 45 | 46 | To use the automated installation, it is first necessary to prepare an 47 | installation ISO. For more details and information on the unattended 48 | installation see `our wiki 49 | `_. 50 | 51 | .. _install_pdm_on_debian: 52 | 53 | Install Proxmox Datacenter Manager on Debian 54 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 55 | 56 | Proxmox ships as a set of Debian packages which can be installed on top of a 57 | standard Debian installation. After configuring the 58 | :ref:`sysadmin_package_repositories`, you need to run: 59 | 60 | .. code-block:: console 61 | 62 | # apt update 63 | # apt install proxmox-datacenter-manager proxmox-datacenter-manager-ui 64 | 65 | The above commands keep the current (Debian) kernel and install a minimal set 66 | of required packages. 67 | 68 | You can install the Proxmox default kernel with ZFS support by using: 69 | 70 | .. code-block:: console 71 | 72 | # apt update 73 | # apt install proxmox-default-kernel 74 | 75 | .. 76 | add meta package 77 | 78 | .. caution:: Installing Proxmox Datacenter Manager on top of an existing Debian_ 79 | installation looks easy, but it assumes that the base system and local 80 | storage have been set up correctly. In general this is not trivial, especially 81 | when LVM_ or ZFS_ is used. The network configuration is completely up to you 82 | as well. 83 | 84 | .. Note:: You can access the web interface of the Proxmox Datacenter Manager with 85 | your web browser, using HTTPS on port 8443. For example at 86 | ``https://:8443`` 87 | 88 | .. include:: package-repositories.rst 89 | -------------------------------------------------------------------------------- /docs/faq.rst: -------------------------------------------------------------------------------- 1 | FAQ 2 | === 3 | 4 | What distribution is Proxmox Datacenter Manager (PDM) based on? 5 | --------------------------------------------------------------- 6 | 7 | Proxmox Datacenter Manager is based on `Debian GNU/Linux `_. 8 | 9 | 10 | Will Proxmox Datacenter Manager run on a 32-bit processor? 11 | ---------------------------------------------------------- 12 | 13 | Proxmox Datacenter Manager only supports 64-bit CPUs (AMD or Intel). There are no future plans to 14 | support 32-bit processors. 15 | 16 | 17 | .. _faq-support-table: 18 | 19 | How long will my Proxmox Datacenter Manager version be supported? 20 | ----------------------------------------------------------------- 21 | 22 | .. csv-table:: 23 | :file: faq-release-support-table.csv 24 | :widths: 30 26 13 13 18 25 | :header-rows: 1 26 | 27 | How can I upgrade Proxmox Datacenter Manager to the next point release? 28 | ----------------------------------------------------------------------- 29 | 30 | Minor version upgrades, for example upgrading from Proxmox Datacenter Manager in rersion 1.0 to 1.1 31 | or 1.3, can be done just like any normal update. 32 | 33 | But, you should still check the `release notes `_ for any 34 | relevant notable, or breaking change. 35 | 36 | For the update itself use either the Web UI *Administration -> Updates* panel or through the CLI 37 | with: 38 | 39 | .. code-block:: console 40 | 41 | apt update 42 | apt full-upgrade 43 | 44 | .. note:: Always ensure you correctly setup the :ref:`package repositories 45 | ` and only continue with the actual upgrade if `apt update` did 46 | not hit any error. 47 | 48 | .. 49 | .. _faq-upgrade-major: 50 | 51 | How can I upgrade Proxmox Datacenter Manager to the next major release? 52 | ----------------------------------------------------------------------- 53 | 54 | Major version upgrades, for example going from Proxmox Datacenter Manager 1.3 to 2.1, are also 55 | supported. 56 | They must be carefully planned and tested and should **never** be started without having successfully 57 | tested backups. 58 | 59 | Although the specific upgrade steps depend on your respective setup, we provide general instructions 60 | and advice of how a upgrade should be performed: 61 | 62 | * `Upgrade from Proxmox Datacenter Manager 1 to 2 `_ 63 | 64 | .. _faq-subscription: 65 | 66 | Is there a dedicated subscription for the Proxmox Datacenter Manager? 67 | --------------------------------------------------------------------- 68 | 69 | No, there is not. However, your existing Basic or higher subscription for Proxmox VE and Proxmox 70 | Backup Server remotes includes access to the Proxmox Datacenter Manager Enterprise Repository and 71 | support at no extra cost. 72 | 73 | .. _faq-enterprise-support: 74 | 75 | How can I get Enterprise Support for the Proxmox Datacenter Manager? 76 | -------------------------------------------------------------------- 77 | 78 | Existing customers with active Basic or higher subscriptions for their Proxmox remotes also gain 79 | access to the Proxmox Datacenter Manager enterprise repository and support. 80 | 81 | .. _faq-enterprise-repository: 82 | 83 | How can I get access to the Proxmox Datacenter Manager Enterprise Repository? 84 | ----------------------------------------------------------------------------- 85 | 86 | The Proxmox Datacenter Manager can use the enterprise repository if at least 80% of the configured 87 | remote nodes have a valid Basic or higher subscription. 88 | -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "server" 3 | description = "Proxmox Datacenter Manager Common API parts" 4 | homepage = "https://www.proxmox.com" 5 | 6 | version.workspace = true 7 | edition.workspace = true 8 | license.workspace = true 9 | repository.workspace = true 10 | 11 | [dependencies] 12 | anyhow.workspace = true 13 | async-stream.workspace = true 14 | async-trait.workspace = true 15 | const_format.workspace = true 16 | futures.workspace = true 17 | hex.workspace = true 18 | http.workspace = true 19 | http-body-util.workspace = true 20 | hyper.workspace = true 21 | hyper-util.workspace = true 22 | libc.workspace = true 23 | log.workspace = true 24 | nix.workspace = true 25 | once_cell.workspace = true 26 | openssl.workspace = true 27 | percent-encoding.workspace = true 28 | serde.workspace = true 29 | serde_json.workspace = true 30 | serde_plain.workspace = true 31 | syslog.workspace = true 32 | tokio = { workspace = true, features = [ "fs", "io-util", "io-std", "macros", "net", "parking_lot", "process", "rt", "rt-multi-thread", "signal", "time" ] } 33 | tokio-stream.workspace = true 34 | tracing.workspace = true 35 | url.workspace = true 36 | zstd.workspace = true 37 | 38 | proxmox-access-control = { workspace = true, features = [ "api" ] } 39 | proxmox-async.workspace = true 40 | proxmox-auth-api = { workspace = true, features = [ "api", "ticket", "pam-authenticator", "password-authenticator" ] } 41 | proxmox-base64.workspace = true 42 | proxmox-daemon.workspace = true 43 | proxmox-docgen.workspace = true 44 | proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these 45 | proxmox-lang.workspace = true 46 | proxmox-ldap.workspace = true 47 | proxmox-log.workspace = true 48 | proxmox-login.workspace = true 49 | proxmox-openid.workspace = true 50 | proxmox-rest-server = { workspace = true, features = [ "templates" ] } 51 | proxmox-router = { workspace = true, features = [ "cli", "server"] } 52 | proxmox-rrd.workspace = true 53 | proxmox-rrd-api-types.workspace = true 54 | proxmox-schema = { workspace = true, features = [ "api-macro" ] } 55 | proxmox-section-config.workspace = true 56 | proxmox-serde = { workspace = true, features = [ "serde_json" ] } 57 | proxmox-sortable-macro.workspace = true 58 | proxmox-subscription = { workspace = true, features = [ "api-types", "impl" ] } 59 | proxmox-sys = { workspace = true, features = [ "timer" ] } 60 | proxmox-systemd.workspace = true 61 | proxmox-tfa = { workspace = true, features = [ "api" ] } 62 | proxmox-time.workspace = true 63 | proxmox-uuid.workspace = true 64 | 65 | proxmox-apt = { workspace = true, features = [ "cache" ] } 66 | proxmox-apt-api-types.workspace = true 67 | 68 | proxmox-client = { workspace = true, features = [ "hyper-client"] } 69 | 70 | proxmox-config-digest = { workspace = true, features = [ "openssl" ] } 71 | proxmox-product-config.workspace = true 72 | proxmox-dns-api = { workspace = true, features = [ "impl" ] } 73 | proxmox-time-api = { workspace = true, features = [ "impl" ] } 74 | proxmox-network-api = { workspace = true, features = [ "impl" ] } 75 | proxmox-syslog-api = { workspace = true, features = [ "impl" ] } 76 | proxmox-acme-api = { workspace = true, features = [ "impl" ] } 77 | proxmox-node-status = { workspace = true, features = [ "api" ] } 78 | 79 | pdm-api-types.workspace = true 80 | pdm-buildcfg.workspace = true 81 | pdm-config.workspace = true 82 | pdm-search.workspace = true 83 | 84 | pve-api-types = { workspace = true, features = [ "client" ] } 85 | pbs-api-types.workspace = true 86 | 87 | [lints.rust.unexpected_cfgs] 88 | level = "warn" 89 | check-cfg = ['cfg(remote_config, values("faked"))'] 90 | -------------------------------------------------------------------------------- /ui/css/pdm.scss: -------------------------------------------------------------------------------- 1 | .pdm-type-icon { 2 | position: relative; 3 | 4 | .status-icon { 5 | text-shadow: 0px 0px 2px var(--pwt-color-background); 6 | position: absolute; 7 | right: -8px; 8 | bottom: 0px; 9 | font-size: 80%; 10 | } 11 | } 12 | 13 | .pve-guest-icon { 14 | position: relative; 15 | 16 | .status-icon { 17 | text-shadow: 0px 0px 2px var(--pwt-color-background); 18 | position: absolute; 19 | right: -6px; 20 | bottom: 1px; 21 | font-size: 80%; 22 | } 23 | } 24 | 25 | .pve-tags { 26 | text-overflow: ellipsis; 27 | .pve-tag { 28 | border-radius: var(--pwt-button-corner-shape); 29 | padding-inline: var(--pwt-spacer-2) 30 | } 31 | } 32 | 33 | .line-usage-graph { 34 | border-radius: var(--pwt-button-corner-shape); 35 | height: map-get($line-height, "label-large"); 36 | } 37 | 38 | .fa-cpu::before { 39 | content: " "; 40 | background-image: url(./images/icon-cpu.svg); 41 | background-size: 16px 16px; 42 | background-repeat: no-repeat; 43 | width: 16px; 44 | height: 16px; 45 | vertical-align: bottom; 46 | display: inline-block; 47 | } 48 | 49 | .fa-memory::before { 50 | content: " "; 51 | background-image: url(./images/icon-memory.svg); 52 | background-size: 16px 16px; 53 | background-repeat: no-repeat; 54 | width: 16px; 55 | height: 16px; 56 | vertical-align: bottom; 57 | display: inline-block; 58 | } 59 | 60 | .fa-cdrom::before { 61 | content: " "; 62 | background-image: url(./images/icon-cd-drive.svg); 63 | background-size: 16px 16px; 64 | background-repeat: no-repeat; 65 | width: 16px; 66 | height: 16px; 67 | vertical-align: bottom; 68 | display: inline-block; 69 | } 70 | 71 | .fa-sdn-vnet::before { 72 | content: " "; 73 | mask-image: url(./images/icon-sdn-vnet.svg); 74 | mask-size: 16px 16px; 75 | mask-repeat: no-repeat; 76 | background-color: var(--pwt-color); 77 | width: 16px; 78 | height: 16px; 79 | vertical-align: bottom; 80 | display: inline-block; 81 | } 82 | 83 | .fa-sdn:before { 84 | content: " "; 85 | mask-image: url(../images/icon-sdn.svg); 86 | mask-size: 16px 16px; 87 | mask-repeat: no-repeat; 88 | background-color: var(--pwt-color); 89 | width: 16px; 90 | height: 16px; 91 | vertical-align: middle; 92 | display: inline-block; 93 | } 94 | 95 | .pwt-nav-menu .pwt-nav-link.active{ 96 | .fa-sdn:before, 97 | .fa-sdn-vnet:before { 98 | background-color: var(--pwt-accent-color); 99 | } 100 | } 101 | 102 | .pwt-panel-header-text{ 103 | .fa-sdn:before, 104 | .fa-sdn-vnet:before { 105 | background-color: var(--pwt-accent-color-background); 106 | } 107 | } 108 | 109 | :root.pwt-dark-mode { 110 | .fa-cdrom, 111 | .fa-memory, 112 | .fa-cpu { 113 | filter: invert(90%); 114 | } 115 | } 116 | 117 | .proxmox-content-spacer { 118 | @include color-scheme-vars("surface"); 119 | color: var(--pwt-color); 120 | background-color: var(--pwt-color-background); 121 | 122 | padding: var(--pwt-spacer-2); 123 | gap: var(--pwt-spacer-2); 124 | 125 | display: flex; 126 | flex-direction: column; 127 | 128 | & > * { 129 | @include color-scheme-vars("neutral"); 130 | border: 1px solid var(--pwt-color-border); 131 | color: var(--pwt-color); 132 | background-color: var(--pwt-color-background); 133 | } 134 | } 135 | 136 | .dragging-item { 137 | opacity: 0.5; 138 | } 139 | -------------------------------------------------------------------------------- /cli/admin/src/main.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{json, Value}; 2 | 3 | use proxmox_router::cli::{ 4 | default_table_format_options, format_and_print_result_full, get_output_format, run_cli_command, 5 | CliCommand, CliCommandMap, CliEnvironment, ColumnConfig, OUTPUT_FORMAT, 6 | }; 7 | use proxmox_router::RpcEnvironment; 8 | 9 | use proxmox_schema::api; 10 | 11 | mod remotes; 12 | mod support_status; 13 | 14 | fn main() { 15 | //pbs_tools::setup_libc_malloc_opts(); // TODO: move from PBS to proxmox-sys and uncomment 16 | 17 | let api_user = pdm_config::api_user().expect("cannot get api user"); 18 | let priv_user = pdm_config::priv_user().expect("cannot get privileged user"); 19 | proxmox_product_config::init(api_user, priv_user); 20 | 21 | proxmox_access_control::init::init( 22 | &pdm_api_types::AccessControlConfig, 23 | pdm_buildcfg::configdir!("/access"), 24 | ) 25 | .expect("failed to setup access control config"); 26 | 27 | proxmox_log::Logger::from_env("PDM_LOG", proxmox_log::LevelFilter::INFO) 28 | .stderr() 29 | .init() 30 | .expect("failed to set up logger"); 31 | 32 | server::context::init().expect("could not set up server context"); 33 | 34 | let cmd_def = CliCommandMap::new() 35 | .insert("remote", remotes::cli()) 36 | .insert( 37 | "report", 38 | CliCommand::new(&API_METHOD_GENERATE_SYSTEM_REPORT), 39 | ) 40 | .insert("support-status", support_status::cli()) 41 | .insert("versions", CliCommand::new(&API_METHOD_GET_VERSIONS)); 42 | 43 | let mut rpcenv = CliEnvironment::new(); 44 | rpcenv.set_auth_id(Some("root@pam".into())); 45 | 46 | run_cli_command( 47 | cmd_def, 48 | rpcenv, 49 | Some(|future| proxmox_async::runtime::main(future)), 50 | ); 51 | } 52 | 53 | #[api( 54 | input: { 55 | properties: { 56 | verbose: { 57 | type: Boolean, 58 | optional: true, 59 | default: false, 60 | description: "Output verbose package information. It is ignored if output-format is specified.", 61 | }, 62 | "output-format": { 63 | schema: OUTPUT_FORMAT, 64 | optional: true, 65 | } 66 | } 67 | } 68 | )] 69 | /// List package versions for important Proxmox Datacenter Manager packages. 70 | async fn get_versions(verbose: bool, param: Value) -> Result { 71 | let output_format = get_output_format(¶m); 72 | 73 | let packages = server::api::nodes::apt::get_versions()?; 74 | let mut packages = json!(if verbose { 75 | &packages[..] 76 | } else { 77 | &packages[1..2] 78 | }); 79 | 80 | let options = default_table_format_options() 81 | .disable_sort() 82 | .noborder(true) // just not helpful for version info which gets copy pasted often 83 | .column(ColumnConfig::new("Package")) 84 | .column(ColumnConfig::new("Version")) 85 | .column(ColumnConfig::new("ExtraInfo").header("Extra Info")); 86 | let return_type = &server::api::nodes::apt::API_METHOD_GET_VERSIONS.returns; 87 | 88 | format_and_print_result_full(&mut packages, return_type, &output_format, &options); 89 | 90 | Ok(Value::Null) 91 | } 92 | 93 | #[api] 94 | /// Generate the system report. 95 | async fn generate_system_report() -> Result<(), anyhow::Error> { 96 | let report = server::api::nodes::report::generate_system_report()?; 97 | print!("{report}"); 98 | 99 | Ok(()) 100 | } 101 | -------------------------------------------------------------------------------- /server/src/api/nodes/sdn.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Error}; 2 | use http::StatusCode; 3 | 4 | use pdm_api_types::{remotes::REMOTE_ID_SCHEMA, sdn::SDN_ID_SCHEMA, NODE_SCHEMA}; 5 | use proxmox_router::{list_subdirs_api_method, Router, SubdirMap}; 6 | use proxmox_schema::api; 7 | use pve_api_types::{SdnVnetMacVrf, SdnZoneIpVrf}; 8 | 9 | use crate::api::pve::{connect, get_remote}; 10 | 11 | mod zones { 12 | use super::*; 13 | 14 | const ZONE_SUBDIRS: SubdirMap = &[("ip-vrf", &Router::new().get(&API_METHOD_GET_IP_VRF))]; 15 | 16 | const ZONE_ROUTER: Router = Router::new() 17 | .get(&list_subdirs_api_method!(ZONE_SUBDIRS)) 18 | .subdirs(ZONE_SUBDIRS); 19 | 20 | pub const ROUTER: Router = Router::new().match_all("zone", &ZONE_ROUTER); 21 | 22 | #[api( 23 | input: { 24 | properties: { 25 | remote: { schema: REMOTE_ID_SCHEMA }, 26 | node: { schema: NODE_SCHEMA }, 27 | zone: { schema: SDN_ID_SCHEMA }, 28 | }, 29 | }, 30 | returns: { type: SdnZoneIpVrf }, 31 | )] 32 | /// Get the IP-VRF for an EVPN zone for a node on a given remote 33 | async fn get_ip_vrf( 34 | remote: String, 35 | node: String, 36 | zone: String, 37 | ) -> Result, Error> { 38 | let (remote_config, _) = pdm_config::remotes::config()?; 39 | let remote = get_remote(&remote_config, &remote)?; 40 | let client = connect(remote)?; 41 | 42 | client 43 | .get_zone_ip_vrf(&node, &zone) 44 | .await 45 | .map_err(|err| match err { 46 | proxmox_client::Error::Api(StatusCode::NOT_IMPLEMENTED, _msg) => { 47 | anyhow!("remote {} does not support the zone ip-vrf API call, please upgrade to the newest version!", remote.id) 48 | } 49 | _ => err.into() 50 | }) 51 | } 52 | } 53 | 54 | mod vnets { 55 | use super::*; 56 | 57 | const VNET_SUBDIRS: SubdirMap = &[("mac-vrf", &Router::new().get(&API_METHOD_GET_MAC_VRF))]; 58 | 59 | const VNET_ROUTER: Router = Router::new() 60 | .get(&list_subdirs_api_method!(VNET_SUBDIRS)) 61 | .subdirs(VNET_SUBDIRS); 62 | 63 | pub const ROUTER: Router = Router::new().match_all("vnet", &VNET_ROUTER); 64 | 65 | #[api( 66 | input: { 67 | properties: { 68 | remote: { schema: REMOTE_ID_SCHEMA }, 69 | node: { schema: NODE_SCHEMA }, 70 | vnet: { schema: SDN_ID_SCHEMA }, 71 | }, 72 | }, 73 | returns: { type: SdnVnetMacVrf }, 74 | )] 75 | /// Get the MAC-VRF for an EVPN vnet for a node on a given remote 76 | async fn get_mac_vrf( 77 | remote: String, 78 | node: String, 79 | vnet: String, 80 | ) -> Result, Error> { 81 | let (remote_config, _) = pdm_config::remotes::config()?; 82 | let remote = get_remote(&remote_config, &remote)?; 83 | let client = connect(&remote)?; 84 | 85 | client 86 | .get_vnet_mac_vrf(&node, &vnet) 87 | .await 88 | .map_err(|err| match err { 89 | proxmox_client::Error::Api(StatusCode::NOT_IMPLEMENTED, _msg) => { 90 | anyhow!("remote {} does not support the vnet mac-vrf API call, please upgrade to the newest version!", remote.id) 91 | } 92 | _ => err.into() 93 | }) 94 | } 95 | } 96 | 97 | const SUBDIRS: SubdirMap = &[("vnets", &vnets::ROUTER), ("zones", &zones::ROUTER)]; 98 | 99 | pub const ROUTER: Router = Router::new() 100 | .get(&list_subdirs_api_method!(SUBDIRS)) 101 | .subdirs(SUBDIRS); 102 | -------------------------------------------------------------------------------- /lib/pdm-buildcfg/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Exports configuration data from the build system 2 | 3 | pub const PROXMOX_PKG_VERSION: &str = concat!( 4 | env!("CARGO_PKG_VERSION_MAJOR"), 5 | ".", 6 | env!("CARGO_PKG_VERSION_MINOR"), 7 | ); 8 | pub const PROXMOX_PKG_RELEASE: &str = env!("CARGO_PKG_VERSION_PATCH"); 9 | pub const PROXMOX_PKG_REPOID: &str = env!("REPOID"); 10 | 11 | /// The configured configuration directory 12 | pub const CONFIGDIR: &str = "/etc/proxmox-datacenter-manager"; 13 | pub const JS_DIR: &str = "/usr/share/javascript/proxmox-datacenter-manager"; 14 | 15 | // FIXME use (and add) pdm user? 16 | /// Unix system user used by proxmox-datacenter-api 17 | pub const BACKUP_USER_NAME: &str = "www-data"; 18 | /// Unix system group used by proxmox-datacenter-api 19 | pub const BACKUP_GROUP_NAME: &str = "www-data"; 20 | 21 | #[macro_export] 22 | macro_rules! PDM_RUN_DIR_M { 23 | () => { 24 | "/run/proxmox-datacenter-manager" 25 | }; 26 | } 27 | 28 | #[macro_export] 29 | macro_rules! PDM_STATE_DIR_M { 30 | () => { 31 | "/var/lib/proxmox-datacenter-manager" 32 | }; 33 | } 34 | 35 | #[macro_export] 36 | macro_rules! PDM_LOG_DIR_M { 37 | () => { 38 | "/var/log/proxmox-datacenter-manager" 39 | }; 40 | } 41 | 42 | #[macro_export] 43 | macro_rules! PDM_CACHE_DIR_M { 44 | () => { 45 | "/var/cache/proxmox-datacenter-manager" 46 | }; 47 | } 48 | 49 | /// Default port to use for PDM services. 50 | pub const PDM_PORT: u16 = 8443; 51 | 52 | /// namespaced directory for in-memory (tmpfs) run state 53 | pub const PDM_RUN_DIR: &str = PDM_RUN_DIR_M!(); 54 | 55 | /// namespaced directory for persistent cache 56 | pub const PDM_CACHE_DIR: &str = PDM_CACHE_DIR_M!(); 57 | 58 | /// namespaced directory for persistent state 59 | pub const PDM_STATE_DIR: &str = PDM_STATE_DIR_M!(); 60 | 61 | /// namespaced directory for persistent logging 62 | pub const PDM_LOG_DIR: &str = PDM_LOG_DIR_M!(); 63 | 64 | /// logfile for all API requests handled by the api server and privileged API daemons. Note that 65 | /// not all failed logins can be logged here with full information, use the auth log for that. 66 | pub const API_ACCESS_LOG_FN: &str = concat!(PDM_LOG_DIR_M!(), "/api/access.log"); 67 | 68 | /// logfile for any failed authentication, via ticket or via token, and new successful ticket 69 | /// creations. This file can be useful for fail2ban. 70 | pub const API_AUTH_LOG_FN: &str = concat!(PDM_LOG_DIR_M!(), "/api/auth.log"); 71 | 72 | /// the PID filename for the unprivileged api daemon 73 | pub const PDM_API_PID_FN: &str = concat!(PDM_RUN_DIR_M!(), "/api.pid"); 74 | 75 | /// the PID filename for the privileged api daemon 76 | pub const PDM_PRIVILEGED_API_PID_FN: &str = concat!(PDM_RUN_DIR_M!(), "/priv.pid"); 77 | 78 | /// The privileged api socket file. 79 | pub const PDM_PRIVILEGED_API_SOCKET_FN: &str = concat!(PDM_RUN_DIR_M!(), "/priv.sock"); 80 | 81 | pub const PDM_SUBSCRIPTION_FN: &str = configdir!("/subscription"); 82 | 83 | pub const APT_PKG_STATE_FN: &str = concat!(PDM_STATE_DIR_M!(), "/pkg-state.json"); 84 | 85 | /// Prepend configuration directory to a file name 86 | /// 87 | /// This is a simply way to get the full path for configuration files. 88 | /// #### Example: 89 | /// ``` 90 | /// use pdm_buildcfg::configdir; 91 | /// let cert_path = configdir!("/api.pfx"); 92 | /// ``` 93 | #[macro_export] 94 | macro_rules! configdir { 95 | ($subdir:expr) => { 96 | concat!("/etc/proxmox-datacenter-manager", $subdir) 97 | }; 98 | } 99 | 100 | /// Prepend the run directory to a file name. 101 | /// 102 | /// This is a simply way to get the full path for files in `/run`. 103 | #[macro_export] 104 | macro_rules! rundir { 105 | ($subdir:expr) => { 106 | concat!($crate::PDM_RUN_DIR_M!(), $subdir) 107 | }; 108 | } 109 | --------------------------------------------------------------------------------